fix(resolution): leave built-in Map calls unresolved (#1566) (#1790)

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
This commit is contained in:
Colby Mchenry
2026-09-08 14:04:43 -05:00
committed by GitHub
co-authored by Colby McHenry
parent 72c1ff13cc
commit de5adba7ea
9 changed files with 151 additions and 59 deletions
+13 -30
View File
@@ -407,34 +407,21 @@ const LITERAL_RECEIVER_TYPES = new Set([
*/
const TS_JS_CHAIN_LANGUAGES = new Set(['typescript', 'tsx', 'javascript', 'jsx']);
/**
* Host objects a TS/JS project never declares: the browser, extension, and
* runtime namespaces, plus the builtin constructors whose statics are library
* calls. A member chain ROOTED at one of these ends in a platform API, so the
* bare method name the extractor used to emit for `chrome.storage.local.get(k)`
* or `document.body.querySelector(s)` could only ever exact-match an unrelated
* project symbol that happened to share the name (#1707). `window` is absent on
* purpose: `window.MyNamespace.doThing()` reaches a project symbol.
*/
const TS_JS_HOST_GLOBAL_ROOTS = new Set([
'chrome', 'browser', 'document', 'navigator', 'performance', 'console',
'localStorage', 'sessionStorage', 'indexedDB', 'crypto', 'globalThis',
'process', 'Math', 'JSON', 'Object', 'Array', 'Reflect', 'Promise', 'Intl',
]);
/** Receiver node types (TS/JS grammars) that continue a member chain downward. */
const TS_JS_CHAIN_RECEIVER_TYPES = new Set(['member_expression', 'subscript_expression']);
/**
* Root identifier of a TS/JS member chain `chrome` for `chrome.storage.local`
* or null when the chain bottoms out in a call, a literal, or `this`.
* Identifier-rooted member chains have no inferred property type (#1566),
* including host API chains (#1707). Keep the existing `window.MyNamespace`
* escape for project globals; call-result and `this` receivers have their own
* paths and are outside this guard.
*/
function tsJsChainRoot(node: SyntaxNode, source: string): string | null {
function isUnresolvedTsJsChain(node: SyntaxNode, source: string): boolean {
let cur: SyntaxNode | null = node;
while (cur && TS_JS_CHAIN_RECEIVER_TYPES.has(cur.type)) {
cur = getChildByField(cur, 'object');
}
return cur && cur.type === 'identifier' ? getNodeText(cur, source) : null;
return !!cur && cur.type === 'identifier' && getNodeText(cur, source) !== 'window';
}
/**
@@ -4786,18 +4773,14 @@ export class TreeSitterExtractor {
TS_JS_CHAIN_LANGUAGES.has(this.language) &&
receiver &&
TS_JS_CHAIN_RECEIVER_TYPES.has(receiver.type) &&
TS_JS_HOST_GLOBAL_ROOTS.has(tsJsChainRoot(receiver, this.source) ?? '')
isUnresolvedTsJsChain(receiver, this.source)
) {
// TS/JS member call reached through a host namespace —
// `chrome.storage.local.get(key)`, `document.body.querySelector(s)`.
// The bare method name this used to emit exact-matched whatever
// project symbol shared it: every `chrome.storage.local.get/set`
// in a storage wrapper bound to the wrapper's own `get`/`set`,
// a self-edge not in the source (#1707). Emit nothing: a silent
// miss, never a wrong edge. A chain rooted at a project value
// (`window.MyNs.run()`, `store.getState().act()`, `ref.value.m()`)
// keeps the bare name — those targets are real, and dropping them
// would cost far more recall than the mis-bind costs precision.
// `holder.values.get()` has no inferred property type (#1566).
// Emitting bare `get` exact-matches an unrelated project method;
// preserving the chain alone would still allow receiver guessing.
// Emit nothing until the property type can be established. This
// also covers host chains such as `chrome.storage.local.get()`
// (#1707). Calls inside arguments are visited independently.
// Mirrored in the kernel's extract_call (tsjs/extractors.rs).
return;
} else {
+1 -8
View File
@@ -29,6 +29,7 @@ import { logDebug } from '../errors';
import { lexicalPathWithinRoot } from '../utils';
import type { ReExport } from './types';
import { LRUCache } from './lru-cache';
import { JS_BUILT_INS } from './js-builtins';
/** Node kinds that can declare supertypes (extends/implements). */
const SUPERTYPE_BEARING_KINDS = new Set<Node['kind']>([
@@ -70,14 +71,6 @@ function resolveCacheLimit(): number {
export * from './types';
// Pre-built Sets for O(1) built-in lookups (allocated once, shared across all instances)
const JS_BUILT_INS = new Set([
'console', 'window', 'document', 'global', 'process',
'Promise', 'Array', 'Object', 'String', 'Number', 'Boolean',
'Date', 'Math', 'JSON', 'RegExp', 'Error', 'Map', 'Set',
'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval',
'fetch', 'require', 'module', 'exports', '__dirname', '__filename',
]);
const REACT_HOOKS = new Set([
'useState', 'useEffect', 'useContext', 'useReducer', 'useCallback',
'useMemo', 'useRef', 'useLayoutEffect', 'useImperativeHandle', 'useDebugValue',
+8
View File
@@ -0,0 +1,8 @@
/** Shared JS/TS built-ins for direct references and inferred receiver types. */
export const JS_BUILT_INS = new Set([
'console', 'window', 'document', 'global', 'process',
'Promise', 'Array', 'Object', 'String', 'Number', 'Boolean',
'Date', 'Math', 'JSON', 'RegExp', 'Error', 'Map', 'Set', 'WeakMap', 'WeakSet',
'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval',
'fetch', 'require', 'module', 'exports', '__dirname', '__filename',
]);
+8
View File
@@ -8,6 +8,7 @@ import * as path from 'path';
import { Language, Node } from '../types';
import { UnresolvedRef, ResolvedRef, ResolutionContext } from './types';
import { blankStringContents, stripCommentsForRegex } from './strip-comments';
import { JS_BUILT_INS } from './js-builtins';
/**
* Ceiling on how many same-named definitions a FUZZY name-match strategy will
@@ -2194,6 +2195,13 @@ export function matchMethodCall(
if (typedMatch) {
return typedMatch;
}
// A known JS/TS builtin receiver is external when it has no project
// method (#1566). Inference already strips generics (`Map<K, V>` →
// `Map`); do not let Strategy 3 guess an unrelated `get`/`set`/`has`.
// Keep the validated match above for a project type shadowing a builtin.
if (ESM_FAMILY.has(ref.language) && JS_BUILT_INS.has(inferredType)) {
return null;
}
}
}