mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-28 22:52:00 +08:00
fix(js,ts): an import spelled with a trailing slash or a ./ segment links its file (#2792)
Issue 2577: `import('@/widgets/Chart/')`, `from 'src/widgets/./Chart'`, a
doubled slash, a `..` after an alias, and a workspace package's
`from '@acme/ui/'` lost the edges the plain spelling gets. The file import
was dropped, the names were matched by name alone, and the lazily loaded
component rendered the importing file's own `lazy(...)` constant.
Root cause: the fallback-alias, workspace, direct-path and own-config
branches of the import resolver join the specifier as written and probe
the result through tryWithExtensions. `fileExists` falls back to the disk,
which accepts any spelling of a file, so the resolver returned
`src/widgets/Chart//index.tsx` or `src/widgets/./Chart/index.tsx`, and every
lookup keyed by the indexed path missed it. The relative and tsconfig-path
branches go through path.resolve and were already fine.
Fix (import-resolver.ts): tryWithExtensions spells each candidate as the
index keys files (indexedSpelling): for the ES module family, a path with a
doubled slash or a `.`/`..` segment is posix-normalized before it is probed
and returned. That is how Node, tsc and the bundlers read a specifier. A
trailing slash still names only a folder, because `Chart/` plus `.tsx` is
no file. Other languages are untouched: a C include's `a/../b.h` follows the
folder on disk, as PR 2610 noted.
Validation:
- __tests__/import-path-spelling.test.ts: 4 of 5 fail on main (the plain
spelling passes). They cover the issue's spellings, a doubled slash, `..`,
a trailing slash beside a same-named file, and a workspace member. All
pass on the kernel and with CODEGRAPH_KERNEL=0. Dropping the
normalization, or its doubled-slash check, fails 4.
- Sync equals a fresh index (CLI) when the folder arrives after its
importer, when the importer is re-spelled, when the folder is deleted
and when the target is edited.
- A/B on 11 TS/JS repos plus 2 controls, both arms with the kernel:
pulse-sms-web (53 imports like '@/utils/') gains 65 edges and corrects
7, including 2 imports that had name-matched to same-named API classes;
react-one gains 11 file imports and corrects 2 bindings. All 85 are right.
The other nine (jvue-admin, vue-invoice-app, vue3-antdv-admin, v-ui,
vue-blog-admin, dreammart, vite, outline, social-app) and the flask and
gin controls are byte-identical.
- Cost: fresh index, median of 3 interleaved: vite 7.59 s main vs 7.49 s,
outline 16.63 s vs 16.94 s.
Fixes #2577
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
e69546e4a3
commit
8651990abd
@@ -1330,14 +1330,38 @@ function resolveAliasedImport(
|
||||
return tryWithExt(importPath);
|
||||
}
|
||||
|
||||
/** The file `basePath` names: with the language's extensions or index files, as is, or as an emitted `.js`. */
|
||||
/**
|
||||
* The file `basePath` names: with the language's extensions or index files, as
|
||||
* is, or as an emitted `.js` — each spelled as the index keys it
|
||||
* (indexedSpelling).
|
||||
*/
|
||||
function tryWithExtensions(basePath: string, language: Language, context: ResolutionContext): string | null {
|
||||
for (const ext of EXTENSION_RESOLUTION[language] || []) {
|
||||
const candidate = basePath + ext;
|
||||
const candidate = indexedSpelling(basePath + ext, language);
|
||||
if (context.fileExists(candidate)) return candidate;
|
||||
}
|
||||
if (context.fileExists(basePath)) return basePath;
|
||||
return findSourceForEmittedSpecifier(basePath, language, context);
|
||||
const asIs = indexedSpelling(basePath, language);
|
||||
if (context.fileExists(asIs)) return asIs;
|
||||
return findSourceForEmittedSpecifier(asIs, language, context);
|
||||
}
|
||||
|
||||
/**
|
||||
* A path as the index keys its files. The alias, workspace and direct-path
|
||||
* branches join an import specifier as written, so `@/widgets/Chart/` made
|
||||
* `src/widgets/Chart//index.tsx` and `src/widgets/./Chart` made
|
||||
* `src/widgets/./Chart/index.tsx`. The disk check accepts either spelling, but
|
||||
* no file node or `files` row is keyed by it, so the import linked nothing
|
||||
* (#2577). An ES module specifier is read lexically — by Node, tsc and the
|
||||
* bundlers alike — so `.` and empty segments drop out and `..` folds, here as
|
||||
* there. A trailing slash still names a folder: `Chart/` gives
|
||||
* `Chart/index.tsx`, never `Chart.tsx`. Other languages keep the path as
|
||||
* found: a C include's `a/../b.h` follows the folder `a` on disk, symlinks
|
||||
* included.
|
||||
*/
|
||||
function indexedSpelling(filePath: string, language: Language): string {
|
||||
return ESM_IMPORT_LANGUAGES.has(language) && /\/\/|(?:^|\/)\.\.?(?:\/|$)/.test(filePath)
|
||||
? path.posix.normalize(filePath)
|
||||
: filePath;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user