Skip to content
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
44 changes: 44 additions & 0 deletions __tests__/resolution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1359,6 +1359,50 @@ func main() {
expect(legacyCallers.some((c) => c.node.filePath === 'src/main.ts')).toBe(false);
});

it('does not match a wildcard alias when the import has no segment for `*` (prefix/suffix overlap)', async () => {
// Pattern `@x/*/index` -> prefix `@x/`, suffix `/index`. An import of
// `@x/index` has nothing in the `*` slot, so it must NOT resolve via
// this alias. Previously startsWith(prefix) && endsWith(suffix) both
// passed for the single shared `/`, producing a bogus mapped target.
fs.mkdirSync(path.join(tempDir, 'src/widgets/button'), { recursive: true });
// The file the BOGUS rewrite would point at (`widgets/index`). If the
// alias wrongly matches `@x/index`, the call would attach here.
fs.writeFileSync(
path.join(tempDir, 'src/widgets/index.ts'),
`export function trap(): number { return 0; }\n`
);
// A real relative-resolvable target for the bare import.
fs.writeFileSync(
path.join(tempDir, 'src/index.ts'),
`export function trap(): number { return 1; }\n`
);
fs.writeFileSync(
path.join(tempDir, 'src/main.ts'),
`import { trap } from '@x/index';\nexport function go(): number { return trap(); }\n`
);
fs.writeFileSync(
path.join(tempDir, 'tsconfig.json'),
JSON.stringify({
compilerOptions: {
baseUrl: './src',
paths: { '@x/*/index': ['widgets/*/index'] },
},
})
);

cg = await CodeGraph.init(tempDir, { index: true });
cg.resolveReferences();

// The widgets/index.ts `trap` must NOT receive a caller from main.ts
// via the overlapping-alias false match.
const widgetsTrap = cg
.getNodesByKind('function')
.find((n) => n.name === 'trap' && n.filePath === 'src/widgets/index.ts');
expect(widgetsTrap).toBeDefined();
const bogusCallers = cg.getCallers(widgetsTrap!.id);
expect(bogusCallers.some((c) => c.node.filePath === 'src/main.ts')).toBe(false);
});

it('falls back gracefully when tsconfig is absent', async () => {
fs.mkdirSync(path.join(tempDir, 'src'), { recursive: true });
fs.writeFileSync(
Expand Down
8 changes: 8 additions & 0 deletions src/resolution/path-aliases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,14 @@ export function applyAliases(

let captured = '';
if (pat.hasWildcard) {
// The prefix and suffix must occupy DISJOINT regions of the import
// path. When they overlap (the import is shorter than prefix+suffix),
// `startsWith` and `endsWith` can both be satisfied by the same
// characters — e.g. pattern `src/*/index` (prefix `src/`, suffix
// `/index`) would otherwise falsely match `src/index`, whose single
// `/` is counted as both the prefix's and the suffix's slash. Such an
// import has no wildcard segment, so it must not match.
if (importPath.length < pat.prefix.length + pat.suffix.length) continue;
captured = importPath.slice(pat.prefix.length, importPath.length - pat.suffix.length);
} else if (importPath !== pat.prefix) {
// Literal pattern must match exactly.
Expand Down