Skip to content

Implement tryLoadInputFileForPath #1302

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 169 additions & 3 deletions internal/module/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/diagnostics"
"github.com/microsoft/typescript-go/internal/outputpaths"
"github.com/microsoft/typescript-go/internal/packagejson"
"github.com/microsoft/typescript-go/internal/semver"
"github.com/microsoft/typescript-go/internal/tspath"
Expand Down Expand Up @@ -54,7 +55,7 @@ type resolutionState struct {
resolvedPackageDirectory bool
failedLookupLocations []string
affectingLocations []string
diagnostics []ast.Diagnostic
diagnostics []*ast.Diagnostic
}

func newResolutionState(
Expand Down Expand Up @@ -748,10 +749,175 @@ func (r *resolutionState) loadModuleFromTargetExportOrImport(extensions extensio
}

func (r *resolutionState) tryLoadInputFileForPath(finalPath string, entry string, packagePath string, isImports bool) *resolved {
Copy link
Preview

Copilot AI Jun 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The logic within tryLoadInputFileForPath is highly complex with deeply nested conditionals and loops (particularly lines 757–871). Consider extracting portions of this logic into smaller, well-named helper functions to improve readability and ease testing.

Copilot uses AI. Check for mistakes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That ship sailed long ago, pal

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don’t think this complex logic needs to be ported at all—inferring the rootDir based on file inclusion is deprecated in 6.0, except in tsconfig-less compilations, which seems unlikely to happen with outDir / declarationDir anyway. I objected to the guessing logic when it went into Strada but was overruled; it seems even more unnecessary now.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess I'm confused, because the reason I'm porting this is to make self-reference imports map properly, per the issue I'm fixing where if we don't do this we get output dts files in the program.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I wasn’t specific enough—this function needs to exist, but I think it should just error (which we won’t actually collect now, as you noted) and return a failed resolution if it can’t compute a rootDir instead of doing all the complicated guessing.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see; I'll give that a shot. This PR was me just asking copilot to port the whole thing which worked, so...

#1304 is my followup to actually report these diags, which I could flip to point at main first.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I'm missing something, but how can I actually emit this error? If I stop the resolution here by adding an error and then return &resolved{}, then all other later resolution steps stop working and hundreds of tests break.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I just say to continue searching, then we end up with the same behavior again, where we end up with output files in the input set, and package import mappings stop working...

// !!!
// 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 !r.isConfigLookup &&
(r.compilerOptions.DeclarationDir != "" || r.compilerOptions.OutDir != "") &&
!strings.Contains(finalPath, "/node_modules/") &&
(r.compilerOptions.ConfigFilePath == "" || tspath.ContainsPath(
tspath.GetDirectoryPath(packagePath),
r.compilerOptions.ConfigFilePath,
tspath.ComparePathsOptions{
UseCaseSensitiveFileNames: r.resolver.host.FS().UseCaseSensitiveFileNames(),
CurrentDirectory: r.resolver.host.GetCurrentDirectory(),
},
)) {
// 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.)

useCaseSensitiveFileNames := r.resolver.host.FS().UseCaseSensitiveFileNames()
var 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 r.compilerOptions.RootDir != "" || (r.compilerOptions.Composite.IsTrue() && r.compilerOptions.ConfigFilePath != "") {
commonDir := tspath.GetNormalizedAbsolutePath(r.getCommonSourceDirectory(), r.resolver.host.GetCurrentDirectory())
commonSourceDirGuesses = append(commonSourceDirGuesses, commonDir)
} else if r.containingDirectory != "" {
// 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.containingDirectory

requestingFile := tspath.GetNormalizedAbsolutePath(tspath.CombinePaths(r.containingDirectory, "index.ts"), r.resolver.host.GetCurrentDirectory())
// 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.
commonDir := tspath.GetNormalizedAbsolutePath(r.getCommonSourceDirectoryForFiles([]string{requestingFile, tspath.GetNormalizedAbsolutePath(packagePath, r.resolver.host.GetCurrentDirectory())}), r.resolver.host.GetCurrentDirectory())
commonSourceDirGuesses = append(commonSourceDirGuesses, commonDir)

fragment := tspath.EnsureTrailingDirectorySeparator(commonDir)
for len(fragment) > 1 {
parts := tspath.GetPathComponents(fragment, "")
if len(parts) <= 1 {
break
}
parts = parts[:len(parts)-1] // remove a directory
commonDir := tspath.GetPathFromPathComponents(parts)
commonSourceDirGuesses = append([]string{commonDir}, commonSourceDirGuesses...) // unshift
fragment = tspath.EnsureTrailingDirectorySeparator(commonDir)
}
}

if len(commonSourceDirGuesses) > 1 {
var diagnostic *ast.Diagnostic
if isImports {
diagnostic = ast.NewDiagnostic(
nil,
core.TextRange{},
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,
core.IfElse(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,
)
} else {
diagnostic = ast.NewDiagnostic(
nil,
core.TextRange{},
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,
core.IfElse(entry == "", ".", entry),
packagePath,
)
}
r.diagnostics = append(r.diagnostics, diagnostic)
}

for _, commonSourceDirGuess := range commonSourceDirGuesses {
candidateDirectories := r.getOutputDirectoriesForBaseDirectory(commonSourceDirGuess)
for _, candidateDir := range candidateDirectories {
if tspath.ContainsPath(candidateDir, finalPath, tspath.ComparePathsOptions{
UseCaseSensitiveFileNames: useCaseSensitiveFileNames,
CurrentDirectory: r.resolver.host.GetCurrentDirectory(),
}) {
// 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
pathFragment := finalPath[len(candidateDir)+1:] // +1 to also remove directory separator
possibleInputBase := tspath.CombinePaths(commonSourceDirGuess, pathFragment)
jsAndDtsExtensions := []string{tspath.ExtensionMjs, tspath.ExtensionCjs, tspath.ExtensionJs, tspath.ExtensionJson, tspath.ExtensionDmts, tspath.ExtensionDcts, tspath.ExtensionDts}
for _, ext := range jsAndDtsExtensions {
if tspath.FileExtensionIs(possibleInputBase, ext) {
inputExts := r.getPossibleOriginalInputExtensionForExtension(possibleInputBase)
for _, possibleExt := range inputExts {
if !extensionIsOk(r.extensions, possibleExt) {
continue
}
possibleInputWithInputExtension := tspath.ChangeExtension(possibleInputBase, possibleExt)
if r.resolver.host.FS().FileExists(possibleInputWithInputExtension) {
resolved := r.loadFileNameFromPackageJSONField(r.extensions, possibleInputWithInputExtension, "", false)
if !resolved.shouldContinueSearching() {
return resolved
}
}
}
}
}
}
}
}
}
return continueSearching()
}

func (r *resolutionState) getCommonSourceDirectory() string {
return outputpaths.GetCommonSourceDirectory(
r.compilerOptions,
func() []string { return []string{} }, // Empty files function for now
r.resolver.host.GetCurrentDirectory(),
r.resolver.host.FS().UseCaseSensitiveFileNames(),
)
}

func (r *resolutionState) getCommonSourceDirectoryForFiles(files []string) string {
return outputpaths.GetCommonSourceDirectory(
r.compilerOptions,
func() []string { return files },
r.resolver.host.GetCurrentDirectory(),
r.resolver.host.FS().UseCaseSensitiveFileNames(),
)
}

func (r *resolutionState) getOutputDirectoriesForBaseDirectory(commonSourceDirGuess string) []string {
// Config file output 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
currentDir := core.IfElse(r.compilerOptions.ConfigFilePath != "", r.resolver.host.GetCurrentDirectory(), commonSourceDirGuess)
var candidateDirectories []string
if r.compilerOptions.DeclarationDir != "" {
candidateDirectories = append(candidateDirectories, tspath.GetNormalizedAbsolutePath(tspath.CombinePaths(currentDir, r.compilerOptions.DeclarationDir), r.resolver.host.GetCurrentDirectory()))
}
if r.compilerOptions.OutDir != "" && r.compilerOptions.OutDir != r.compilerOptions.DeclarationDir {
candidateDirectories = append(candidateDirectories, tspath.GetNormalizedAbsolutePath(tspath.CombinePaths(currentDir, r.compilerOptions.OutDir), r.resolver.host.GetCurrentDirectory()))
}
return candidateDirectories
}

func (r *resolutionState) getPossibleOriginalInputExtensionForExtension(path string) []string {
if tspath.FileExtensionIsOneOf(path, []string{tspath.ExtensionDmts, tspath.ExtensionMjs, tspath.ExtensionMts}) {
return []string{tspath.ExtensionMts, tspath.ExtensionMjs}
}
if tspath.FileExtensionIsOneOf(path, []string{tspath.ExtensionDcts, tspath.ExtensionCjs, tspath.ExtensionCts}) {
return []string{tspath.ExtensionCts, tspath.ExtensionCjs}
}
if tspath.FileExtensionIs(path, ".d.json.ts") {
return []string{tspath.ExtensionJson}
}
return []string{tspath.ExtensionTsx, tspath.ExtensionTs, tspath.ExtensionJsx, tspath.ExtensionJs}
}

func (r *resolutionState) loadModuleFromNearestNodeModulesDirectory(typesScopeOnly bool) *resolved {
mode := core.ResolutionModeCommonJS
if r.esmMode || r.conditionMatches("import") {
Expand Down Expand Up @@ -1377,7 +1543,7 @@ func (r *resolutionState) loadFileNameFromPackageJSONField(extensions extensions
return &resolved{
path: path,
extension: extension,
resolvedUsingTsExtension: !strings.HasSuffix(packageJSONValue, extension),
resolvedUsingTsExtension: packageJSONValue != "" && !strings.HasSuffix(packageJSONValue, extension),
}
}
return continueSearching()
Expand Down
2 changes: 1 addition & 1 deletion internal/module/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func (p *PackageId) PackageName() string {
type LookupLocations struct {
FailedLookupLocations []string
AffectingLocations []string
ResolutionDiagnostics []ast.Diagnostic
ResolutionDiagnostics []*ast.Diagnostic
}

type ResolvedModule struct {
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import * as me from "#dep";
>me : Symbol(me, Decl(index.ts, 0, 6))

me.thing();
>me.thing : Symbol(thing, Decl(index.ts, 2, 11))
>me : Symbol(me, Decl(index.ts, 0, 6))
>thing : Symbol(thing, Decl(index.ts, 2, 11))

export function thing(): void {}
>thing : Symbol(thing, Decl(index.ts, 2, 11))
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

=== index.ts ===
import * as me from "#dep";
>me : any
>me : typeof me

me.thing();
>me.thing() : any
>me.thing : any
>me : any
>thing : any
>me.thing() : void
>me.thing : () => void
>me : typeof me
>thing : () => void

export function thing(): void {}
>thing : () => void
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,19 @@
-
-
-!!! error TS2209: The project root is ambiguous, but is required to resolve export map entry '.' in file 'package.json'. Supply the `rootDir` compiler option to disambiguate.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is missing because we currently aren't collecting resolution diagnostics (nor any program diagnostic).

+index.ts(1,21): error TS2307: Cannot find module '@this/package' or its corresponding type declarations.
+
+
==== package.json (0 errors) ====
{
"name": "@this/package",
@@= skipped -9, +8 lines =@@
".": "./dist/index.js"
}
}
-==== package.json (0 errors) ====
- {
- "name": "@this/package",
- "type": "module",
- "exports": {
- ".": "./dist/index.js"
- }
- }
-==== index.ts (0 errors) ====
+==== index.ts (1 errors) ====
import * as me from "@this/package";
+ ~~~~~~~~~~~~~~~
+!!! error TS2307: Cannot find module '@this/package' or its corresponding type declarations.
me.thing();
- import * as me from "@this/package";
-
- me.thing();
-
- export function thing(): void {}
-
+<no content>
Loading