mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-18 13:53:01 +08:00
fix(resolution): Expo module bindings, imported receivers, TS type-only imports (#2134)
- Expo Modules: a JS call on a `requireNativeModule('N')` binding — typed
(`<CameraNativeModule>`) or not, local or imported (expo-camera's
`import CameraManager from './ExpoCameraManager'`) — resolves to module
N's declared function, Swift first and Kotlin beside it, else to the
member on the binding's declared type. It went to the same-named static
method making the call.
- A JS/TS member call on an import binding never picks a method declared in
the calling file, and ruling those out never manufactures a unique guess.
- `import type { X }` was parsed as a default import named `type` (every
type-only import bound `type`), and `{ a, type B }` bound `type B`.
- A name the file binds from an out-of-repo package names nothing in the
project, for every reference kind, and JS/TS fuzzy matching is exact-case:
trpc's 402 `Record<…>` references went to a `record` property, element-plus's
121 `mount` imports from @vue/test-utils to a test helper.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,9 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Calls into an Expo module now reach the module's native functions even when the JavaScript names the module something else, like expo-camera's `CameraManager` for `requireNativeModule('ExpoCamera')`. Both the iOS and the Android implementations are linked. expo-camera's calls used to link back to the same-named method making the call, so a method looked like it called itself.
|
||||
- A JavaScript or TypeScript call on something the file imports, like `fireEvent.click(…)` or `util.omit(…)`, no longer links to a same-named method declared in the calling file itself.
|
||||
- TypeScript's `import type { X }` no longer reads as an import of something named `type`, and `import { a, type B }` binds `B`, not `type B`. So a type-only import now links to the module it names, where it used to link to whatever project symbol had that name. A type imported from a package, like `RsbuildConfig` from `@rsbuild/core`, no longer links to a project symbol, and JavaScript and TypeScript names no longer match a symbol whose name differs only in case (`Mock` is not `mock`).
|
||||
- Angular apps split across Nx libraries, like angular-spotify, now have their routes and navigation. A lazily loaded route written `async () => (await import('@app/home')).HomeModule` is followed through the library's `index.ts` to the module it re-exports, and a `routerLink` in one library now reaches a screen declared in another. Route paths built from a class constant (`RouterUtil.Configuration.Lyrics`, `` `issue/:${ProjectConst.IssueId}` ``) or an enum are now read. A redirect in a routes file that only lazy-loads others now counts, and `router.navigate(['project', 'issue', id])` without `relativeTo` is read from the root, as Angular does. Re-index Angular projects after upgrading.
|
||||
- React Router apps now show navigation written as React Router v5's `<Redirect to="/login" />` and through a styled link, like react-boilerplate's `HeaderLink = styled(Link)` used as `<HeaderLink to="/features">`. Apps built this way had routes but no links between their screens.
|
||||
- SvelteKit routes are now named by the address a browser asks for. A `(group)` folder like `(app)` or `(marketing)` is no longer part of a route's path, and a parameter with a matcher, like `[id=integer]`, is just `:id`. So `goto('/blocks')` and `<a href="/blocks">` now reach a page that lives in `src/routes/(app)/blocks/`, where before they reached nothing and the app's screens showed no navigation. Re-index SvelteKit projects after upgrading.
|
||||
|
||||
@@ -363,7 +363,7 @@ Real iOS and React Native codebases live across multiple languages — a Swift c
|
||||
| **React Native legacy bridge** | JS `NativeModules.X.fn(...)` | ObjC `RCT_EXPORT_METHOD` / `RCT_REMAP_METHOD` · Java/Kotlin `@ReactMethod` | Parses macro/annotation declarations to build a JS-name → native-method map |
|
||||
| **React Native TurboModules** | JS `import M from './NativeM'; M.fn(...)` | Native impl matching the Codegen spec | Treats the `Native<X>.ts` spec interface as ground truth |
|
||||
| **RN native → JS events** | JS `new NativeEventEmitter(...).addListener('e', cb)` | ObjC `[self sendEventWithName:@"e" body:...]` · Swift `sendEvent(withName: "e", ...)` · Java/Kotlin `.emit("e", ...)` | Synthesized cross-language event channel keyed by literal event name |
|
||||
| **Expo Modules** | JS `requireNativeModule('X').fn(...)` | Swift / Kotlin `Module { Name("X"); AsyncFunction("fn") { ... } }` | Parses the Expo DSL literals; synthetic method nodes resolve via existing name-match |
|
||||
| **Expo Modules** | JS `requireNativeModule('X').fn(...)`, directly or through a binding (`export default requireNativeModule<T>('X')`) | Swift / Kotlin `Module { Name("X"); AsyncFunction("fn") { ... } }` | Parses the Expo DSL literals into method nodes; a call on a binding resolves to module `X`'s `fn` on both platforms, else to the method on the binding's declared type |
|
||||
| **Fabric view components** | JSX `<MyView prop={v}/>` | TS Codegen spec + native impl class | Spec → `component` node; convention-based name+suffix lookup (`View`/`ComponentView`/`Manager`/`ViewManager`) bridges to native |
|
||||
| **Legacy Paper view managers** | JSX `<MyView prop={v}/>` | ObjC `RCT_EXPORT_VIEW_PROPERTY` · Java/Kotlin `@ReactProp` | Same as Fabric — Paper-era declarations also produce `component` + `property` nodes |
|
||||
|
||||
|
||||
@@ -205,3 +205,80 @@ class BatteryModule : Module() {
|
||||
expect(pair.c).toBeGreaterThanOrEqual(2); // swift->kotlin AND kotlin->swift
|
||||
});
|
||||
});
|
||||
|
||||
describe('Expo Modules — a JS binding named for its role, not its module', () => {
|
||||
let dir: string;
|
||||
beforeEach(() => {
|
||||
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'expo-modules-binding-'));
|
||||
});
|
||||
afterEach(() => {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('expo-camera: `CameraManager.fn()` on `export default requireNativeModule<T>("ExpoCamera")` reaches the module’s functions', async () => {
|
||||
const write = (rel: string, content: string) => {
|
||||
fs.mkdirSync(path.dirname(path.join(dir, rel)), { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, rel), content);
|
||||
};
|
||||
write('package.json', '{"dependencies":{"expo-modules-core":"^1.0.0","expo":"*"}}');
|
||||
write('ios/CameraViewModule.swift', `import ExpoModulesCore
|
||||
public final class CameraViewModule: Module {
|
||||
public func definition() -> ModuleDefinition {
|
||||
Name("ExpoCamera")
|
||||
AsyncFunction("launchScanner") { (options: ScannerOptions) in }
|
||||
AsyncFunction("dismissScanner") { }
|
||||
}
|
||||
}
|
||||
`);
|
||||
write('android/src/main/java/expo/modules/camera/CameraViewModule.kt', `package expo.modules.camera
|
||||
class CameraViewModule : Module() {
|
||||
override fun definition() = ModuleDefinition {
|
||||
Name("ExpoCamera")
|
||||
AsyncFunction("launchScanner") { options: ScannerOptions -> }
|
||||
AsyncFunction("dismissScanner") { }
|
||||
}
|
||||
}
|
||||
`);
|
||||
write('src/Camera.types.ts', `import { NativeModule } from 'expo';
|
||||
export declare class CameraNativeModule extends NativeModule {
|
||||
readonly isAvailableAsync: () => Promise<boolean>;
|
||||
}
|
||||
`);
|
||||
write('src/ExpoCameraManager.ts', `import { requireNativeModule } from 'expo';
|
||||
import type { CameraNativeModule } from './Camera.types';
|
||||
|
||||
export default requireNativeModule<CameraNativeModule>('ExpoCamera');
|
||||
`);
|
||||
write('src/CameraView.tsx', `import CameraManager from './ExpoCameraManager';
|
||||
|
||||
export default class CameraView {
|
||||
static async isAvailableAsync(): Promise<boolean> {
|
||||
return CameraManager.isAvailableAsync();
|
||||
}
|
||||
static async launchScanner(options: object): Promise<void> {
|
||||
await CameraManager.launchScanner(options);
|
||||
}
|
||||
}
|
||||
`);
|
||||
const cg = await CodeGraph.init(dir, { index: true });
|
||||
try {
|
||||
const view = cg.getNodesInFile('src/CameraView.tsx');
|
||||
const from = (name: string) => view.find((n) => n.name === name && n.kind === 'method')!;
|
||||
const callees = (name: string) =>
|
||||
cg
|
||||
.getOutgoingEdges(from(name).id)
|
||||
.filter((e) => e.kind === 'calls')
|
||||
.map((e) => cg.getNode(e.target)!)
|
||||
.map((n) => `${n.language}:${n.qualifiedName.split('::').pop()}`)
|
||||
.sort();
|
||||
// Both platforms' `launchScanner`, not CameraView's own.
|
||||
expect(callees('launchScanner')).toEqual(['kotlin:ExpoCamera.launchScanner', 'swift:ExpoCamera.launchScanner']);
|
||||
// Declared natively nowhere here: the type the binding is given, never the caller itself.
|
||||
expect(callees('isAvailableAsync')).toEqual(['typescript:isAvailableAsync']);
|
||||
const target = cg.getOutgoingEdges(from('isAvailableAsync').id).find((e) => e.kind === 'calls')!;
|
||||
expect(cg.getNode(target.target)!.filePath).toBe('src/Camera.types.ts');
|
||||
} finally {
|
||||
cg.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* TypeScript's `type` modifiers are not bindings.
|
||||
*
|
||||
* `import type { X } from './x'` used to read as a default import named
|
||||
* `type` beside `X` — every type-only import line bound `type` — so zod's
|
||||
* `type.innerType()` (a parameter named `type`) was taken for a call on an
|
||||
* import. An inline `{ util, type objectUtil }` named its binding
|
||||
* `type objectUtil`.
|
||||
*/
|
||||
import { describe, it, expect, afterAll } from 'vitest';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { CodeGraph } from '../src';
|
||||
import { extractImportMappings } from '../src/resolution/import-resolver';
|
||||
|
||||
const bindings = (source: string) =>
|
||||
extractImportMappings('src/a.ts', source, 'typescript').map((m) => `${m.localName}<${m.exportedName}${m.isNamespace ? ' ns' : ''}`);
|
||||
|
||||
describe('import mappings and TypeScript type modifiers', () => {
|
||||
it('a type-only import binds its names, never `type`', () => {
|
||||
expect(bindings(`import type { enumUtil } from './helpers/enumUtil.js';\n`)).toEqual(['enumUtil<enumUtil']);
|
||||
expect(bindings(`import type * as z from './z';\n`)).toEqual(['z<* ns']);
|
||||
expect(bindings(`import type Foo from './foo';\n`)).toEqual(['Foo<default']);
|
||||
});
|
||||
|
||||
it('an inline `type` modifier is not part of the name', () => {
|
||||
expect(bindings(`import { util, type objectUtil } from './helpers/util.js';\n`)).toEqual(['util<util', 'objectUtil<objectUtil']);
|
||||
expect(bindings(`import { type A as B } from './ab';\n`)).toEqual(['B<A']);
|
||||
});
|
||||
|
||||
it('a default import that is named `type` is still one', () => {
|
||||
expect(bindings(`import type from './type';\n`)).toEqual(['type<default']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('a name imported from a package names nothing in the project', () => {
|
||||
const roots: string[] = [];
|
||||
afterAll(() => {
|
||||
for (const r of roots.splice(0)) fs.rmSync(r, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('halo: `type RsbuildConfig` and `type Command` are not a local `rsbuildConfig` or `command`', async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-import-type-'));
|
||||
roots.push(root);
|
||||
const files: Record<string, string> = {
|
||||
'package.json': JSON.stringify({ name: 'console', dependencies: { '@rsbuild/core': '*', '@tiptap/core': '*' } }),
|
||||
'src/rsbuild.ts': `import { defineConfig, type RsbuildConfig } from '@rsbuild/core';
|
||||
export function rsbuildConfig(): RsbuildConfig { return defineConfig({}); }
|
||||
`,
|
||||
'src/menu.ts': `export function command() { return 1; }
|
||||
`,
|
||||
'src/gap.ts': `import type { Command } from '@tiptap/core';
|
||||
export function gapCursor(): Command { return () => true; }
|
||||
`,
|
||||
};
|
||||
for (const [rel, content] of Object.entries(files)) {
|
||||
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
|
||||
fs.writeFileSync(path.join(root, rel), content);
|
||||
}
|
||||
const cg = await CodeGraph.init(root, { index: true });
|
||||
try {
|
||||
const targets = (file: string) =>
|
||||
cg
|
||||
.getOutgoingEdgesFrom(cg.getNodesInFile(file).map((n) => n.id), ['references', 'imports', 'type_of', 'returns'])
|
||||
.map((e) => cg.getNode(e.target)!)
|
||||
.filter((n) => n.kind !== 'file' && n.kind !== 'import')
|
||||
.map((n) => n.name);
|
||||
expect(targets('src/rsbuild.ts')).not.toContain('rsbuildConfig');
|
||||
expect(targets('src/gap.ts')).not.toContain('command');
|
||||
} finally {
|
||||
cg.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -42,7 +42,12 @@ import type { Node } from '../../types';
|
||||
import {
|
||||
FrameworkExtractionResult,
|
||||
FrameworkResolver,
|
||||
ResolutionContext,
|
||||
ResolvedRef,
|
||||
UnresolvedRef,
|
||||
} from '../types';
|
||||
import { resolveImportPath } from '../import-resolver';
|
||||
import { stripCommentsForRegex } from '../strip-comments';
|
||||
|
||||
/**
|
||||
* Match `Function("name")`, `AsyncFunction("name")`, or `Property("name")`
|
||||
@@ -196,3 +201,103 @@ export const expoModulesResolver: FrameworkResolver = {
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// The JS side: `ExpoCamera.scanFromURLAsync(…)` → the module's function
|
||||
// =============================================================================
|
||||
|
||||
/** `requireNativeModule('ExpoCamera')`, typed or not, optional or not. */
|
||||
const REQUIRE_NATIVE = /\brequire(?:Optional)?NativeModule\s*(?:<\s*([A-Za-z_$][\w$]*)[^>]*>)?\s*\(\s*['"]([A-Za-z_$][\w$]*)['"]\s*\)/;
|
||||
|
||||
interface NativeBinding {
|
||||
/** The `Name("…")` the module declares. */
|
||||
module: string;
|
||||
/** The declared JS type, from `requireNativeModule<CameraNativeModule>(…)`. */
|
||||
type: string | null;
|
||||
/** The file the binding (and its type) is written in. */
|
||||
file: string;
|
||||
}
|
||||
|
||||
const bindingMemo = new WeakMap<ResolutionContext, Map<string, NativeBinding | null>>();
|
||||
|
||||
/**
|
||||
* What `name` is bound to in `file`: `const X = requireNativeModule('N')`
|
||||
* here, or the import of a module whose default export (or a named export)
|
||||
* is one — expo-camera's `import CameraManager from './ExpoCameraManager'`,
|
||||
* whose whole body is `export default requireNativeModule<CameraNativeModule>('ExpoCamera')`.
|
||||
*/
|
||||
function nativeBinding(name: string, file: string, context: ResolutionContext): NativeBinding | null {
|
||||
let memo = bindingMemo.get(context);
|
||||
if (!memo) bindingMemo.set(context, (memo = new Map()));
|
||||
const key = `${file}\0${name}`;
|
||||
if (memo.has(key)) return memo.get(key)!;
|
||||
let found: NativeBinding | null = null;
|
||||
const own = context.readFile(file);
|
||||
if (own && own.includes('NativeModule')) {
|
||||
const local = new RegExp(String.raw`\b(?:const|let|var)\s+${name}\s*(?::[^=]+)?=\s*` + REQUIRE_NATIVE.source).exec(stripCommentsForRegex(own, 'typescript'));
|
||||
if (local) found = { type: local[1] ?? null, module: local[2]!, file };
|
||||
}
|
||||
if (!found) {
|
||||
const language = /\.tsx$/.test(file) ? 'tsx' : /\.[cm]?ts$/.test(file) ? 'typescript' : /\.jsx$/.test(file) ? 'jsx' : 'javascript';
|
||||
const binding = context.getImportMappings(file, language).find((m) => m.localName === name);
|
||||
const target = binding && binding.source.startsWith('.') ? resolveImportPath(binding.source, file, language, context) : null;
|
||||
const content = target ? context.readFile(target) : null;
|
||||
if (target && content && content.includes('NativeModule')) {
|
||||
const safe = stripCommentsForRegex(content, 'typescript');
|
||||
const pattern = binding!.isDefault
|
||||
? new RegExp(String.raw`\bexport\s+default\s+` + REQUIRE_NATIVE.source)
|
||||
: new RegExp(String.raw`\bexport\s+(?:const|let)\s+${binding!.exportedName}\s*(?::[^=]+)?=\s*` + REQUIRE_NATIVE.source);
|
||||
const m = pattern.exec(safe);
|
||||
if (m) found = { type: m[1] ?? null, module: m[2]!, file: target };
|
||||
}
|
||||
}
|
||||
memo.set(key, found);
|
||||
return found;
|
||||
}
|
||||
|
||||
/**
|
||||
* JS calls into an Expo module, bound by the module's NAME, not by the
|
||||
* method name alone: expo-camera calls its module `CameraManager`, so
|
||||
* `CameraManager.isAvailableAsync()` went to `CameraView`'s own static
|
||||
* `isAvailableAsync` in the same file. The native functions the module
|
||||
* declares are the targets — Swift first, Kotlin beside it — and when it
|
||||
* declares none of that name, the method on the type the binding is given
|
||||
* (`requireNativeModule<CameraNativeModule>`).
|
||||
*/
|
||||
export const expoModulesJsResolver: FrameworkResolver = {
|
||||
name: 'expo-modules-js',
|
||||
languages: ['typescript', 'tsx', 'javascript', 'jsx'],
|
||||
|
||||
detect(context) {
|
||||
return expoModulesResolver.detect(context);
|
||||
},
|
||||
|
||||
resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
|
||||
if (ref.referenceKind !== 'calls') return null;
|
||||
const m = /^([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)$/.exec(ref.referenceName);
|
||||
if (!m) return null;
|
||||
const binding = nativeBinding(m[1]!, ref.filePath, context);
|
||||
if (!binding) return null;
|
||||
const fn = m[2]!;
|
||||
const declared = context
|
||||
.getNodesByName(fn)
|
||||
.filter((n) => n.id.startsWith('expo-module:') && n.qualifiedName.endsWith(`::${binding.module}.${fn}`))
|
||||
.sort((a, b) => (a.language === 'swift' ? 0 : 1) - (b.language === 'swift' ? 0 : 1));
|
||||
if (declared.length > 0) {
|
||||
return {
|
||||
original: ref,
|
||||
targetNodeId: declared[0]!.id,
|
||||
...(declared.length > 1 ? { alsoTargets: declared.slice(1).map((n) => ({ targetNodeId: n.id })) } : {}),
|
||||
confidence: 0.95,
|
||||
resolvedBy: 'framework',
|
||||
};
|
||||
}
|
||||
if (binding.type) {
|
||||
const typed = context
|
||||
.getNodesByName(fn)
|
||||
.find((n) => (n.kind === 'method' || n.kind === 'property' || n.kind === 'field') && n.qualifiedName.endsWith(`${binding.type}::${fn}`));
|
||||
if (typed) return { original: ref, targetNodeId: typed.id, confidence: 0.9, resolvedBy: 'framework' };
|
||||
}
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -31,7 +31,7 @@ import { aspnetResolver } from './csharp';
|
||||
import { swiftUIResolver, uikitResolver, vaporResolver } from './swift';
|
||||
import { swiftObjcBridgeResolver } from './swift-objc';
|
||||
import { reactNativeBridgeResolver } from './react-native';
|
||||
import { expoModulesResolver } from './expo-modules';
|
||||
import { expoModulesResolver, expoModulesJsResolver } from './expo-modules';
|
||||
import { expoRouterResolver } from './expo-router';
|
||||
import { fabricViewResolver } from './fabric';
|
||||
import { cicsResolver } from './cics';
|
||||
@@ -90,6 +90,8 @@ const FRAMEWORK_RESOLVERS: FrameworkResolver[] = [
|
||||
reactNativeBridgeResolver,
|
||||
// Expo Modules — Function/AsyncFunction/Property DSL on Swift/Kotlin
|
||||
expoModulesResolver,
|
||||
// Expo Modules, JS side — `M.fn()` on a `requireNativeModule('N')` binding → module N's `fn`
|
||||
expoModulesJsResolver,
|
||||
// Expo Router — `app/` screen files → route nodes; `router.push('/x')` → navigates edges
|
||||
expoRouterResolver,
|
||||
// React Native Fabric / Codegen view components — TS spec → component nodes
|
||||
@@ -177,6 +179,6 @@ export { aspnetResolver } from './csharp';
|
||||
export { swiftUIResolver, uikitResolver, vaporResolver } from './swift';
|
||||
export { swiftObjcBridgeResolver } from './swift-objc';
|
||||
export { reactNativeBridgeResolver } from './react-native';
|
||||
export { expoModulesResolver } from './expo-modules';
|
||||
export { expoModulesResolver, expoModulesJsResolver } from './expo-modules';
|
||||
export { expoRouterResolver } from './expo-router';
|
||||
export { fabricViewResolver } from './fabric';
|
||||
|
||||
@@ -933,8 +933,11 @@ export function extractImportMappings(
|
||||
function extractJSImports(content: string): ImportMapping[] {
|
||||
const mappings: ImportMapping[] = [];
|
||||
|
||||
// ES6 imports
|
||||
const importRegex = /import\s+(?:(\w+)\s*,?\s*)?(?:\{([^}]+)\})?\s*(?:(\*)\s+as\s+(\w+))?\s*from\s*['"]([^'"]+)['"]/g;
|
||||
// ES6 imports. `import type { X }` / `import type * as ns` is TypeScript's
|
||||
// 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;
|
||||
|
||||
let match;
|
||||
while ((match = importRegex.exec(content)) !== null) {
|
||||
@@ -953,7 +956,8 @@ function extractJSImports(content: string): ImportMapping[] {
|
||||
|
||||
// Named imports
|
||||
if (namedImports) {
|
||||
const names = namedImports.split(',').map((s) => s.trim());
|
||||
// `{ 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)/, ''));
|
||||
for (const name of names) {
|
||||
const aliasMatch = name.match(/(\w+)\s+as\s+(\w+)/);
|
||||
if (aliasMatch) {
|
||||
|
||||
@@ -1509,6 +1509,12 @@ export function matchByExactName(
|
||||
const bareJs = isBareJsCall(ref, context);
|
||||
const bareGo = isBareGoCall(ref, context);
|
||||
const barePhp = isBarePhpCall(ref, context);
|
||||
// A type, a value or an import the file binds from a package outside the
|
||||
// repository names nothing in it, whatever kind of reference it is.
|
||||
if (!bareJs && JS_FAMILY.has(ref.language) && ref.referenceKind !== 'calls' &&
|
||||
/^[A-Za-z_$][\w$]*$/.test(ref.referenceName) && isOutOfRepoBinding(ref.referenceName, ref, context)) {
|
||||
return null;
|
||||
}
|
||||
if (bareJs) {
|
||||
const storeAction = matchJsStoreBindingCall(ref, context);
|
||||
if (storeAction) return storeAction;
|
||||
@@ -3598,10 +3604,22 @@ export function matchMethodCall(
|
||||
|
||||
// Filter to same-language candidates first
|
||||
const sameLanguageMethods = methods.filter(m => m.language === ref.language);
|
||||
const targetMethods = sameLanguageMethods.length > 0 ? sameLanguageMethods : methods;
|
||||
let targetMethods = sameLanguageMethods.length > 0 ? sameLanguageMethods : methods;
|
||||
// A receiver the file imports is another module's value: never a method
|
||||
// declared in the calling file. expo-camera's `CameraManager.isAvailableAsync()`
|
||||
// (`import CameraManager from './ExpoCameraManager'`) went to `CameraView`'s
|
||||
// own static `isAvailableAsync` — the method making the call.
|
||||
// Ruling the caller's file out may reject a guess; it must never
|
||||
// manufacture one — the one method left is then no likelier than before.
|
||||
let narrowed = false;
|
||||
if (JS_FAMILY.has(ref.language) && isImportBinding(objectOrClass!, ref, context)) {
|
||||
const kept = targetMethods.filter((m) => m.filePath !== ref.filePath);
|
||||
narrowed = kept.length !== targetMethods.length;
|
||||
targetMethods = kept;
|
||||
}
|
||||
|
||||
// If only one same-language method with this name exists, use it
|
||||
if (targetMethods.length === 1 && targetMethods[0]!.language === ref.language) {
|
||||
if (targetMethods.length === 1 && !narrowed && targetMethods[0]!.language === ref.language) {
|
||||
return {
|
||||
original: ref,
|
||||
targetNodeId: targetMethods[0]!.id,
|
||||
@@ -3660,6 +3678,12 @@ function isImportedModuleReceiver(receiver: string, ref: UnresolvedRef, context:
|
||||
return binding.isNamespace || context.isOutOfRepoImport?.(binding.source, ref.filePath, ref.language) === true;
|
||||
}
|
||||
|
||||
/** Is the root of a member call's receiver (`CameraManager` in `CameraManager.x`) one of the file's imports? */
|
||||
function isImportBinding(receiver: string, ref: UnresolvedRef, context: ResolutionContext): boolean {
|
||||
const root = receiver.split('.')[0]!;
|
||||
return context.getImportMappings?.(ref.filePath, ref.language)?.some((m) => m.localName === root) === true;
|
||||
}
|
||||
|
||||
/** Does the file bind `name` by importing it from outside the repository? */
|
||||
function isOutOfRepoBinding(name: string, ref: UnresolvedRef, context: ResolutionContext): boolean {
|
||||
const binding = context.getImportMappings?.(ref.filePath, ref.language)?.find((m) => m.localName === name);
|
||||
@@ -4558,6 +4582,9 @@ export function matchFuzzy(
|
||||
const callableCandidates = candidates.filter((n) => callableKinds.has(n.kind) && !(typeRef && !canNameInTypePosition(n)) &&
|
||||
!(rustBare && (n.name !== ref.referenceName || !isRustNameInScope(n, ref, context))) &&
|
||||
!(ref.language === 'python' && n.name !== ref.referenceName) &&
|
||||
// JS and TS too: halo's `type RsbuildConfig` (imported from @rsbuild/core)
|
||||
// is not its local `rsbuildConfig`, kit's vitest `Mock` not a `mock`.
|
||||
!(JS_FAMILY.has(ref.language) && n.name !== ref.referenceName) &&
|
||||
!(pythonShape && !fitsPythonCallShape(n, pythonShape, ref, context)) &&
|
||||
!(javaBare && n.kind === 'method' && !isJavaMethodInScope(n, ref, context)))
|
||||
.filter((n) => (ref.referenceKind !== 'references' && ref.referenceKind !== 'function_ref') ||
|
||||
@@ -4587,8 +4614,9 @@ export function matchFuzzy(
|
||||
isVisibleAcrossFiles(finalCandidates[0]!, ref, context) &&
|
||||
isCrossFileReachable(finalCandidates[0]!, ref, context) &&
|
||||
!(isBareJsCall(ref, context) &&
|
||||
(TYPE_MEMBER_KINDS.has(finalCandidates[0]!.kind) || isOutOfRepoBinding(ref.referenceName, ref, context) ||
|
||||
(TYPE_MEMBER_KINDS.has(finalCandidates[0]!.kind) ||
|
||||
(finalCandidates[0]!.filePath !== ref.filePath && isLocallyBoundJsName(ref.referenceName, ref.filePath, context)))) &&
|
||||
!(JS_FAMILY.has(ref.language) && isOutOfRepoBinding(ref.referenceName, ref, context)) &&
|
||||
!(finalCandidates[0]!.kind === 'method' && isBareGoCall(ref, context)) &&
|
||||
// A bare PHP call is a function call (case-insensitive, so fuzzy may find
|
||||
// one) — never the class `View` for `view(…)`, never a method.
|
||||
|
||||
Reference in New Issue
Block a user