mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-26 04:19:33 +08:00
fix(js,ts): link calls through an import whose name contains $ (#2378)
The ES import regex in extractJSImports captured default and namespace
bindings with \w+, which stops at a `$`. So `import items$ from './store'`,
`import $ from './dom'` and `import * as ns$ from './ns'` failed the whole
statement and produced no import mapping, and `items$.getState().inc()`
never reached the store action. The `as` split of named imports, the
`a: b` split of a CommonJS destructuring and the re-export checks had the
same \w-only shape. Some bound a cut-off name instead (`{ a as b$ }` bound
`b`, `{ default as items$ }` bound `items`), and `export { a$ } from` was
dropped from barrel chasing.
Capture every binding with the ASCII identifier class [A-Za-z_$][\w$]*,
keeping the `type`-modifier lookahead: `import type from './x'` still
binds `type`, and `import type $T` / `import type from$` now bind. A
default binding is never followed by `{`, so the `${` of a code
generator's template literal (`import ${x} from '${src}'`, as in qwik's
optimizer) is not taken for a binding.
importShadowedAt and hasParameterBinding bounded the name with \b, which
never fires beside a `$`, so a local `const items$ = ...` or an `items$`
parameter did not shadow the imported store. Named `$` imports already
got those wrong edges on main, and the mapping fix would have extended
them to default and namespace imports. Bound the name with (?<![\w$]) and
(?![\w$]) instead.
Resolution-only: the kernel has no import-mapping mirror. A store
exported by `export { items$ }` (the \b in isExportedLater's clause
branch, left out of #2366) is a separate extraction fix.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5a11cd7ab7
commit
0571286907
@@ -964,7 +964,11 @@ function extractJSImports(content: string): ImportMapping[] {
|
||||
// type-only form, not a default import named `type` — which every such
|
||||
// line used to add, making `type.innerType()` a call on an import.
|
||||
// (`import type from './x'` still binds `type`: backtracking gives it back.)
|
||||
const importRegex = /import\s+(?:type\s+(?=[{*]|(?!from\b)\w))?(?:(\w+)\s*,?\s*)?(?:\{([^}]+)\})?\s*(?:(\*)\s+as\s+(\w+))?\s*from\s*['"]([^'"]+)['"]/g;
|
||||
// A binding is any JS identifier, `$` included: `\w` stops at the `$` of
|
||||
// `import $store from './store'`, failing the whole statement. A default
|
||||
// binding is never followed by `{`, so the `${` of a code generator's
|
||||
// `` `import ${name} from '${src}'` `` is no binding.
|
||||
const importRegex = /import\s+(?:type\s+(?=[{*]|(?!from(?![\w$]))[A-Za-z_$]))?(?:([A-Za-z_$][\w$]*)(?![\w${])\s*,?\s*)?(?:\{([^}]+)\})?\s*(?:(\*)\s+as\s+([A-Za-z_$][\w$]*))?\s*from\s*['"]([^'"]+)['"]/g;
|
||||
|
||||
let match;
|
||||
while ((match = importRegex.exec(content)) !== null) {
|
||||
@@ -984,9 +988,9 @@ function extractJSImports(content: string): ImportMapping[] {
|
||||
// Named imports
|
||||
if (namedImports) {
|
||||
// `{ util, type objectUtil }`: an inline `type` modifier is not part of the name.
|
||||
const names = namedImports.split(',').map((s) => s.trim().replace(/^type\s+(?=\w)/, ''));
|
||||
const names = namedImports.split(',').map((s) => s.trim().replace(/^type\s+(?=[A-Za-z_$])/, ''));
|
||||
for (const name of names) {
|
||||
const aliasMatch = name.match(/(\w+)\s+as\s+(\w+)/);
|
||||
const aliasMatch = name.match(/([A-Za-z_$][\w$]*)\s+as\s+([A-Za-z_$][\w$]*)/);
|
||||
if (aliasMatch) {
|
||||
mappings.push({
|
||||
localName: aliasMatch[2]!,
|
||||
@@ -1038,7 +1042,7 @@ function extractJSImports(content: string): ImportMapping[] {
|
||||
if (destructured) {
|
||||
const names = destructured.split(',').map((s) => s.trim());
|
||||
for (const name of names) {
|
||||
const aliasMatch = name.match(/(\w+)\s*:\s*(\w+)/);
|
||||
const aliasMatch = name.match(/([A-Za-z_$][\w$]*)\s*:\s*([A-Za-z_$][\w$]*)/);
|
||||
if (aliasMatch) {
|
||||
mappings.push({
|
||||
localName: aliasMatch[2]!,
|
||||
@@ -1373,7 +1377,7 @@ export function extractReExports(content: string, language: Language): ReExport[
|
||||
for (const raw of inner.split(',')) {
|
||||
const item = raw.trim();
|
||||
if (!item) continue;
|
||||
const aliasMatch = item.match(/^(\w+)\s+as\s+(\w+)$/);
|
||||
const aliasMatch = item.match(/^([A-Za-z_$][\w$]*)\s+as\s+([A-Za-z_$][\w$]*)$/);
|
||||
if (aliasMatch) {
|
||||
out.push({
|
||||
kind: 'named',
|
||||
@@ -1381,7 +1385,7 @@ export function extractReExports(content: string, language: Language): ReExport[
|
||||
originalName: aliasMatch[1]!,
|
||||
source,
|
||||
});
|
||||
} else if (/^\w+$/.test(item)) {
|
||||
} else if (/^[A-Za-z_$][\w$]*$/.test(item)) {
|
||||
out.push({
|
||||
kind: 'named',
|
||||
exportedName: item,
|
||||
|
||||
@@ -11171,7 +11171,8 @@ function importShadowedAt(name: string, ref: UnresolvedRef, context: ResolutionC
|
||||
return stack;
|
||||
};
|
||||
const scope = stackAt(code.length);
|
||||
const declarations = new RegExp(`\\b(?:const|let|var|function|class)\\s+(?:${escaped}\\b|\\{[^}]*\\b${escaped}\\b)`, 'g');
|
||||
// Not `\b`: there is no word boundary beside the `$` of `items$` or `$store`.
|
||||
const declarations = new RegExp(`\\b(?:const|let|var|function|class)\\s+(?:${escaped}(?![\\w$])|\\{[^}]*(?<![\\w$])${escaped}(?![\\w$]))`, 'g');
|
||||
return [...code.matchAll(declarations)].some(m => stackAt(m.index!).every((p, i) => scope[i] === p));
|
||||
}
|
||||
|
||||
@@ -11179,8 +11180,8 @@ function importShadowedAt(name: string, ref: UnresolvedRef, context: ResolutionC
|
||||
* parentheses must not make the outer shadow invisible. Conservative when a
|
||||
* parameter's type mentions the same name: leave that call unresolved. */
|
||||
function hasParameterBinding(code: string, escapedName: string): boolean {
|
||||
const name = new RegExp(`\\b${escapedName}\\b`);
|
||||
if (new RegExp(`\\b${escapedName}\\s*=>`).test(code)) return true;
|
||||
const name = new RegExp(`(?<![\\w$])${escapedName}(?![\\w$])`);
|
||||
if (new RegExp(`(?<![\\w$])${escapedName}\\s*=>`).test(code)) return true;
|
||||
for (let i = 0; i < code.length; i++) {
|
||||
if (code[i] !== '(' || /\b(?:if|while|for|switch|with)\s*$/.test(code.slice(0, i))) continue;
|
||||
let depth = 1, j = i + 1;
|
||||
|
||||
Reference in New Issue
Block a user