diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index ef22e9dfd73e6..7a75d6e925f8f 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -9206,7 +9206,7 @@ namespace ts { } } const sourceTypes = some(constructorTypes, t => !!(t.flags & ~TypeFlags.Nullable)) ? constructorTypes : types; // TODO: GH#18217 - type = getUnionType(sourceTypes!, UnionReduction.Subtype); + type = getUnionType(sourceTypes!); } } const widened = getWidenedType(addOptionality(type, /*isProperty*/ false, definedInMethod && !definedInConstructor)); diff --git a/src/compiler/moduleNameResolver.ts b/src/compiler/moduleNameResolver.ts index 874344ae321be..15022bd59f67f 100644 --- a/src/compiler/moduleNameResolver.ts +++ b/src/compiler/moduleNameResolver.ts @@ -69,7 +69,8 @@ namespace ts { JavaScript, /** '.js' or '.jsx' */ Json, /** '.json' */ TSConfig, /** '.json' with `tsconfig` used instead of `index` */ - DtsOnly /** Only '.d.ts' */ + DtsOnly, /** Only '.d.ts' */ + TsOnly, /** '.[cm]tsx?' but not .d.ts variants */ } interface PathAndPackageId { @@ -1290,7 +1291,19 @@ namespace ts { export function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache, redirectedReference?: ResolvedProjectReference): ResolvedModuleWithFailedLookupLocations; /* @internal */ export function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache, redirectedReference?: ResolvedProjectReference, lookupConfig?: boolean): ResolvedModuleWithFailedLookupLocations; // eslint-disable-line @typescript-eslint/unified-signatures export function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache, redirectedReference?: ResolvedProjectReference, lookupConfig?: boolean): ResolvedModuleWithFailedLookupLocations { - return nodeModuleNameResolverWorker(NodeResolutionFeatures.None, moduleName, getDirectoryPath(containingFile), compilerOptions, host, cache, lookupConfig ? tsconfigExtensions : (compilerOptions.resolveJsonModule ? tsPlusJsonExtensions : tsExtensions), redirectedReference); + 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, getDirectoryPath(containingFile), compilerOptions, host, cache, extensions, redirectedReference); } function nodeModuleNameResolverWorker(features: NodeResolutionFeatures, moduleName: string, containingDirectory: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache: ModuleResolutionCache | undefined, extensions: Extensions[], redirectedReference: ResolvedProjectReference | undefined): ResolvedModuleWithFailedLookupLocations { @@ -1299,6 +1312,11 @@ namespace ts { const failedLookupLocations: string[] = []; // conditions are only used by the node12/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 state: ModuleResolutionState = { compilerOptions, host, @@ -1306,7 +1324,7 @@ namespace ts { failedLookupLocations, packageJsonInfoCache: cache, features, - conditions: features & NodeResolutionFeatures.EsmMode ? ["node", "import", "types"] : ["node", "require", "types"] + conditions, }; const result = forEach(extensions, ext => tryResolve(ext)); @@ -1533,20 +1551,22 @@ namespace ts { default: return tryExtension(Extension.Dts); } case Extensions.TypeScript: + case Extensions.TsOnly: + const useDts = extensions === Extensions.TypeScript; switch (originalExtension) { case Extension.Mjs: case Extension.Mts: case Extension.Dmts: - return tryExtension(Extension.Mts) || tryExtension(Extension.Dmts); + return tryExtension(Extension.Mts) || (useDts ? tryExtension(Extension.Dmts) : undefined); case Extension.Cjs: case Extension.Cts: case Extension.Dcts: - return tryExtension(Extension.Cts) || tryExtension(Extension.Dcts); + return tryExtension(Extension.Cts) || (useDts ? tryExtension(Extension.Dcts) : undefined); case Extension.Json: candidate += Extension.Json; - return tryExtension(Extension.Dts); + return useDts ? tryExtension(Extension.Dts) : undefined; default: - return tryExtension(Extension.Ts) || tryExtension(Extension.Tsx) || tryExtension(Extension.Dts); + return tryExtension(Extension.Ts) || tryExtension(Extension.Tsx) || (useDts ? tryExtension(Extension.Dts) : undefined); } case Extensions.JavaScript: switch (originalExtension) { @@ -1813,6 +1833,7 @@ namespace ts { switch (extensions) { case Extensions.JavaScript: case Extensions.Json: + case Extensions.TsOnly: packageFile = readPackageJsonMainField(jsonContent, candidate, state); break; case Extensions.TypeScript: @@ -1893,14 +1914,16 @@ namespace ts { function extensionIsOk(extensions: Extensions, extension: Extension): boolean { switch (extensions) { case Extensions.JavaScript: - return extension === Extension.Js || extension === Extension.Jsx; + return extension === Extension.Js || extension === Extension.Jsx || extension === Extension.Mjs || extension === Extension.Cjs; case Extensions.TSConfig: case Extensions.Json: return extension === Extension.Json; case Extensions.TypeScript: - return extension === Extension.Ts || extension === Extension.Tsx || extension === Extension.Dts; + return extension === Extension.Ts || extension === Extension.Tsx || extension === Extension.Mts || extension === Extension.Cts || extension === Extension.Dts || extension === Extension.Dmts || extension === Extension.Dcts; + case Extensions.TsOnly: + return extension === Extension.Ts || extension === Extension.Tsx || extension === Extension.Mts || extension === Extension.Cts; case Extensions.DtsOnly: - return extension === Extension.Dts; + return extension === Extension.Dts || extension === Extension.Dmts || extension === Extension.Dcts; } } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 0831da97a629c..3a88c1b5c3774 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -4668,8 +4668,10 @@ namespace ts { export type AnyImportOrRequire = AnyImportSyntax | VariableDeclarationInitializedTo; /* @internal */ - export type AnyImportOrRequireStatement = AnyImportSyntax | RequireVariableStatement; + export type AnyImportOrBareOrAccessedRequire = AnyImportSyntax | VariableDeclarationInitializedTo; + /* @internal */ + export type AnyImportOrRequireStatement = AnyImportSyntax | RequireVariableStatement; /* @internal */ export type AnyImportOrReExport = AnyImportSyntax | ExportDeclaration; @@ -6178,6 +6180,8 @@ namespace ts { assumeChangesOnlyAffectDirectDependencies?: boolean; noLib?: boolean; noResolve?: boolean; + /*@internal*/ + noDtsResolution?: boolean; noUncheckedIndexedAccess?: boolean; out?: string; outDir?: string; diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 6dbd1da2e70f4..d44df87c458a6 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -933,6 +933,10 @@ namespace ts { } } + export function isAnyImportOrBareOrAccessedRequire(node: Node): node is AnyImportOrBareOrAccessedRequire { + return isAnyImportSyntax(node) || isVariableDeclarationInitializedToBareOrAccessedRequire(node); + } + export function isLateVisibilityPaintedStatement(node: Node): node is LateVisibilityPaintedStatement { switch (node.kind) { case SyntaxKind.ImportDeclaration: @@ -2558,14 +2562,14 @@ namespace ts { return decl.kind === SyntaxKind.FunctionDeclaration || isVariableDeclaration(decl) && decl.initializer && isFunctionLike(decl.initializer); } - export function tryGetModuleSpecifierFromDeclaration(node: AnyImportOrRequire): string | undefined { + export function tryGetModuleSpecifierFromDeclaration(node: AnyImportOrBareOrAccessedRequire): StringLiteralLike | undefined { switch (node.kind) { case SyntaxKind.VariableDeclaration: - return node.initializer.arguments[0].text; + return findAncestor(node.initializer, (node): node is RequireOrImportCall => isRequireCall(node, /*requireStringLiteralLikeArgument*/ true))?.arguments[0]; case SyntaxKind.ImportDeclaration: - return tryCast(node.moduleSpecifier, isStringLiteralLike)?.text; + return tryCast(node.moduleSpecifier, isStringLiteralLike); case SyntaxKind.ImportEqualsDeclaration: - return tryCast(tryCast(node.moduleReference, isExternalModuleReference)?.expression, isStringLiteralLike)?.text; + return tryCast(tryCast(node.moduleReference, isExternalModuleReference)?.expression, isStringLiteralLike); default: Debug.assertNever(node); } @@ -3131,21 +3135,31 @@ namespace ts { // export = // export default // module.exports = - // {} - // {name: } + // module.exports.x = + // const x = require("...") + // const { x } = require("...") + // const x = require("...").y + // const { x } = require("...").y export function isAliasSymbolDeclaration(node: Node): boolean { - return node.kind === SyntaxKind.ImportEqualsDeclaration || + if (node.kind === SyntaxKind.ImportEqualsDeclaration || node.kind === SyntaxKind.NamespaceExportDeclaration || node.kind === SyntaxKind.ImportClause && !!(node as ImportClause).name || node.kind === SyntaxKind.NamespaceImport || node.kind === SyntaxKind.NamespaceExport || node.kind === SyntaxKind.ImportSpecifier || node.kind === SyntaxKind.ExportSpecifier || - node.kind === SyntaxKind.ExportAssignment && exportAssignmentIsAlias(node as ExportAssignment) || + node.kind === SyntaxKind.ExportAssignment && exportAssignmentIsAlias(node as ExportAssignment) + ) { + return true; + } + + return isInJSFile(node) && ( isBinaryExpression(node) && getAssignmentDeclarationKind(node) === AssignmentDeclarationKind.ModuleExports && exportAssignmentIsAlias(node) || - isPropertyAccessExpression(node) && isBinaryExpression(node.parent) && node.parent.left === node && node.parent.operatorToken.kind === SyntaxKind.EqualsToken && isAliasableExpression(node.parent.right) || - node.kind === SyntaxKind.ShorthandPropertyAssignment || - node.kind === SyntaxKind.PropertyAssignment && isAliasableExpression((node as PropertyAssignment).initializer); + isPropertyAccessExpression(node) + && isBinaryExpression(node.parent) + && node.parent.left === node + && node.parent.operatorToken.kind === SyntaxKind.EqualsToken + && isAliasableExpression(node.parent.right)); } export function getAliasDeclarationFromName(node: EntityName): Declaration | undefined { @@ -3156,6 +3170,7 @@ namespace ts { case SyntaxKind.ExportSpecifier: case SyntaxKind.ExportAssignment: case SyntaxKind.ImportEqualsDeclaration: + case SyntaxKind.NamespaceExport: return node.parent as Declaration; case SyntaxKind.QualifiedName: do { @@ -5142,6 +5157,11 @@ namespace ts { (node.parent.kind === SyntaxKind.PropertyAccessExpression && (node.parent as PropertyAccessExpression).name === node); } + export function isRightSideOfAccessExpression(node: Node) { + return isPropertyAccessExpression(node.parent) && node.parent.name === node + || isElementAccessExpression(node.parent) && node.parent.argumentExpression === node; + } + export function isRightSideOfQualifiedNameOrPropertyAccessOrJSDocMemberName(node: Node) { return isQualifiedName(node.parent) && node.parent.right === node || isPropertyAccessExpression(node.parent) && node.parent.name === node @@ -5829,6 +5849,45 @@ namespace ts { return expr; } + export function forEachNameInAccessChainWalkingLeft(name: MemberName | StringLiteralLike, action: (name: MemberName | StringLiteralLike) => T | undefined): T | undefined { + if (isAccessExpression(name.parent) && isRightSideOfAccessExpression(name)) { + return walkAccessExpression(name.parent); + } + + function walkAccessExpression(access: AccessExpression): T | undefined { + if (access.kind === SyntaxKind.PropertyAccessExpression) { + const res = action(access.name); + if (res !== undefined) { + return res; + } + } + else if (access.kind === SyntaxKind.ElementAccessExpression) { + if (isIdentifier(access.argumentExpression) || isStringLiteralLike(access.argumentExpression)) { + const res = action(access.argumentExpression); + if (res !== undefined) { + return res; + } + } + else { + // Chain interrupted by non-static-name access 'x[expr()].y.z' + return undefined; + } + } + + if (isAccessExpression(access.expression)) { + return walkAccessExpression(access.expression); + } + if (isIdentifier(access.expression)) { + // End of chain at Identifier 'x.y.z' + return action(access.expression); + } + // End of chain at non-Identifier 'x().y.z' + return undefined; + } + } + + + export function getLeftmostExpression(node: Expression, stopAtCallExpressions: boolean) { while (true) { switch (node.kind) { diff --git a/src/harness/client.ts b/src/harness/client.ts index 296832388baf6..388f6473fa8da 100644 --- a/src/harness/client.ts +++ b/src/harness/client.ts @@ -298,7 +298,7 @@ namespace ts.server { getDefinitionAndBoundSpan(fileName: string, position: number): DefinitionInfoAndBoundSpan { const args: protocol.FileLocationRequestArgs = this.createFileLocationRequestArgs(fileName, position); - const request = this.processRequest(CommandNames.DefinitionAndBoundSpan, args); + const request = this.processRequest(CommandNames.DefinitionAndBoundSpan, args); const response = this.processResponse(request); const body = Debug.checkDefined(response.body); // TODO: GH#18217 @@ -332,6 +332,23 @@ namespace ts.server { })); } + getSourceDefinitionAndBoundSpan(fileName: string, position: number): DefinitionInfo[] { + const args: protocol.FileLocationRequestArgs = this.createFileLocationRequestArgs(fileName, position); + const request = this.processRequest(CommandNames.FindSourceDefinition, args); + const response = this.processResponse(request); + const body = Debug.checkDefined(response.body); // TODO: GH#18217 + + return body.map(entry => ({ + containerKind: ScriptElementKind.unknown, + containerName: "", + fileName: entry.file, + textSpan: this.decodeSpan(entry), + kind: ScriptElementKind.unknown, + name: "", + unverified: entry.unverified, + })); + } + getImplementationAtPosition(fileName: string, position: number): ImplementationLocation[] { const args = this.createFileLocationRequestArgs(fileName, position); diff --git a/src/harness/fourslashImpl.ts b/src/harness/fourslashImpl.ts index f4b2832c00b6e..d65aa86fb6b02 100644 --- a/src/harness/fourslashImpl.ts +++ b/src/harness/fourslashImpl.ts @@ -697,6 +697,13 @@ namespace FourSlash { this.verifyGoToX(arg0, endMarkerNames, () => this.getGoToDefinitionAndBoundSpan()); } + public verifyGoToSourceDefinition(startMarkerNames: ArrayOrSingle, end?: ArrayOrSingle | { file: string, unverified?: boolean }) { + if (this.testType !== FourSlashTestType.Server) { + this.raiseError("goToSourceDefinition may only be used in fourslash/server tests."); + } + this.verifyGoToX(startMarkerNames, end, () => (this.languageService as ts.server.SessionClient).getSourceDefinitionAndBoundSpan(this.activeFile.fileName, this.currentCaretPosition)!); + } + private getGoToDefinition(): readonly ts.DefinitionInfo[] { return this.languageService.getDefinitionAtPosition(this.activeFile.fileName, this.currentCaretPosition)!; } @@ -764,7 +771,9 @@ namespace FourSlash { } if (endMarkers.length !== definitions.length) { - this.raiseError(`${testName} failed - expected to find ${endMarkers.length} definitions but got ${definitions.length}`); + const markers = definitions.map(d => ({ text: "HERE", fileName: d.fileName, position: d.textSpan.start })); + const actual = this.renderMarkers(markers); + this.raiseError(`${testName} failed - expected to find ${endMarkers.length} definitions but got ${definitions.length}\n\n${actual}`); } ts.zipWith(endMarkers, definitions, (endMarkerOrFileResult, definition, i) => { @@ -774,17 +783,8 @@ namespace FourSlash { ts.Debug.assert(typeof expectedFileName === "string"); const expectedPosition = marker?.position || 0; if (ts.comparePaths(expectedFileName, definition.fileName, /*ignoreCase*/ true) !== ts.Comparison.EqualTo || expectedPosition !== definition.textSpan.start) { - const filesToDisplay = ts.deduplicate([expectedFileName, definition.fileName], ts.equateValues); const markers = [{ text: "EXPECTED", fileName: expectedFileName, position: expectedPosition }, { text: "ACTUAL", fileName: definition.fileName, position: definition.textSpan.start }]; - const text = filesToDisplay.map(fileName => { - const markersToRender = markers.filter(m => m.fileName === fileName).sort((a, b) => b.position - a.position); - let fileContent = this.tryGetFileContent(fileName) || ""; - for (const marker of markersToRender) { - fileContent = fileContent.slice(0, marker.position) + `\x1b[1;4m/*${marker.text}*/\x1b[0;31m` + fileContent.slice(marker.position); - } - return `// @Filename: ${fileName}\n${fileContent}`; - }).join("\n\n"); - + const text = this.renderMarkers(markers); this.raiseError(`${testName} failed for definition ${markerName || expectedFileName} (${i}): expected ${expectedFileName} at ${expectedPosition}, got ${definition.fileName} at ${definition.textSpan.start}\n\n${text}\n`); } if (definition.unverified && (typeof endMarkerOrFileResult === "string" || !endMarkerOrFileResult.unverified)) { @@ -798,6 +798,18 @@ namespace FourSlash { }); } + private renderMarkers(markers: { text: string, fileName: string, position: number }[]) { + const filesToDisplay = ts.deduplicate(markers.map(m => m.fileName), ts.equateValues); + return filesToDisplay.map(fileName => { + const markersToRender = markers.filter(m => m.fileName === fileName).sort((a, b) => b.position - a.position); + let fileContent = this.tryGetFileContent(fileName) || ""; + for (const marker of markersToRender) { + fileContent = fileContent.slice(0, marker.position) + `\x1b[1;4m/*${marker.text}*/\x1b[0;31m` + fileContent.slice(marker.position); + } + return `// @Filename: ${fileName}\n${fileContent}`; + }).join("\n\n"); + } + private verifyDefinitionTextSpan(defs: ts.DefinitionInfoAndBoundSpan, startMarkerName: string) { const range = this.testData.ranges.find(range => this.markerName(range.marker!) === startMarkerName); diff --git a/src/harness/fourslashInterfaceImpl.ts b/src/harness/fourslashInterfaceImpl.ts index ea07f4f386fc4..6eb4414df25ac 100644 --- a/src/harness/fourslashInterfaceImpl.ts +++ b/src/harness/fourslashInterfaceImpl.ts @@ -324,6 +324,10 @@ namespace FourSlashInterface { this.state.verifyGoToType(arg0, endMarkerName); } + public goToSourceDefinition(startMarkerNames: ArrayOrSingle, end: { file: string } | ArrayOrSingle) { + this.state.verifyGoToSourceDefinition(startMarkerNames, end); + } + public goToDefinitionForMarkers(...markerNames: string[]) { this.state.verifyGoToDefinitionForMarkers(markerNames); } diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 5a81aff9cc461..d40294989ee68 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -715,6 +715,8 @@ namespace ts.server { readonly newInferredProjectName = createProjectNameFactoryWithCounter(makeInferredProjectName); /*@internal*/ readonly newAutoImportProviderProjectName = createProjectNameFactoryWithCounter(makeAutoImportProviderProjectName); + /*@internal*/ + readonly newAuxiliaryProjectName = createProjectNameFactoryWithCounter(makeAuxiliaryProjectName); /** * Open files: with value being project root path, and key being Path of the file that is open */ @@ -989,13 +991,15 @@ namespace ts.server { private delayUpdateProjectGraph(project: Project) { project.markAsDirty(); - const projectName = project.getProjectName(); - this.pendingProjectUpdates.set(projectName, project); - this.throttledOperations.schedule(projectName, /*delay*/ 250, () => { - if (this.pendingProjectUpdates.delete(projectName)) { - updateProjectIfDirty(project); - } - }); + if (project.projectKind !== ProjectKind.AutoImportProvider && project.projectKind !== ProjectKind.Auxiliary) { + const projectName = project.getProjectName(); + this.pendingProjectUpdates.set(projectName, project); + this.throttledOperations.schedule(projectName, /*delay*/ 250, () => { + if (this.pendingProjectUpdates.delete(projectName)) { + updateProjectIfDirty(project); + } + }); + } } /*@internal*/ @@ -2297,7 +2301,7 @@ namespace ts.server { configFileExistenceInfo.config.watchedDirectoriesStale = undefined; } - private updateNonInferredProjectFiles(project: ExternalProject | ConfiguredProject | AutoImportProviderProject, files: T[], propertyReader: FilePropertyReader) { + private updateNonInferredProjectFiles(project: Project, files: T[], propertyReader: FilePropertyReader) { const projectRootFilesMap = project.getRootFilesMap(); const newRootScriptInfoMap = new Map(); @@ -3589,7 +3593,7 @@ namespace ts.server { const toRemoveScriptInfos = new Map(this.filenameToScriptInfo); this.filenameToScriptInfo.forEach(info => { // If script info is open or orphan, retain it and its dependencies - if (!info.isScriptOpen() && info.isOrphan() && !info.isContainedByAutoImportProvider()) { + if (!info.isScriptOpen() && info.isOrphan() && !info.isContainedByBackgroundProject()) { // Otherwise if there is any source info that is alive, this alive too if (!info.sourceMapFilePath) return; let sourceInfos: Set | undefined; diff --git a/src/server/project.ts b/src/server/project.ts index 715a659281788..f4a4f51b46192 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -5,6 +5,7 @@ namespace ts.server { Configured, External, AutoImportProvider, + Auxiliary, } /* @internal */ @@ -209,6 +210,9 @@ namespace ts.server { /*@internal*/ private packageJsonsForAutoImport: Set | undefined; + /*@internal*/ + private noDtsResolutionProject?: AuxiliaryProject | undefined; + /*@internal*/ getResolvedProjectReferenceToRedirect(_fileName: string): ResolvedProjectReference | undefined { return undefined; @@ -811,6 +815,10 @@ namespace ts.server { this.autoImportProviderHost.close(); } this.autoImportProviderHost = undefined; + if (this.noDtsResolutionProject) { + this.noDtsResolutionProject.close(); + } + this.noDtsResolutionProject = undefined; // signal language service to release source files acquired from document registry this.languageService.dispose(); @@ -1406,6 +1414,7 @@ namespace ts.server { const oldOptions = this.compilerOptions; this.compilerOptions = compilerOptions; this.setInternalCompilerOptionsForEmittingJsFiles(); + this.noDtsResolutionProject?.setCompilerOptions(this.getCompilerOptionsForNoDtsResolutionProject()); if (changesAffectModuleResolution(oldOptions, compilerOptions)) { // reset cached unresolved imports if changes in compiler options affected module resolution this.cachedUnresolvedImportsPerFile.clear(); @@ -1780,6 +1789,54 @@ namespace ts.server { getIncompleteCompletionsCache() { return this.projectService.getIncompleteCompletionsCache(); } + + /*@internal*/ + getNoDtsResolutionProject(rootFileNames: readonly string[]): Project { + Debug.assert(this.projectService.serverMode === LanguageServiceMode.Semantic); + if (!this.noDtsResolutionProject) { + this.noDtsResolutionProject = new AuxiliaryProject(this.projectService, this.documentRegistry, this.getCompilerOptionsForNoDtsResolutionProject()); + } + + enumerateInsertsAndDeletes( + rootFileNames.map(toNormalizedPath), + this.noDtsResolutionProject.getRootFiles(), + getStringComparer(!this.useCaseSensitiveFileNames()), + pathToAdd => { + const info = this.projectService.getOrCreateScriptInfoNotOpenedByClient( + pathToAdd, + this.currentDirectory, + this.noDtsResolutionProject!.directoryStructureHost); + if (info) { + this.noDtsResolutionProject!.addRoot(info, pathToAdd); + } + }, + pathToRemove => { + // It may be preferable to remove roots only once project grows to a certain size? + const info = this.noDtsResolutionProject!.getScriptInfo(pathToRemove); + if (info) { + this.noDtsResolutionProject!.removeRoot(info); + } + }, + ); + + return this.noDtsResolutionProject; + } + + /*@internal*/ + private getCompilerOptionsForNoDtsResolutionProject() { + return { + ...this.getCompilerOptions(), + noDtsResolution: true, + allowJs: true, + maxNodeModuleJsDepth: 3, + diagnostics: false, + skipLibCheck: true, + sourceMap: false, + types: ts.emptyArray, + lib: ts.emptyArray, + noLib: true, + }; + } } function getUnresolvedImports(program: Program, cachedUnresolvedImportsPerFile: ESMap): SortedReadonlyArray { @@ -1920,6 +1977,32 @@ namespace ts.server { } } + class AuxiliaryProject extends Project { + constructor(projectService: ProjectService, documentRegistry: DocumentRegistry, compilerOptions: CompilerOptions) { + super(projectService.newAuxiliaryProjectName(), + ProjectKind.Auxiliary, + projectService, + documentRegistry, + /*hasExplicitListOfFiles*/ false, + /*lastFileExceededProgramSize*/ undefined, + compilerOptions, + /*compileOnSaveEnabled*/ false, + /*watchOptions*/ undefined, + projectService.host, + /*currentDirectory*/ undefined); + } + + isOrphan(): boolean { + return true; + } + + /*@internal*/ + scheduleInvalidateResolutionsOfFailedLookupLocations(): void { + // Invalidation will happen on-demand as part of updateGraph + return; + } + } + export class AutoImportProviderProject extends Project { /*@internal*/ private static readonly maxDependencies = 10; @@ -2129,6 +2212,12 @@ namespace ts.server { return hasSameSetOfFiles; } + /*@internal*/ + scheduleInvalidateResolutionsOfFailedLookupLocations(): void { + // Invalidation will happen on-demand as part of updateGraph + return; + } + hasRoots() { return !!this.rootFileNames?.length; } diff --git a/src/server/protocol.ts b/src/server/protocol.ts index df61ead170eb4..fc53ad151ed67 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -83,6 +83,7 @@ namespace ts.server.protocol { SignatureHelp = "signatureHelp", /* @internal */ SignatureHelpFull = "signatureHelp-full", + FindSourceDefinition = "findSourceDefinition", Status = "status", TypeDefinition = "typeDefinition", ProjectInfo = "projectInfo", @@ -904,6 +905,10 @@ namespace ts.server.protocol { readonly command: CommandTypes.DefinitionAndBoundSpan; } + export interface FindSourceDefinitionRequest extends FileLocationRequest { + readonly command: CommandTypes.FindSourceDefinition; + } + export interface DefinitionAndBoundSpanResponse extends Response { readonly body: DefinitionInfoAndBoundSpan; } diff --git a/src/server/scriptInfo.ts b/src/server/scriptInfo.ts index a26fcc6748932..dedd30fe19b93 100644 --- a/src/server/scriptInfo.ts +++ b/src/server/scriptInfo.ts @@ -501,7 +501,7 @@ namespace ts.server { case 0: return Errors.ThrowNoProject(); case 1: - return ensureNotAutoImportProvider(this.containingProjects[0]); + return ensurePrimaryProjectKind(this.containingProjects[0]); default: // If this file belongs to multiple projects, below is the order in which default project is used // - for open script info, its default configured project during opening is default if info is part of it @@ -536,7 +536,7 @@ namespace ts.server { firstInferredProject = project; } } - return ensureNotAutoImportProvider(defaultConfiguredProject || + return ensurePrimaryProjectKind(defaultConfiguredProject || firstNonSourceOfProjectReferenceRedirect || firstConfiguredProject || firstExternalProject || @@ -622,8 +622,10 @@ namespace ts.server { } /*@internal*/ - isContainedByAutoImportProvider() { - return some(this.containingProjects, p => p.projectKind === ProjectKind.AutoImportProvider); + isContainedByBackgroundProject() { + return some( + this.containingProjects, + p => p.projectKind === ProjectKind.AutoImportProvider || p.projectKind === ProjectKind.Auxiliary); } /** @@ -669,8 +671,13 @@ namespace ts.server { } } - function ensureNotAutoImportProvider(project: Project | undefined) { - if (!project || project.projectKind === ProjectKind.AutoImportProvider) { + /** + * Throws an error if `project` is an AutoImportProvider or AuxiliaryProject, + * which are used in the background by other Projects and should never be + * reported as the default project for a ScriptInfo. + */ + function ensurePrimaryProjectKind(project: Project | undefined) { + if (!project || project.projectKind === ProjectKind.AutoImportProvider || project.projectKind === ProjectKind.Auxiliary) { return Errors.ThrowNoProject(); } return project; diff --git a/src/server/session.ts b/src/server/session.ts index 16b04c6979220..3345f362eda77 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1193,6 +1193,7 @@ namespace ts.server { containerName: info.containerName, kind: info.kind, name: info.name, + failedAliasResolution: info.failedAliasResolution, ...info.unverified && { unverified: info.unverified }, }; }); @@ -1228,6 +1229,159 @@ namespace ts.server { }; } + private findSourceDefinition(args: protocol.FileLocationRequestArgs): readonly protocol.DefinitionInfo[] { + const { file, project } = this.getFileAndProject(args); + const position = this.getPositionInFile(args, file); + const unmappedDefinitions = project.getLanguageService().getDefinitionAtPosition(file, position); + let definitions: readonly DefinitionInfo[] = this.mapDefinitionInfoLocations(unmappedDefinitions || emptyArray, project).slice(); + const needsJsResolution = this.projectService.serverMode === LanguageServiceMode.Semantic && ( + !some(definitions, d => toNormalizedPath(d.fileName) !== file && !d.isAmbient) || + some(definitions, d => !!d.failedAliasResolution)); + + if (needsJsResolution) { + const definitionSet = createSet(d => d.textSpan.start, documentSpansEqual); + definitions?.forEach(d => definitionSet.add(d)); + const noDtsProject = project.getNoDtsResolutionProject([file]); + const ls = noDtsProject.getLanguageService(); + const jsDefinitions = ls.getDefinitionAtPosition(file, position, /*searchOtherFilesOnly*/ true) + ?.filter(d => toNormalizedPath(d.fileName) !== file); + if (some(jsDefinitions)) { + for (const jsDefinition of jsDefinitions) { + if (jsDefinition.unverified) { + const refined = tryRefineDefinition(jsDefinition, project.getLanguageService().getProgram()!, ls.getProgram()!); + if (some(refined)) { + for (const def of refined) { + definitionSet.add(def); + } + continue; + } + } + definitionSet.add(jsDefinition); + } + } + else { + const ambientCandidates = definitions.filter(d => toNormalizedPath(d.fileName) !== file && d.isAmbient); + for (const candidate of some(ambientCandidates) ? ambientCandidates : getAmbientCandidatesByClimbingAccessChain()) { + const fileNameToSearch = findImplementationFileFromDtsFileName(candidate.fileName, file, noDtsProject); + if (!fileNameToSearch || !ensureRoot(noDtsProject, fileNameToSearch)) { + continue; + } + const noDtsProgram = ls.getProgram()!; + const fileToSearch = Debug.checkDefined(noDtsProgram.getSourceFile(fileNameToSearch)); + for (const match of searchForDeclaration(candidate.name, fileToSearch, noDtsProgram)) { + definitionSet.add(match); + } + } + } + definitions = arrayFrom(definitionSet.values()); + } + + definitions = definitions.filter(d => !d.isAmbient && !d.failedAliasResolution); + return this.mapDefinitionInfo(definitions, project); + + function findImplementationFileFromDtsFileName(fileName: string, resolveFromFile: string, auxiliaryProject: Project) { + const nodeModulesPathParts = getNodeModulePathParts(fileName); + if (nodeModulesPathParts && fileName.lastIndexOf(nodeModulesPathPart) === nodeModulesPathParts.topLevelNodeModulesIndex) { + // Second check ensures the fileName only contains one `/node_modules/`. If there's more than one I give up. + const packageDirectory = fileName.substring(0, nodeModulesPathParts.packageRootIndex); + const packageJsonCache = project.getModuleResolutionCache()?.getPackageJsonInfoCache(); + const compilerOptions = project.getCompilationSettings(); + const packageJson = getPackageScopeForPath(project.toPath(packageDirectory + "/package.json"), packageJsonCache, project, compilerOptions); + if (!packageJson) return undefined; + // Use fake options instead of actual compiler options to avoid following export map if the project uses node12 or nodenext - + // Mapping from an export map entry across packages is out of scope for now. Returned entrypoints will only be what can be + // resolved from the package root under --moduleResolution node + const entrypoints = getEntrypointsFromPackageJsonInfo( + packageJson, + { moduleResolution: ModuleResolutionKind.NodeJs }, + project, + project.getModuleResolutionCache()); + // This substring is correct only because we checked for a single `/node_modules/` at the top. + const packageNamePathPart = fileName.substring( + nodeModulesPathParts.topLevelPackageNameIndex + 1, + nodeModulesPathParts.packageRootIndex); + const packageName = getPackageNameFromTypesPackageName(unmangleScopedPackageName(packageNamePathPart)); + const path = project.toPath(fileName); + if (entrypoints && some(entrypoints, e => project.toPath(e) === path)) { + // This file was the main entrypoint of a package. Try to resolve that same package name with + // the auxiliary project that only resolves to implementation files. + const [implementationResolution] = auxiliaryProject.resolveModuleNames([packageName], resolveFromFile); + return implementationResolution?.resolvedFileName; + } + else { + // It wasn't the main entrypoint but we are in node_modules. Try a subpath into the package. + const pathToFileInPackage = fileName.substring(nodeModulesPathParts.packageRootIndex + 1); + const specifier = `${packageName}/${removeFileExtension(pathToFileInPackage)}`; + const [implementationResolution] = auxiliaryProject.resolveModuleNames([specifier], resolveFromFile); + return implementationResolution?.resolvedFileName; + } + } + // We're not in node_modules, and we only get to this function if non-dts module resolution failed. + // I'm not sure what else I can do here that isn't already covered by that module resolution. + return undefined; + } + + // In 'foo.bar./**/baz', if we got not results on 'baz', see if we can get an ambient definition + // for 'bar' or 'foo' (in that order) so we can search for declarations of 'baz' later. + function getAmbientCandidatesByClimbingAccessChain(): readonly { name: string, fileName: string }[] { + const ls = project.getLanguageService(); + const program = ls.getProgram()!; + const initialNode = getTouchingPropertyName(program.getSourceFile(file)!, position); + if ((isStringLiteralLike(initialNode) || isIdentifier(initialNode)) && isAccessExpression(initialNode.parent)) { + return forEachNameInAccessChainWalkingLeft(initialNode, nameInChain => { + if (nameInChain === initialNode) return undefined; + const candidates = ls.getDefinitionAtPosition(file, nameInChain.getStart(), /*searchOtherFilesOnly*/ true) + ?.filter(d => toNormalizedPath(d.fileName) !== file && d.isAmbient) + .map(d => ({ + fileName: d.fileName, + name: getTextOfIdentifierOrLiteral(initialNode) + })); + if (some(candidates)) { + return candidates; + } + }) || emptyArray; + } + return emptyArray; + } + + function tryRefineDefinition(definition: DefinitionInfo, program: Program, noDtsProgram: Program) { + const fileToSearch = noDtsProgram.getSourceFile(definition.fileName); + if (!fileToSearch) { + return undefined; + } + const initialNode = getTouchingPropertyName(program.getSourceFile(file)!, position); + const symbol = program.getTypeChecker().getSymbolAtLocation(initialNode); + const importSpecifier = symbol && getDeclarationOfKind(symbol, SyntaxKind.ImportSpecifier); + if (!importSpecifier) return undefined; + + const nameToSearch = importSpecifier.propertyName?.text || importSpecifier.name.text; + return searchForDeclaration(nameToSearch, fileToSearch, noDtsProgram); + } + + function searchForDeclaration(declarationName: string, fileToSearch: SourceFile, noDtsProgram: Program) { + const matches = FindAllReferences.Core.getTopMostDeclarationNamesInFile(declarationName, fileToSearch); + return mapDefined(matches, match => { + const symbol = noDtsProgram.getTypeChecker().getSymbolAtLocation(match); + const decl = getDeclarationFromName(match); + if (symbol && decl) { + // I think the last argument to this is supposed to be the start node, but it doesn't seem important. + // Callers internal to GoToDefinition already get confused about this. + return GoToDefinition.createDefinitionInfo(decl, noDtsProgram.getTypeChecker(), symbol, decl, /*unverified*/ true); + } + }); + } + + function ensureRoot(project: Project, fileName: string) { + const info = project.getScriptInfo(fileName); + if (!info) return false; + if (!project.containsScriptInfo(info)) { + project.addRoot(info); + project.updateGraph(); + } + return true; + } + } + private getEmitOutput(args: protocol.EmitOutputRequestArgs): EmitOutput | protocol.EmitOutput { const { file, project } = this.getFileAndProject(args); if (!project.shouldEmitFile(project.getScriptInfo(file))) { @@ -2751,12 +2905,15 @@ namespace ts.server { [CommandNames.DefinitionFull]: (request: protocol.DefinitionRequest) => { return this.requiredResponse(this.getDefinition(request.arguments, /*simplifiedResult*/ false)); }, - [CommandNames.DefinitionAndBoundSpan]: (request: protocol.DefinitionRequest) => { + [CommandNames.DefinitionAndBoundSpan]: (request: protocol.DefinitionAndBoundSpanRequest) => { return this.requiredResponse(this.getDefinitionAndBoundSpan(request.arguments, /*simplifiedResult*/ true)); }, - [CommandNames.DefinitionAndBoundSpanFull]: (request: protocol.DefinitionRequest) => { + [CommandNames.DefinitionAndBoundSpanFull]: (request: protocol.DefinitionAndBoundSpanRequest) => { return this.requiredResponse(this.getDefinitionAndBoundSpan(request.arguments, /*simplifiedResult*/ false)); }, + [CommandNames.FindSourceDefinition]: (request: protocol.FindSourceDefinitionRequest) => { + return this.requiredResponse(this.findSourceDefinition(request.arguments)); + }, [CommandNames.EmitOutput]: (request: protocol.EmitOutputRequest) => { return this.requiredResponse(this.getEmitOutput(request.arguments)); }, diff --git a/src/server/utilitiesPublic.ts b/src/server/utilitiesPublic.ts index 026162133bcbb..7b28a9b17d367 100644 --- a/src/server/utilitiesPublic.ts +++ b/src/server/utilitiesPublic.ts @@ -122,6 +122,11 @@ namespace ts.server { return `/dev/null/autoImportProviderProject${counter}*`; } + /*@internal*/ + export function makeAuxiliaryProjectName(counter: number): string { + return `/dev/null/auxiliaryProject${counter}*`; + } + export function createSortedArray(): SortedArray { return [] as any as SortedArray; // TODO: GH#19873 } diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index 485fc02727990..f4e8b416f0590 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -453,7 +453,7 @@ namespace ts.codefix { // and it is up to the user to decide which one fits best. return firstDefined(existingImports, ({ declaration }): FixUseNamespaceImport | undefined => { const namespacePrefix = getNamespaceLikeImportText(declaration); - const moduleSpecifier = tryGetModuleSpecifierFromDeclaration(declaration); + const moduleSpecifier = tryGetModuleSpecifierFromDeclaration(declaration)?.text; if (namespacePrefix && moduleSpecifier) { const moduleSymbol = getTargetModuleFromNamespaceLikeImport(declaration, checker); if (moduleSymbol && moduleSymbol.exports!.has(escapeLeadingUnderscores(symbolName))) { @@ -670,7 +670,7 @@ namespace ts.codefix { checker: TypeChecker, compilerOptions: CompilerOptions ): FixAddNewImport | undefined { - const moduleSpecifier = tryGetModuleSpecifierFromDeclaration(declaration); + const moduleSpecifier = tryGetModuleSpecifierFromDeclaration(declaration)?.text; if (moduleSpecifier) { const addAsTypeOnly = useRequire ? AddAsTypeOnly.NotAllowed diff --git a/src/services/findAllReferences.ts b/src/services/findAllReferences.ts index aeac984d5dbb7..ba46b5d311f46 100644 --- a/src/services/findAllReferences.ts +++ b/src/services/findAllReferences.ts @@ -1365,6 +1365,31 @@ namespace ts.FindAllReferences { } } + export function getTopMostDeclarationNamesInFile(declarationName: string, sourceFile: SourceFile): readonly Node[] { + const candidates = filter(getPossibleSymbolReferenceNodes(sourceFile, declarationName), name => !!getDeclarationFromName(name)); + return candidates.reduce((topMost, decl) => { + const depth = getDepth(decl); + if (!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 Node[] }).declarationNames; + + function getDepth(declaration: Node | undefined) { + let depth = 0; + while (declaration) { + declaration = getContainerNode(declaration); + depth++; + } + return depth; + } + } + export function someSignatureUsage( signature: SignatureDeclaration, sourceFiles: readonly SourceFile[], diff --git a/src/services/goToDefinition.ts b/src/services/goToDefinition.ts index 3b49ec4b64610..b1c8ec28c1b8d 100644 --- a/src/services/goToDefinition.ts +++ b/src/services/goToDefinition.ts @@ -1,6 +1,6 @@ /* @internal */ namespace ts.GoToDefinition { - export function getDefinitionAtPosition(program: Program, sourceFile: SourceFile, position: number): readonly DefinitionInfo[] | undefined { + export function getDefinitionAtPosition(program: Program, sourceFile: SourceFile, position: number, searchOtherFilesOnly?: boolean): readonly DefinitionInfo[] | undefined { const resolvedRef = getReferenceAtPosition(sourceFile, position, program); const fileReferenceDefinition = resolvedRef && [getDefinitionInfoForFileReference(resolvedRef.reference.fileName, resolvedRef.fileName, resolvedRef.unverified)] || emptyArray; if (resolvedRef?.file) { @@ -28,18 +28,49 @@ namespace ts.GoToDefinition { if (isStaticModifier(node) && isClassStaticBlockDeclaration(node.parent)) { const classDecl = node.parent.parent; - const symbol = getSymbol(classDecl, typeChecker); + const { symbol, failedAliasResolution } = getSymbol(classDecl, typeChecker); + const staticBlocks = filter(classDecl.members, isClassStaticBlockDeclaration); const containerName = symbol ? typeChecker.symbolToString(symbol, classDecl) : ""; const sourceFile = node.getSourceFile(); return map(staticBlocks, staticBlock => { let { pos } = moveRangePastModifiers(staticBlock); pos = skipTrivia(sourceFile.text, pos); - return createDefinitionInfoFromName(typeChecker, staticBlock, ScriptElementKind.constructorImplementationElement, "static {}", containerName, { start: pos, length: "static".length }); + return createDefinitionInfoFromName(typeChecker, staticBlock, ScriptElementKind.constructorImplementationElement, "static {}", containerName, /*unverified*/ false, failedAliasResolution, { start: pos, length: "static".length }); }); } - const symbol = getSymbol(node, typeChecker); + let { symbol, failedAliasResolution } = getSymbol(node, typeChecker); + let fallbackNode = node; + + if (searchOtherFilesOnly && failedAliasResolution) { + // We couldn't resolve the specific import, try on the module specifier. + const importDeclaration = forEach([node, ...symbol?.declarations || emptyArray], n => findAncestor(n, isAnyImportOrBareOrAccessedRequire)); + const moduleSpecifier = importDeclaration && tryGetModuleSpecifierFromDeclaration(importDeclaration); + if (moduleSpecifier) { + ({ symbol, failedAliasResolution } = getSymbol(moduleSpecifier, typeChecker)); + fallbackNode = moduleSpecifier; + } + } + + if (!symbol && 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, getModeForUsageLocation(sourceFile, fallbackNode)); + if (ref) { + return [{ + name: fallbackNode.text, + fileName: ref.resolvedFileName, + containerName: undefined!, + containerKind: undefined!, + kind: ScriptElementKind.scriptElement, + textSpan: createTextSpan(0, 0), + failedAliasResolution, + isAmbient: isDeclarationFileName(ref.resolvedFileName), + unverified: fallbackNode !== node, + }]; + } + } // 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 @@ -47,17 +78,19 @@ namespace ts.GoToDefinition { return concatenate(fileReferenceDefinition, getDefinitionInfoForIndexSignatures(node, typeChecker)); } + if (searchOtherFilesOnly && every(symbol.declarations, d => d.getSourceFile().fileName === sourceFile.fileName)) return undefined; + const calledDeclaration = tryGetSignatureDeclaration(typeChecker, node); // Don't go to the component constructor definition for a JSX element, just go to the component definition. if (calledDeclaration && !(isJsxOpeningLikeElement(node.parent) && isConstructorLike(calledDeclaration))) { - const sigInfo = createDefinitionFromSignatureDeclaration(typeChecker, calledDeclaration); + 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, calledDeclaration) || emptyArray; + const defs = getDefinitionFromSymbol(typeChecker, symbol, node, failedAliasResolution, calledDeclaration) || emptyArray; // For a 'super()' call, put the signature first, else put the variable first. return node.kind === SyntaxKind.SuperKeyword ? [sigInfo, ...defs] : [...defs, sigInfo]; } @@ -70,7 +103,7 @@ namespace ts.GoToDefinition { // assignment. This case and others are handled by the following code. if (node.parent.kind === SyntaxKind.ShorthandPropertyAssignment) { const shorthandSymbol = typeChecker.getShorthandAssignmentValueSymbol(symbol.valueDeclaration); - const definitions = shorthandSymbol?.declarations ? shorthandSymbol.declarations.map(decl => createDefinitionInfo(decl, typeChecker, shorthandSymbol, node)) : emptyArray; + const definitions = shorthandSymbol?.declarations ? shorthandSymbol.declarations.map(decl => createDefinitionInfo(decl, typeChecker, shorthandSymbol, node, /*unverified*/ false, failedAliasResolution)) : emptyArray; return concatenate(definitions, getDefinitionFromObjectLiteralElement(typeChecker, node) || emptyArray); } @@ -95,7 +128,7 @@ namespace ts.GoToDefinition { }); } - return concatenate(fileReferenceDefinition, getDefinitionFromObjectLiteralElement(typeChecker, node) || getDefinitionFromSymbol(typeChecker, symbol, node)); + return concatenate(fileReferenceDefinition, getDefinitionFromObjectLiteralElement(typeChecker, node) || getDefinitionFromSymbol(typeChecker, symbol, node, failedAliasResolution)); } /** @@ -199,25 +232,25 @@ namespace ts.GoToDefinition { } if (isImportMeta(node.parent) && node.parent.name === node) { - return definitionFromType(typeChecker.getTypeAtLocation(node.parent), typeChecker, node.parent); + return definitionFromType(typeChecker.getTypeAtLocation(node.parent), typeChecker, node.parent, /*failedAliasResolution*/ false); } - const symbol = getSymbol(node, typeChecker); + const { symbol, failedAliasResolution } = getSymbol(node, typeChecker); if (!symbol) return undefined; const typeAtLocation = typeChecker.getTypeOfSymbolAtLocation(symbol, node); const returnType = tryGetReturnTypeOfFunction(symbol, typeAtLocation, typeChecker); - const fromReturnType = returnType && definitionFromType(returnType, typeChecker, node); + 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); + const typeDefinitions = fromReturnType && fromReturnType.length !== 0 ? fromReturnType : definitionFromType(typeAtLocation, typeChecker, node, failedAliasResolution); return typeDefinitions.length ? typeDefinitions - : !(symbol.flags & SymbolFlags.Value) && symbol.flags & SymbolFlags.Type ? getDefinitionFromSymbol(typeChecker, skipAlias(symbol, typeChecker), node) + : !(symbol.flags & SymbolFlags.Value) && symbol.flags & SymbolFlags.Type ? getDefinitionFromSymbol(typeChecker, skipAlias(symbol, typeChecker), node, failedAliasResolution) : undefined; } - function definitionFromType(type: Type, checker: TypeChecker, node: Node): readonly DefinitionInfo[] { + function definitionFromType(type: Type, checker: TypeChecker, node: Node, failedAliasResolution: boolean | undefined): readonly DefinitionInfo[] { return flatMap(type.isUnion() && !(type.flags & TypeFlags.Enum) ? type.types : [type], t => - t.symbol && getDefinitionFromSymbol(checker, t.symbol, node)); + t.symbol && getDefinitionFromSymbol(checker, t.symbol, node, failedAliasResolution)); } function tryGetReturnTypeOfFunction(symbol: Symbol, type: Type, checker: TypeChecker): Type | undefined { @@ -259,19 +292,23 @@ namespace ts.GoToDefinition { return mapDefined(checker.getIndexInfosAtLocation(node), info => info.declaration && createDefinitionFromSignatureDeclaration(checker, info.declaration)); } - function getSymbol(node: Node, checker: TypeChecker): Symbol | undefined { + function getSymbol(node: Node, checker: TypeChecker) { 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 & SymbolFlags.Alias && shouldSkipAlias(node, symbol.declarations[0])) { const aliased = checker.getAliasedSymbol(symbol); if (aliased.declarations) { - return aliased; + return { symbol: aliased }; + } + else { + failedAliasResolution = true; } } - return symbol; + return { symbol, failedAliasResolution }; } // Go to the original declaration for cases: @@ -286,28 +323,44 @@ namespace ts.GoToDefinition { if (node.parent === declaration) { return true; } - switch (declaration.kind) { - case SyntaxKind.ImportClause: - case SyntaxKind.ImportEqualsDeclaration: - return true; - case SyntaxKind.ImportSpecifier: - return declaration.parent.kind === SyntaxKind.NamedImports; - case SyntaxKind.BindingElement: - case SyntaxKind.VariableDeclaration: - return isInJSFile(declaration) && isVariableDeclarationInitializedToBareOrAccessedRequire(declaration); - default: - return false; + if (declaration.kind === SyntaxKind.NamespaceImport) { + return false; } + return true; + } + + /** + * ```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: Declaration): boolean { + if (!isAssignmentDeclaration(node)) return false; + const containingAssignment = findAncestor(node, p => { + if (isAssignmentExpression(p)) return true; + if (!isAssignmentDeclaration(p as Declaration)) return "quit"; + return false; + }) as AssignmentExpression | undefined; + return !!containingAssignment && getAssignmentDeclarationKind(containingAssignment) === AssignmentDeclarationKind.Property; } - function getDefinitionFromSymbol(typeChecker: TypeChecker, symbol: Symbol, node: Node, declarationNode?: Node): DefinitionInfo[] | undefined { - // There are cases when you extend a function by adding properties to it afterwards, - // we want to strip those extra properties. - // For deduping purposes, we also want to exclude any declarationNodes if provided. - const filteredDeclarations = - filter(symbol.declarations, d => d !== declarationNode && (!isAssignmentDeclaration(d) || d === symbol.valueDeclaration)) - || undefined; - return getConstructSignatureDefinition() || getCallSignatureDefinition() || map(filteredDeclarations, declaration => createDefinitionInfo(declaration, typeChecker, symbol, node)); + function getDefinitionFromSymbol(typeChecker: TypeChecker, symbol: Symbol, node: Node, failedAliasResolution?: boolean, excludeDeclaration?: Node): DefinitionInfo[] | undefined { + const filteredDeclarations = filter(symbol.declarations, d => d !== excludeDeclaration); + const withoutExpandos = filter(filteredDeclarations, d => !isExpandoDeclaration(d)); + const results = some(withoutExpandos) ? withoutExpandos : filteredDeclarations; + return getConstructSignatureDefinition() || getCallSignatureDefinition() || map(results, declaration => createDefinitionInfo(declaration, typeChecker, symbol, node, /*unverified*/ false, failedAliasResolution)); function getConstructSignatureDefinition(): DefinitionInfo[] | undefined { // Applicable only if we are in a new expression, or we are on a constructor declaration @@ -335,21 +388,21 @@ namespace ts.GoToDefinition { return declarations.length ? declarationsWithBody.length !== 0 ? declarationsWithBody.map(x => createDefinitionInfo(x, typeChecker, symbol, node)) - : [createDefinitionInfo(last(declarations), typeChecker, symbol, node)] + : [createDefinitionInfo(last(declarations), typeChecker, symbol, node, /*unverified*/ false, failedAliasResolution)] : undefined; } } /** Creates a DefinitionInfo from a Declaration, using the declaration's name if possible. */ - function createDefinitionInfo(declaration: Declaration, checker: TypeChecker, symbol: Symbol, node: Node): DefinitionInfo { + export function createDefinitionInfo(declaration: Declaration, checker: TypeChecker, symbol: Symbol, node: Node, unverified?: boolean, failedAliasResolution?: boolean): DefinitionInfo { const symbolName = checker.symbolToString(symbol); // Do not get scoped name, just the name of the symbol const symbolKind = SymbolDisplay.getSymbolKind(checker, symbol, node); const containerName = symbol.parent ? checker.symbolToString(symbol.parent, node) : ""; - return createDefinitionInfoFromName(checker, declaration, symbolKind, symbolName, containerName); + return createDefinitionInfoFromName(checker, declaration, symbolKind, symbolName, containerName, unverified, failedAliasResolution); } /** Creates a DefinitionInfo directly from the name of a declaration. */ - function createDefinitionInfoFromName(checker: TypeChecker, declaration: Declaration, symbolKind: ScriptElementKind, symbolName: string, containerName: string, textSpan?: TextSpan): DefinitionInfo { + function createDefinitionInfoFromName(checker: TypeChecker, declaration: Declaration, symbolKind: ScriptElementKind, symbolName: string, containerName: string, unverified?: boolean, failedAliasResolution?: boolean, textSpan?: TextSpan): DefinitionInfo { const sourceFile = declaration.getSourceFile(); if (!textSpan) { const name = getNameOfDeclaration(declaration) || declaration; @@ -367,7 +420,10 @@ namespace ts.GoToDefinition { sourceFile, FindAllReferences.getContextNode(declaration) ), - isLocal: !isDefinitionVisible(checker, declaration) + isLocal: !isDefinitionVisible(checker, declaration), + isAmbient: !!(declaration.flags & NodeFlags.Ambient), + unverified, + failedAliasResolution, }; } @@ -402,8 +458,8 @@ namespace ts.GoToDefinition { } } - function createDefinitionFromSignatureDeclaration(typeChecker: TypeChecker, decl: SignatureDeclaration): DefinitionInfo { - return createDefinitionInfo(decl, typeChecker, decl.symbol, decl); + function createDefinitionFromSignatureDeclaration(typeChecker: TypeChecker, decl: SignatureDeclaration, failedAliasResolution?: boolean): DefinitionInfo { + return createDefinitionInfo(decl, typeChecker, decl.symbol, decl, /*unverified*/ false, failedAliasResolution); } export function findReferenceInPosition(refs: readonly FileReference[], pos: number): FileReference | undefined { diff --git a/src/services/services.ts b/src/services/services.ts index 9e39d4a405330..20373fa773e91 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1768,9 +1768,9 @@ namespace ts { } /// Goto definition - function getDefinitionAtPosition(fileName: string, position: number): readonly DefinitionInfo[] | undefined { + function getDefinitionAtPosition(fileName: string, position: number, searchOtherFilesOnly?: boolean): readonly DefinitionInfo[] | undefined { synchronizeHostData(); - return GoToDefinition.getDefinitionAtPosition(program, getValidSourceFile(fileName), position); + return GoToDefinition.getDefinitionAtPosition(program, getValidSourceFile(fileName), position, searchOtherFilesOnly); } function getDefinitionAndBoundSpan(fileName: string, position: number): DefinitionInfoAndBoundSpan | undefined { diff --git a/src/services/types.ts b/src/services/types.ts index 54bd93f7f32ce..f84ee7d3ca16b 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -472,6 +472,9 @@ namespace ts { getSmartSelectionRange(fileName: string, position: number): SelectionRange; + /*@internal*/ + // eslint-disable-next-line @typescript-eslint/unified-signatures + getDefinitionAtPosition(fileName: string, position: number, searchOtherFilesOnly: boolean): 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; @@ -1032,6 +1035,8 @@ namespace ts { containerName: string; unverified?: boolean; /* @internal */ isLocal?: boolean; + /* @internal */ isAmbient?: boolean; + /* @internal */ failedAliasResolution?: boolean; } export interface DefinitionInfoAndBoundSpan { diff --git a/src/testRunner/tsconfig.json b/src/testRunner/tsconfig.json index 2caa9873f29dd..599aaa19a8615 100644 --- a/src/testRunner/tsconfig.json +++ b/src/testRunner/tsconfig.json @@ -169,6 +169,7 @@ "unittests/tscWatch/watchEnvironment.ts", "unittests/tsserver/applyChangesToOpenFiles.ts", "unittests/tsserver/autoImportProvider.ts", + "unittests/tsserver/auxiliaryProject.ts", "unittests/tsserver/cachingFileSystemInformation.ts", "unittests/tsserver/cancellationToken.ts", "unittests/tsserver/compileOnSave.ts", diff --git a/src/testRunner/unittests/tsserver/auxiliaryProject.ts b/src/testRunner/unittests/tsserver/auxiliaryProject.ts new file mode 100644 index 0000000000000..4b5f865c19aeb --- /dev/null +++ b/src/testRunner/unittests/tsserver/auxiliaryProject.ts @@ -0,0 +1,47 @@ +namespace ts.projectSystem { + const aTs: File = { + path: "/a.ts", + content: `import { B } from "./b";` + }; + const bDts: File = { + path: "/b.d.ts", + content: `export declare class B {}` + }; + const bJs: File = { + path: "/b.js", + content: `export class B {}` + }; + describe("unittests:: tsserver:: auxiliaryProject", () => { + it("AuxiliaryProject does not remove scrips from InferredProject", () => { + const host = createServerHost([aTs, bDts, bJs]); + const session = createSession(host); + const projectService = session.getProjectService(); + openFilesForSession([aTs], session); + + // Open file is in inferred project + 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(); + + // 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 = 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. + openFilesForSession([bJs], session); + assert(!bJsScriptInfo.isOrphan()); + assert(bJsScriptInfo.isContainedByBackgroundProject()); + assert.equal(bJsScriptInfo.getDefaultProject().projectKind, server.ProjectKind.Inferred); + }); + }); +} diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 6146af8bb9f75..a08dea9d05f4a 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -7038,6 +7038,7 @@ declare namespace ts.server.protocol { Rename = "rename", Saveto = "saveto", SignatureHelp = "signatureHelp", + FindSourceDefinition = "findSourceDefinition", Status = "status", TypeDefinition = "typeDefinition", ProjectInfo = "projectInfo", @@ -7657,6 +7658,9 @@ declare namespace ts.server.protocol { interface DefinitionAndBoundSpanRequest extends FileLocationRequest { readonly command: CommandTypes.DefinitionAndBoundSpan; } + interface FindSourceDefinitionRequest extends FileLocationRequest { + readonly command: CommandTypes.FindSourceDefinition; + } interface DefinitionAndBoundSpanResponse extends Response { readonly body: DefinitionInfoAndBoundSpan; } @@ -9905,7 +9909,8 @@ declare namespace ts.server { Inferred = 0, Configured = 1, External = 2, - AutoImportProvider = 3 + AutoImportProvider = 3, + Auxiliary = 4 } function allRootFilesAreJsOrDts(project: Project): boolean; function allFilesAreJsOrDts(project: Project): boolean; @@ -10640,6 +10645,7 @@ declare namespace ts.server { private getDefinition; private mapDefinitionInfoLocations; private getDefinitionAndBoundSpan; + private findSourceDefinition; private getEmitOutput; private mapJSDocTagInfo; private mapDisplayParts; diff --git a/tests/baselines/reference/showConfig/Shows tsconfig for single option/noDtsResolution/tsconfig.json b/tests/baselines/reference/showConfig/Shows tsconfig for single option/noDtsResolution/tsconfig.json new file mode 100644 index 0000000000000..a879d26a750ec --- /dev/null +++ b/tests/baselines/reference/showConfig/Shows tsconfig for single option/noDtsResolution/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "noDtsResolution": true + } +} diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index 486d24d4922a3..93aca2cfb1f89 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -317,6 +317,9 @@ declare namespace FourSlashInterface { goToDefinition(startsAndEnds: { [startMarkerName: string]: ArrayOrSingle }): void; /** Verifies goToDefinition for each `${markerName}Reference` -> `${markerName}Definition` */ goToDefinitionForMarkers(...markerNames: string[]): void; + goToSourceDefinition(startMarkerNames: ArrayOrSingle, fileResult: { file: string, unverified?: boolean }): void; + goToSourceDefinition(startMarkerNames: ArrayOrSingle, endMarkerNames: ArrayOrSingle): void; + goToSourceDefinition(startMarkerNames: ArrayOrSingle, endMarkerNames: ArrayOrSingle): void; goToType(startsAndEnds: { [startMarkerName: string]: ArrayOrSingle }): void; goToType(startMarkerNames: ArrayOrSingle, endMarkerNames: ArrayOrSingle): void; verifyGetEmitOutputForCurrentFile(expected: string): void; diff --git a/tests/cases/fourslash/goToDefinitionExpandoElementAccess.ts b/tests/cases/fourslash/goToDefinitionExpandoElementAccess.ts index 5deeb4c02532e..67efdb195e690 100644 --- a/tests/cases/fourslash/goToDefinitionExpandoElementAccess.ts +++ b/tests/cases/fourslash/goToDefinitionExpandoElementAccess.ts @@ -4,4 +4,4 @@ ////f[/*0*/"x"] = 0; ////f[[|/*1*/"x"|]] = 1; -verify.goToDefinition("1", "0"); +verify.goToDefinition("1", ["0", "1"]); diff --git a/tests/cases/fourslash/quickInfoUntypedModuleImport.ts b/tests/cases/fourslash/quickInfoUntypedModuleImport.ts index a25be52954766..15bc5000d1eff 100644 --- a/tests/cases/fourslash/quickInfoUntypedModuleImport.ts +++ b/tests/cases/fourslash/quickInfoUntypedModuleImport.ts @@ -1,7 +1,7 @@ /// // @Filename: node_modules/foo/index.js -////{} +//// /*index*/{} // @Filename: a.ts ////import /*foo*/foo from /*fooModule*/"foo"; @@ -11,7 +11,7 @@ goTo.file("a.ts"); verify.numberOfErrorsInCurrentFile(0); goTo.marker("fooModule"); -verify.goToDefinitionIs([]); +verify.goToDefinitionIs(["index"]); verify.quickInfoIs(""); goTo.marker("foo"); diff --git a/tests/cases/fourslash/server/goToSource10_mapFromAtTypes3.ts b/tests/cases/fourslash/server/goToSource10_mapFromAtTypes3.ts new file mode 100644 index 0000000000000..6b636839e1781 --- /dev/null +++ b/tests/cases/fourslash/server/goToSource10_mapFromAtTypes3.ts @@ -0,0 +1,72 @@ +/// + +// @moduleResolution: node + +// @Filename: /node_modules/lodash/package.json +//// { "name": "lodash", "version": "4.17.15", "main": "./lodash.js" } + +// @Filename: /node_modules/lodash/lodash.js +//// ;(function() { +//// /** +//// * Adds two numbers. +//// * +//// * @static +//// * @memberOf _ +//// * @since 3.4.0 +//// * @category Math +//// * @param {number} augend The first number in an addition. +//// * @param {number} addend The second number in an addition. +//// * @returns {number} Returns the total. +//// * @example +//// * +//// * _.add(6, 4); +//// * // => 10 +//// */ +//// var [|/*variable*/add|] = createMathOperation(function(augend, addend) { +//// return augend + addend; +//// }, 0); +//// +//// function lodash(value) {} +//// lodash.[|/*property*/add|] = add; +//// +//// /** Detect free variable `global` from Node.js. */ +//// var freeGlobal = typeof global == 'object' && global && global.Object === Object && global; +//// /** Detect free variable `self`. */ +//// var freeSelf = typeof self == 'object' && self && self.Object === Object && self; +//// /** Used as a reference to the global object. */ +//// var root = freeGlobal || freeSelf || Function('return this')(); +//// /** Detect free variable `exports`. */ +//// var freeExports = typeof exports == 'object' && exports && !exports.nodeType && exports;//// +//// /** Detect free variable `module`. */ +//// var freeModule = freeExports && typeof module == 'object' && module && !module.nodeType && module; +//// if (freeModule) { +//// // Export for Node.js. +//// (freeModule.exports = _)._ = _; +//// // Export for CommonJS support. +//// freeExports._ = _; +//// } +//// else { +//// // Export to the global object. +//// root._ = _; +//// } +//// }.call(this)); + +// @Filename: /node_modules/@types/lodash/package.json +//// { "name": "@types/lodash", "version": "4.14.97", "types": "index.d.ts" } + +// @Filename: /node_modules/@types/lodash/index.d.ts +//// export = _; +//// export as namespace _; +//// declare const _: _.LoDashStatic; +//// declare namespace _ { +//// interface LoDashStatic {} +//// } + + +// @Filename: /index.ts +//// import { [|/*start*/add|] } from 'lodash'; + +verify.goToSourceDefinition("start", [ + { marker: "variable", unverified: true }, + { marker: "property", unverified: true }, +]); diff --git a/tests/cases/fourslash/server/goToSource11_propertyOfAlias.ts b/tests/cases/fourslash/server/goToSource11_propertyOfAlias.ts new file mode 100644 index 0000000000000..9856d82610946 --- /dev/null +++ b/tests/cases/fourslash/server/goToSource11_propertyOfAlias.ts @@ -0,0 +1,15 @@ +/// + +// @moduleResolution: node + +// @Filename: /a.js +//// export const a = { /*end*/a: 'a' }; + +// @Filename: /a.d.ts +//// export declare const a: { a: string }; + +// @Filename: /b.ts +//// import { a } from './a'; +//// a.[|a/*start*/|] + +verify.goToSourceDefinition("start", "end"); diff --git a/tests/cases/fourslash/server/goToSource12_callbackParam.ts b/tests/cases/fourslash/server/goToSource12_callbackParam.ts new file mode 100644 index 0000000000000..948591ba48e5f --- /dev/null +++ b/tests/cases/fourslash/server/goToSource12_callbackParam.ts @@ -0,0 +1,43 @@ +/// + +// @moduleResolution: node + +// Actual yargs doesn't work like this because the implementation package's +// main entry exports a small function wrapper function whose return value +// is derived from something imported from another file where all the +// important code lives, and it's not quite clear on what grounds we can +// make the jump from the main entry to that other file. But this test +// demonstrates the need to do some filename-based jumping: regular go-to-def +// on `yargs` goes to the type definition `Yargs`, but a JS-only go-to-def +// simply stops on the callback parameter. So we have to start at the type +// definition and say "well, maybe I can find a JS counterpart to this .d.ts +// and just look for declarations called 'positional' in there." + +// @Filename: /node_modules/@types/yargs/package.json +//// { +//// "name": "@types/yargs", +//// "version": "1.0.0", +//// "types": "./index.d.ts" +//// } + +// @Filename: /node_modules/@types/yargs/index.d.ts +//// export interface Yargs { positional(): Yargs; } +//// export declare function command(command: string, cb: (yargs: Yargs) => void): void; + +// @Filename: /node_modules/yargs/package.json +//// { +//// "name": "yargs", +//// "version": "1.0.0", +//// "main": "index.js" +//// } + +// @Filename: /node_modules/yargs/index.js +//// export function command(cmd, cb) { cb({ /*end*/positional: "This is obviously not even close to realistic" }); } + +// @Filename: /index.ts +//// import { command } from "yargs"; +//// command("foo", yargs => { +//// yargs.[|/*start*/positional|](); +//// }); + +verify.goToSourceDefinition("start", { marker: "end", unverified: true }); diff --git a/tests/cases/fourslash/server/goToSource1_localJsBesideDts.ts b/tests/cases/fourslash/server/goToSource1_localJsBesideDts.ts new file mode 100644 index 0000000000000..5685db4076977 --- /dev/null +++ b/tests/cases/fourslash/server/goToSource1_localJsBesideDts.ts @@ -0,0 +1,14 @@ +/// + +// @Filename: /a.js +//// export const /*end*/a = "a"; + +// @Filename: /a.d.ts +//// export declare const a: string; + +// @Filename: /index.ts +//// import { a } from [|"./a"/*moduleSpecifier*/|]; +//// [|a/*identifier*/|] + +verify.goToSourceDefinition("identifier", "end"); +verify.goToSourceDefinition("moduleSpecifier", { file: "/a.js" }) \ No newline at end of file diff --git a/tests/cases/fourslash/server/goToSource2_nodeModulesWithTypes.ts b/tests/cases/fourslash/server/goToSource2_nodeModulesWithTypes.ts new file mode 100644 index 0000000000000..a2166786c54cd --- /dev/null +++ b/tests/cases/fourslash/server/goToSource2_nodeModulesWithTypes.ts @@ -0,0 +1,18 @@ +/// + +// @moduleResolution: node + +// @Filename: /node_modules/foo/package.json +//// { "name": "foo", "version": "1.0.0", "main": "./lib/main.js", "types": "./types/main.d.ts" } + +// @Filename: /node_modules/foo/lib/main.js +//// export const /*end*/a = "a"; + +// @Filename: /node_modules/foo/types/main.d.ts +//// export declare const a: string; + +// @Filename: /index.ts +//// import { a } from "foo"; +//// [|a/*start*/|] + +verify.goToSourceDefinition("start", "end"); diff --git a/tests/cases/fourslash/server/goToSource3_nodeModulesAtTypes.ts b/tests/cases/fourslash/server/goToSource3_nodeModulesAtTypes.ts new file mode 100644 index 0000000000000..152d411040a12 --- /dev/null +++ b/tests/cases/fourslash/server/goToSource3_nodeModulesAtTypes.ts @@ -0,0 +1,21 @@ +/// + +// @moduleResolution: node + +// @Filename: /node_modules/foo/package.json +//// { "name": "foo", "version": "1.0.0", "main": "./lib/main.js" } + +// @Filename: /node_modules/foo/lib/main.js +//// export const /*end*/a = "a"; + +// @Filename: /node_modules/@types/foo/package.json +//// { "name": "@types/foo", "version": "1.0.0", "types": "./index.d.ts" } + +// @Filename: /node_modules/@types/foo/index.d.ts +//// export declare const a: string; + +// @Filename: /index.ts +//// import { a } from "foo"; +//// [|a/*start*/|] + +verify.goToSourceDefinition("start", "end"); diff --git a/tests/cases/fourslash/server/goToSource5_sameAsGoToDef1.ts b/tests/cases/fourslash/server/goToSource5_sameAsGoToDef1.ts new file mode 100644 index 0000000000000..f2b7e2b73457f --- /dev/null +++ b/tests/cases/fourslash/server/goToSource5_sameAsGoToDef1.ts @@ -0,0 +1,17 @@ +/// + +// @Filename: /a.ts +//// export const /*end*/a = 'a'; + +// @Filename: /a.d.ts +//// export declare const a: string; + +// @Filename: /a.js +//// export const a = 'a'; + +// @Filename: /b.ts +//// import { a } from './a'; +//// [|a/*start*/|] + +verify.goToDefinition("start", "end"); +verify.goToSourceDefinition("start", "end"); diff --git a/tests/cases/fourslash/server/goToSource6_sameAsGoToDef2.ts b/tests/cases/fourslash/server/goToSource6_sameAsGoToDef2.ts new file mode 100644 index 0000000000000..87b7065a17b0a --- /dev/null +++ b/tests/cases/fourslash/server/goToSource6_sameAsGoToDef2.ts @@ -0,0 +1,24 @@ +/// + +// @Filename: /node_modules/foo/package.json +//// { "name": "foo", "version": "1.2.3", "typesVersions": { "*": { "*": ["./types/*"] } } } + +// @Filename: /node_modules/foo/src/a.ts +//// export const /*end*/a = 'a'; + +// @Filename: /node_modules/foo/types/a.d.ts +//// export declare const a: string; +//// //# sourceMappingURL=a.d.ts.map + +// @Filename: /node_modules/foo/types/a.d.ts.map +//// {"version":3,"file":"a.d.ts","sourceRoot":"","sources":["../src/a.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,EAAE,OAAO,CAAC;;AACvB,wBAAsB"} + +// @Filename: /node_modules/foo/dist/a.js +//// export const a = 'a'; + +// @Filename: /b.ts +//// import { a } from 'foo/a'; +//// [|a/*start*/|] + +verify.goToDefinition("start", "end"); +verify.goToSourceDefinition("start", "end"); diff --git a/tests/cases/fourslash/server/goToSource7_conditionallyMinified.ts b/tests/cases/fourslash/server/goToSource7_conditionallyMinified.ts new file mode 100644 index 0000000000000..6a0e13962ba38 --- /dev/null +++ b/tests/cases/fourslash/server/goToSource7_conditionallyMinified.ts @@ -0,0 +1,33 @@ +/// + +// @moduleResolution: node + +// @Filename: /node_modules/react/package.json +//// { "name": "react", "version": "16.8.6", "main": "index.js" } + +// @Filename: /node_modules/react/index.js +//// 'use strict'; +//// +//// if (process.env.NODE_ENV === 'production') { +//// module.exports = require('./cjs/react.production.min.js'); +//// } else { +//// module.exports = require('./cjs/react.development.js'); +//// } + +// @Filename: /node_modules/react/cjs/react.production.min.js +//// 'use strict';exports./*production*/useState=function(a){};exports.version='16.8.6'; + +// @Filename: /node_modules/react/cjs/react.development.js +//// 'use strict'; +//// if (process.env.NODE_ENV !== 'production') { +//// (function() { +//// function useState(initialState) {} +//// exports./*development*/useState = useState; +//// exports.version = '16.8.6'; +//// }()); +//// } + +// @Filename: /index.ts +//// import { [|/*start*/useState|] } from 'react'; + +verify.goToSourceDefinition("start", ["production", "development"]); diff --git a/tests/cases/fourslash/server/goToSource8_mapFromAtTypes.ts b/tests/cases/fourslash/server/goToSource8_mapFromAtTypes.ts new file mode 100644 index 0000000000000..690ae8b5fa5a6 --- /dev/null +++ b/tests/cases/fourslash/server/goToSource8_mapFromAtTypes.ts @@ -0,0 +1,80 @@ +/// + +// @moduleResolution: node + +// @Filename: /node_modules/lodash/package.json +//// { "name": "lodash", "version": "4.17.15", "main": "./lodash.js" } + +// @Filename: /node_modules/lodash/lodash.js +//// ;(function() { +//// /** +//// * Adds two numbers. +//// * +//// * @static +//// * @memberOf _ +//// * @since 3.4.0 +//// * @category Math +//// * @param {number} augend The first number in an addition. +//// * @param {number} addend The second number in an addition. +//// * @returns {number} Returns the total. +//// * @example +//// * +//// * _.add(6, 4); +//// * // => 10 +//// */ +//// var [|/*variable*/add|] = createMathOperation(function(augend, addend) { +//// return augend + addend; +//// }, 0); +//// +//// function lodash(value) {} +//// lodash.[|/*property*/add|] = add; +//// +//// /** Detect free variable `global` from Node.js. */ +//// var freeGlobal = typeof global == 'object' && global && global.Object === Object && global; +//// /** Detect free variable `self`. */ +//// var freeSelf = typeof self == 'object' && self && self.Object === Object && self; +//// /** Used as a reference to the global object. */ +//// var root = freeGlobal || freeSelf || Function('return this')(); +//// /** Detect free variable `exports`. */ +//// var freeExports = typeof exports == 'object' && exports && !exports.nodeType && exports;//// +//// /** Detect free variable `module`. */ +//// var freeModule = freeExports && typeof module == 'object' && module && !module.nodeType && module; +//// if (freeModule) { +//// // Export for Node.js. +//// (freeModule.exports = _)._ = _; +//// // Export for CommonJS support. +//// freeExports._ = _; +//// } +//// else { +//// // Export to the global object. +//// root._ = _; +//// } +//// }.call(this)); + +// @Filename: /node_modules/@types/lodash/package.json +//// { "name": "@types/lodash", "version": "4.14.97", "types": "index.d.ts" } + +// @Filename: /node_modules/@types/lodash/index.d.ts +//// /// +//// export = _; +//// export as namespace _; +//// declare const _: _.LoDashStatic; +//// declare namespace _ { +//// interface LoDashStatic {} +//// } + +// @Filename: /node_modules/@types/lodash/common/math.d.ts +//// import _ = require("../index"); +//// declare module "../index" { +//// interface LoDashStatic { +//// add(augend: number, addend: number): number; +//// } +//// } + +// @Filename: /index.ts +//// import { [|/*start*/add|] } from 'lodash'; + +verify.goToSourceDefinition("start", [ + { marker: "variable", unverified: true }, + { marker: "property", unverified: true }, +]); diff --git a/tests/cases/fourslash/server/goToSource9_mapFromAtTypes2.ts b/tests/cases/fourslash/server/goToSource9_mapFromAtTypes2.ts new file mode 100644 index 0000000000000..024fe099b4d8e --- /dev/null +++ b/tests/cases/fourslash/server/goToSource9_mapFromAtTypes2.ts @@ -0,0 +1,81 @@ +/// + +// @moduleResolution: node + +// @Filename: /node_modules/lodash/package.json +//// { "name": "lodash", "version": "4.17.15", "main": "./lodash.js" } + +// @Filename: /node_modules/lodash/lodash.js +//// ;(function() { +//// /** +//// * Adds two numbers. +//// * +//// * @static +//// * @memberOf _ +//// * @since 3.4.0 +//// * @category Math +//// * @param {number} augend The first number in an addition. +//// * @param {number} addend The second number in an addition. +//// * @returns {number} Returns the total. +//// * @example +//// * +//// * _.add(6, 4); +//// * // => 10 +//// */ +//// var [|/*variable*/add|] = createMathOperation(function(augend, addend) { +//// return augend + addend; +//// }, 0); +//// +//// function lodash(value) {} +//// lodash.[|/*property*/add|] = add; +//// +//// /** Detect free variable `global` from Node.js. */ +//// var freeGlobal = typeof global == 'object' && global && global.Object === Object && global; +//// /** Detect free variable `self`. */ +//// var freeSelf = typeof self == 'object' && self && self.Object === Object && self; +//// /** Used as a reference to the global object. */ +//// var root = freeGlobal || freeSelf || Function('return this')(); +//// /** Detect free variable `exports`. */ +//// var freeExports = typeof exports == 'object' && exports && !exports.nodeType && exports;//// +//// /** Detect free variable `module`. */ +//// var freeModule = freeExports && typeof module == 'object' && module && !module.nodeType && module; +//// if (freeModule) { +//// // Export for Node.js. +//// (freeModule.exports = _)._ = _; +//// // Export for CommonJS support. +//// freeExports._ = _; +//// } +//// else { +//// // Export to the global object. +//// root._ = _; +//// } +//// }.call(this)); + +// @Filename: /node_modules/@types/lodash/package.json +//// { "name": "@types/lodash", "version": "4.14.97", "types": "index.d.ts" } + +// @Filename: /node_modules/@types/lodash/index.d.ts +//// /// +//// export = _; +//// export as namespace _; +//// declare const _: _.LoDashStatic; +//// declare namespace _ { +//// interface LoDashStatic {} +//// } + +// @Filename: /node_modules/@types/lodash/common/math.d.ts +//// import _ = require("../index"); +//// declare module "../index" { +//// interface LoDashStatic { +//// add(augend: number, addend: number): number; +//// } +//// } + +// @Filename: /index.ts +//// import [|/*defaultImport*/_|], { [|/*unresolvableNamedImport*/foo|] } from [|/*moduleSpecifier*/'lodash'|]; +//// _.[|/*propertyAccess*/add|] + +verify.goToSourceDefinition("defaultImport", { file: "/node_modules/lodash/lodash.js", unverified: true }); +verify.goToSourceDefinition("unresolvableNamedImport", { file: "/node_modules/lodash/lodash.js", unverified: true }); +verify.goToSourceDefinition("moduleSpecifier", { file: "/node_modules/lodash/lodash.js", unverified: true }); +verify.goToSourceDefinition("propertyAccess", [{ marker: "variable", unverified: true }, { marker: "property", unverified: true }])