Fix auto-import lowercasing PascalCase default-import bindings on case-insensitive file systems#3952
Conversation
…e-insensitive file systems Use the original-case moduleFileName instead of the lowercased moduleID (from file.Path()) when deriving a fallback identifier name for default/export-equals exports. On case-insensitive file systems, Path() lowercases the entire path, so using it to derive an identifier from the filename loses internal capitals (e.g., "ChargerHeader" becomes "chargerheader", then "Chargerheader" after JSX capitalization). Agent-Logs-Url: https://github.com/microsoft/typescript-go/sessions/906fc5d5-16e0-4a35-b14f-4b160557b961 Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com>
Agent-Logs-Url: https://github.com/microsoft/typescript-go/sessions/906fc5d5-16e0-4a35-b14f-4b160557b961 Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes auto-import fallback names for default-like exports so PascalCase file names are preserved on case-insensitive file systems, fitting into the language service auto-import extraction path.
Changes:
- Uses original-cased file names instead of lowercased module paths when deriving fallback local names.
- Adds fourslash coverage for PascalCase default imports across named, anonymous, alias, and case-insensitive scenarios.
- Adds reference baselines for the new auto-import tests.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
internal/ls/autoimport/extract.go |
Updates fallback local-name derivation for default-like exports. |
internal/fourslash/tests/autoImportDefaultPascalCase_test.go |
Adds fourslash auto-import tests for PascalCase default bindings. |
testdata/baselines/reference/fourslash/autoImports/autoImportDefaultPascalCase.baseline.md |
Adds baseline for named default PascalCase auto-import. |
testdata/baselines/reference/fourslash/autoImports/autoImportDefaultPascalCaseAnonymous.baseline.md |
Adds baseline for anonymous default PascalCase auto-import. |
testdata/baselines/reference/fourslash/autoImports/autoImportDefaultPascalCaseCaseInsensitive.baseline.md |
Adds baseline for named default on case-insensitive file systems. |
testdata/baselines/reference/fourslash/autoImports/autoImportDefaultPascalCaseAnonymousCaseInsensitive.baseline.md |
Adds baseline for anonymous default on case-insensitive file systems. |
testdata/baselines/reference/fourslash/autoImports/autoImportDefaultPascalCaseAliasCaseInsensitive.baseline.md |
Adds baseline for aliased default on case-insensitive file systems. |
…ive FS
This test exercises the targetSymbol file-name fallback branch (lines 340-347
in extract.go) by using `export { default } from "./ChargerHeader"` where the
source file has an anonymous default export.
Agent-Logs-Url: https://github.com/microsoft/typescript-go/sessions/f6824fed-6f67-4b85-b0ef-a0512ad6611a
Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com>
| targetFileName := "" | ||
| if len(targetSymbol.Declarations) > 0 { | ||
| targetFileName = ast.GetSourceFileOfNode(targetSymbol.Declarations[0]).FileName() | ||
| } | ||
| if targetFileName == "" { | ||
| targetFileName = moduleFileName | ||
| } | ||
| if targetFileName != "" { | ||
| export.localName = lsutil.ModuleSpecifierToValidIdentifier(targetFileName, false) |
There was a problem hiding this comment.
@copilot this code is really obtuse. I also wonder if this same problem has already been solved elsewhere, e.g. the modulespeciifers package or something. Even elsewhere in this file or the LS
There was a problem hiding this comment.
Simplified in 8d290bf. Extracted a fileNameForDefaultExportName helper that encapsulates the priority logic (target symbol's source file → module file name → moduleID). The main flow now has a single fallback call at the end instead of deeply nested conditionals. I checked modulespecifiers and completions — they don't have this exact pattern since they work with module symbols (via ModuleSymbolToValidIdentifier) rather than the raw file path fallback for anonymous defaults.
Extract fileNameForDefaultExportName() to consolidate the repeated conditional logic for choosing a file name to derive fallback identifiers. This makes the code clearer and removes deep nesting. Agent-Logs-Url: https://github.com/microsoft/typescript-go/sessions/1503940b-1479-45c6-b5a1-b491d287f2c6 Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com>
| if moduleFileName != "" { | ||
| return moduleFileName | ||
| } | ||
| return string(moduleID) |
There was a problem hiding this comment.
@copilot Does this ever actually happen? Can you temporarily panic here and see what happens? (Don't commit it, just tell me how it goes)
There was a problem hiding this comment.
I added a panic and ran all fourslash tests plus the internal/ls/... tests — it never triggers. The only call site that passes moduleFileName="" is line 118 (extractFromModuleDeclaration for ambient modules like declare module "foo"), but in that scenario targetSymbol.Declarations[0] always has a valid source file, so it returns from line 453 before reaching line 460. In practice this is dead code, but it's there as a safety net for the ambient module edge case. Happy to remove it if you'd prefer.
moduleID(fromfile.Path()) is lowercased on case-insensitive file systems, losing PascalCase capitals in the fallback pathmoduleFileName(preserves original casing) in fallback paths