mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-29 06:32:28 +00:00
fix(scope): label library-rooted receiver drops as external from provider evidence (#3658)
* refactor(scope): expose the position a receiver-chain fold reaches
`foldReceiverChain` answers "which class", so a chain whose last step is
declared as a type the program does not contain (`Dictionary<string,
User>`, `String`) reads only as a miss. That miss is informative to the
receiver-origin classifier: every hop was typed by a program declaration
and the last one names a library type.
Extract the fold loop into `foldReceiverChainState` and export
`foldReceiverChainPosition`, which returns the final position including
one whose declared type named no class, and reports nothing to
`recordReceiverType`. `foldReceiverChain` keeps its behaviour: it now
wraps the same loop and makes the receiver-type report from the final
state, as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(scope): classify library-rooted receiver drops from provider evidence
`classifyReceiverOrigin` had one source of positive external evidence,
`LanguageProvider.isBuiltInName`. That set also filters callee names out
of the call graph, so it could not grow to cover host globals, stdlib
imports or library-typed values without dropping real edges, and every
such drop hedged `impact()` to `lower-bound`. It also read a base name as
evidence even where the program bound that name itself: a parameter
named `console` with an unresolved type, or an unresolved import of a
built-in spelling from the program's own module, came back `external`.
Add two provider data hooks read only by the classifier:
`externalReceiverNames` (globals, platform roots, library types) and
`isExternalModuleSpecifier` (import specifiers reserved for the runtime
or stdlib). The classifier now answers `external` when:
- the base is unbound at the site and is a built-in or listed name;
- the base is bound only by imports that resolved to no program file and
all name a library module;
- the receiver chain folds, through declarations each owner makes, to a
position declared as a library type (`this.data.Keys` with
`Dictionary<string, User> data`), and the program declares nothing by
the dropped member's name (an extension method or trait impl could be
the real target). The fold skips ancestor-hoisted bindings, which can
lend another class's member type to the step.
A base the program binds (a parameter, a local, a resolved import, an
import from a non-library module) never takes a name-based verdict. The
in-program arms are unchanged and still win first. No drop is added or
removed and no edge changes.
Java supplies its reserved `java` package root for both hooks: the JVM
refuses to define classes under `java.*`, so `java.util.Optional.of(...)`
and `import java.util.Map` are rooted in the platform. Without the
specifier hook, the stricter import rule would have demoted the existing
`import java.util.List; List.of(...)` verdict to unknown.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(typescript): name JavaScript host globals as the program boundary
`document.body.appendChild(...)`, `window.history.pushState(...)` and
`Buffer.from(raw).toString()` were recorded `unknown`: none of
`document`, `window`, `Buffer`, `process` or `globalThis` is in
`BUILT_INS`, and they cannot be added there because that set also
filters callee names, so `process` would drop every call to a program
function named `process`. Each drop hedged `impact()` to `lower-bound`
although no program node could have been the target.
List the host objects in `EXTERNAL_RECEIVER_NAMES` and supply it to the
TypeScript, JavaScript and Vue providers as `externalReceiverNames`. A
name counts only where the program binds nothing by it and declares
nothing by it, so a `document: Doc` parameter, an untyped `window`
parameter, a local `navigator` and a program `class Buffer` keep their
verdicts. `location`, `history`, `self` and `crypto` stay out: they are
ordinary local names.
Corpus: 5 call drops move from unknown to external. `process.env.*`
(3 reads) stays in-program there because three fixtures declare a
top-level `function process()`, one of them in a global script.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(python): name standard-library imports as the program boundary
`os.environ.setdefault(...)` after `import os`, and
`Path(__file__).resolve()` after `from pathlib import Path`, were
recorded `unknown`. The imports resolve to no file in the program, but an
unresolved import alone cannot be told apart from an in-program module
the resolver missed, and nothing said that `os` and `pathlib` belong to
the interpreter.
Supply `isExternalModuleSpecifier` from a curated set of standard-library
modules, matched on the specifier's first segment (`os.path` -> `os`).
A relative specifier never matches. The classifier still requires the
import to have resolved to no program file, so a local `os.py` that the
resolver finds wins, and a parameter named `os` is the program's own.
Generic nouns an application plausibly uses for its own modules
(`types`, `queue`, `signal`, `secrets`, `string`, `email`, `http`, ...)
are left out. Third-party packages are not listed: an unresolved
`fastapi` import stays `unknown`.
Corpus: 4 call drops move from unknown to external
(`os.environ.setdefault` x3, `Path(__file__).resolve()`).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(zig): name @import("std") and @import("builtin") as the program boundary
`std.sort.pdq(...)` and `const Allocator = std.mem.Allocator;` were
recorded `unknown`: `std` is bound by `const std = @import("std")`, an
import the resolver deliberately leaves external, and nothing told the
classifier that the module is the standard library rather than an
in-program file it failed to find.
Supply `isExternalModuleSpecifier` for the two module names Zig
reserves: `std` and the compiler-generated `builtin`. Exact names only,
so `@import("std.zig")` (a file path) stays `unknown`, as does `root`
(the program's own root module) and any module `build.zig` names.
Corpus: 9 drops move from unknown to external: `std.sort.pdq` (2 calls,
2 reads), `std.mem.Allocator` (1 read), and the
`zig-qualified-literal` literals `std.Thread.Mutex{}` and
`std.mem.Allocator{...}` (2 calls, 2 reads). The last four are that
fixture's edge negative control (`useExternal` must emit no edge, which
still holds); their receiver is std, so `external` is the right origin.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(csharp): name the System namespace and Dictionary as the program boundary
`System.Console.WriteLine(...)` was recorded `unknown`: `Console` is in
`BUILT_INS` but the chain base is `System`, which is not, and `this.data.Keys`
with `Dictionary<string, User> data` was recorded `in-program` because
the base `this` resolves to a program class even though the member lives
on a BCL type.
Supply `externalReceiverNames` with `System` (the BCL root namespace),
`Dictionary` (the one collection capture does not reduce to its element,
because it takes two type arguments) and `string` (a keyword alias that
no program can declare). The base check covers `System.Console`; the
chain fold reaches `Dictionary<string, User>` through the program's own
field declaration. A program `class System`, a field of a program type,
and an extension method the program declares on `Dictionary` all keep
their verdicts.
Corpus: 3 call drops move from unknown to external
(`System.Console.WriteLine`). The two `this.data.Keys/Values` reads in
`csharp-nested-member-foreach` stay in-program there: the fixture's
global-namespace `App` is one of several in the merged corpus, so `this`
binds another fixture's `App` and the fold declines. Analysed alone,
both read `external`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(kotlin): name java.lang.System and the platform roots as the program boundary
`System.out.println(...)` in Kotlin was recorded `unknown`: Kotlin's
`BUILT_INS` lists stdlib function names, not `System`, which Kotlin/JVM
imports by default from `java.lang`.
Supply `externalReceiverNames` with `System` and the package roots no
program can declare into (`java`, refused by the JVM; `kotlin`, reserved
by the compiler for the standard library), and `isExternalModuleSpecifier`
for imports under those roots. A program `object System` keeps the
receiver `in-program`.
Corpus: 1 call drop moves from unknown to external
(`kotlin-object-literal-scope` `System.out.println`, whose decoy
`println` on the enclosing object still gets no edge).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(bench): rebaseline receiver-origin buckets for library receivers
The preceding commits label library- and runtime-rooted drops `external`
instead of `unknown`. Call drops by origin move from 55 in-program, 44
external and 29 unknown to 55, 62 and 11. Total drops (192), call drops
(128), every per-extension and per-shape count, and every shape-arm
state are unchanged; a same-corpus A/B against main keeps all 2,838
CALLS/ACCESSES edges byte-identical.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(scope): preserve library member collision hedge
* fix(scope): cover more library receiver evidence
* fix(vue): classify imported runtime receivers
---------
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
65b101407e
commit
169aa5d960
19 changed files with 1408 additions and 83 deletions
|
|
@ -995,9 +995,11 @@ Two incidental corrections from that check, both to KTD6:
|
|||
gap that was really a fixture defect — the same error class as the untyped
|
||||
`$repo` control caught earlier.
|
||||
|
||||
### Library-rooted receiver evidence (#3658, #3498)
|
||||
|
||||
### Merged main `f6e42092`
|
||||
After merging main `2b4456553`, receiver totals and all shape states are unchanged: 133 call drops, 160 across all kinds, 27 reads and 0 writes. Provider evidence now classifies 30 calls external, 45 unknown, 56 in-program and 2 unclassified.
|
||||
|
||||
The member-name veto spans the workspace graph. A JavaScript receiver through `window.app` stays uncertain when its member is declared only in TypeScript. Direct built-in calls also stay uncertain when a program declaration matches the member name; that precision cost is deliberate. `globalThis` supplies no external evidence, and library imports must be selected by the lexical claim walk. The post-main build, both TypeScript configurations and all 105 classifier, pipeline and CommonJS checks pass. Before the main merge, the mixed-language correction preserves all 5,319 resolution records byte-for-byte; the final corpus check is recorded with the PR validation.
|
||||
Re-measured both benchmark arms after merging the Zig receiver fix from main.
|
||||
Shape states remain unchanged; counts are 132 call drops and
|
||||
178 drops of all kinds (26 reads,
|
||||
|
|
@ -1017,6 +1019,7 @@ Main `2b4456553` is merged normally. The field lookup retains both the declared
|
|||
|
||||
Main `2b4456553` is merged normally. The measured baseline has 130 call drops and 156 across all kinds, with 26 reads and zero writes. Main has 133 calls, 27 reads and zero writes (160 total). Every shape state matches main. The fresh build and both TypeScript configurations pass, as do all 68 namespace-boundary, capture, hook and CommonJS merge checks.
|
||||
|
||||
After merging main caacc3bac normally, totals remain 130 calls, 156 across all kinds, 26 reads and zero writes. Origin labels are 30 external, 45 unknown, 53 in-program and 2 unclassified. Shape states are unchanged. The 5,328-record corpus is byte-identical to current main. Package build, both pinned TypeScript configurations, 23 CommonJS/C# precedence tests and 15 library-origin boundary tests pass.
|
||||
### Rust qualified factories after #3655
|
||||
|
||||
Main `caacc3bac` is merged normally. The C# capture baseline retains main's fixture additions, and receiver counts are regenerated on the combined source: 132 call drops, 160 total, 28 reads and zero writes. All shape states match the preceding Rust baseline. Fresh build, both TypeScript configurations and 23 Rust factory, capture and CommonJS checks pass. Final hosted CI remains required.
|
||||
|
|
@ -1029,3 +1032,7 @@ The prioritized merge validation passes a fresh package build, both TypeScript c
|
|||
### Rust qualified factories after #3656
|
||||
|
||||
Main `a09f76be3` is merged normally. The shared receiver pass keeps Rust's provider-qualified callee hook and C++'s position-aware qualified-member lookup. Fresh package build, both TypeScript configurations and 302 tests across 20 Rust, C++, Go, CommonJS and sealed-disk-scope files pass. All 16 capture scaling checks pass without changing their budgets. The regenerated receiver check passes with 132 call drops, 160 across all kinds, 28 reads and zero writes; shape states are unchanged. The corpus adds 32 Rust resolution records to main's 5,392, removes none, and totals 5,424. Exact-head hosted CI remains required.
|
||||
|
||||
### External receiver origins after #3657
|
||||
|
||||
Main `65b101407` is merged normally. Origin evidence retains the qualified-type lookup policy and both pipeline passes retain workspace file paths. Package build, both pinned TypeScript configurations, 425 focused tests across 25 files, and all 16 capture checks pass. The regenerated receiver baseline preserves every shape state and main's 132 call drops, 160 total, 28 reads and zero writes. Origin labels are 30 external, 45 unknown, 55 in-program and two unclassified. The 5,424-record resolution corpus is byte-identical to main. Exact-head hosted CI remains required.
|
||||
|
|
|
|||
|
|
@ -229,8 +229,8 @@
|
|||
},
|
||||
"callDropsByOrigin": {
|
||||
"in-program": 55,
|
||||
"external": 44,
|
||||
"unknown": 31,
|
||||
"unknown": 45,
|
||||
"external": 30,
|
||||
"<<unclassified>>": 2
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -870,6 +870,43 @@ interface LanguageProviderConfig {
|
|||
* Default: undefined (no language-specific filtering). */
|
||||
readonly builtInNames?: ReadonlySet<string>;
|
||||
|
||||
// ── Program boundary (receiver-origin classification only) ───────────
|
||||
/**
|
||||
* Values, namespaces and types the language's RUNTIME or STANDARD LIBRARY
|
||||
* supplies without the program declaring them — `document`, `process`,
|
||||
* `System`, `Dictionary` — matched against a bare identifier: a dropped
|
||||
* receiver's chain base, or the head of the type a receiver chain ends at.
|
||||
*
|
||||
* Read ONLY by `classifyReceiverOrigin`, which labels a call it could not
|
||||
* resolve `external` (nothing in the program could have been the target)
|
||||
* instead of `unknown`. Kept apart from `builtInNames` on purpose: that set
|
||||
* also filters CALLEE names out of the call graph (`parse-worker.ts`,
|
||||
* `type-env.ts`, the free-call fallback), so `process` there would drop every
|
||||
* call to an in-program function named `process`. Nothing here can change an
|
||||
* edge.
|
||||
*
|
||||
* A name only counts where the program binds nothing by it at the site and
|
||||
* declares no type or value by it, so a local `document` parameter or an
|
||||
* in-program `class System` still wins. The failure is asymmetric — a wrong
|
||||
* entry hides a real resolver gap behind `epistemic: 'exact'`, a missing one
|
||||
* only costs a hedge — so a set should under-include. Default: undefined (no
|
||||
* name is evidence).
|
||||
*/
|
||||
readonly externalReceiverNames?: ReadonlySet<string>;
|
||||
/**
|
||||
* Does this import SPECIFIER name a module the language reserves for its
|
||||
* runtime or standard library (Python `os`, Zig `std`, Java `java.util`)?
|
||||
*
|
||||
* Read ONLY by `classifyReceiverOrigin`, and only for an import that also
|
||||
* resolved to no file in the program: a name bound by such an import is
|
||||
* rooted outside the program however it is spelled locally
|
||||
* (`const s = @import("std")`). Third-party packages are NOT answered here —
|
||||
* an unresolved `fastapi` or `express` import cannot be told apart from an
|
||||
* in-program module the resolver missed, so it stays `unknown`. Default:
|
||||
* undefined (no import is evidence).
|
||||
*/
|
||||
readonly isExternalModuleSpecifier?: (specifier: string) => boolean;
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// Scope-based resolution hooks (RFC #909 — Ring 1 #911)
|
||||
//
|
||||
|
|
|
|||
|
|
@ -132,6 +132,31 @@ const BUILT_INS: ReadonlySet<string> = new Set([
|
|||
'ThrowIfNull',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Program-boundary names for `LanguageProvider.externalReceiverNames`, read
|
||||
* against a dropped receiver's base (`System.Console.WriteLine(...)`) and
|
||||
* against the type a receiver chain ends at (`this.data.Keys` with
|
||||
* `Dictionary<string, User> data`). Kept out of `BUILT_INS`, which also
|
||||
* filters callee names.
|
||||
*
|
||||
* `System` is the BCL's root namespace. These two-argument collection types
|
||||
* retain their outer name at capture rather than reducing to one element type,
|
||||
* so they reach a receiver position spelled as themselves. `string`
|
||||
* is a keyword alias of `System.String` and cannot be declared by a program.
|
||||
* A program type or binding by any of these names still wins.
|
||||
*/
|
||||
const EXTERNAL_RECEIVER_NAMES: ReadonlySet<string> = new Set([
|
||||
'System',
|
||||
'Dictionary',
|
||||
'SortedDictionary',
|
||||
'SortedList',
|
||||
'ConcurrentDictionary',
|
||||
'ReadOnlyDictionary',
|
||||
'IDictionary',
|
||||
'IReadOnlyDictionary',
|
||||
'string',
|
||||
]);
|
||||
|
||||
export const csharpProvider = defineLanguage({
|
||||
rename: csharpRenameProvider,
|
||||
id: SupportedLanguages.CSharp,
|
||||
|
|
@ -202,6 +227,7 @@ export const csharpProvider = defineLanguage({
|
|||
// ── XML doc comments (`///`) → description (issue #2270) ──
|
||||
descriptionExtractor: createLeadingDocDescriptionExtractor(),
|
||||
builtInNames: BUILT_INS,
|
||||
externalReceiverNames: EXTERNAL_RECEIVER_NAMES,
|
||||
|
||||
// ── RFC #909 Ring 3: scope-based resolution hooks (RFC §5) ──────────
|
||||
// C# is the second migration after Python. See ./csharp/index.ts for
|
||||
|
|
|
|||
|
|
@ -126,6 +126,19 @@ const BUILT_INS: ReadonlySet<string> = new Set([
|
|||
'Objects',
|
||||
]);
|
||||
|
||||
/**
|
||||
* The platform's reserved package root, for both program-boundary hooks:
|
||||
* `java.util.Optional.of(...)` spelled out in full, and `import java.util.List`
|
||||
* — an import the workspace can never resolve. The JVM refuses to define a
|
||||
* class in a `java.*` package outside the platform ("Prohibited package name"),
|
||||
* so nothing a program declares can live under it. `javax` is deliberately
|
||||
* absent: that root carries no such guarantee.
|
||||
*/
|
||||
const JAVA_PLATFORM_ROOT = 'java';
|
||||
|
||||
const isJavaPlatformModule = (specifier: string): boolean =>
|
||||
specifier === JAVA_PLATFORM_ROOT || specifier.startsWith(`${JAVA_PLATFORM_ROOT}.`);
|
||||
|
||||
const orderJavaSameNameTypeCandidates = ({
|
||||
callSiteFilePath,
|
||||
candidates,
|
||||
|
|
@ -208,6 +221,8 @@ export const javaProvider = defineLanguage({
|
|||
// ── Javadoc → description (issue #2270) ──
|
||||
descriptionExtractor: createLeadingDocDescriptionExtractor(),
|
||||
builtInNames: BUILT_INS,
|
||||
externalReceiverNames: new Set([JAVA_PLATFORM_ROOT]),
|
||||
isExternalModuleSpecifier: isJavaPlatformModule,
|
||||
|
||||
// ── RFC #909 Ring 3: scope-based resolution hooks ──
|
||||
emitScopeCaptures: emitJavaScopeCaptures,
|
||||
|
|
|
|||
|
|
@ -128,6 +128,22 @@ const BUILT_INS: ReadonlySet<string> = new Set([
|
|||
'step',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Program-boundary names for `LanguageProvider.externalReceiverNames`.
|
||||
*
|
||||
* `System` is `java.lang.System`, which Kotlin/JVM imports by default, so
|
||||
* `System.out.println(...)` reaches the JDK. `java` and `kotlin` are package
|
||||
* roots no program can declare into — the JVM refuses `java.*`, and the
|
||||
* compiler reserves `kotlin.*` for the standard library — so a fully qualified
|
||||
* `java.util.UUID.randomUUID()` is rooted outside too. A name counts only where
|
||||
* the program binds nothing by it, so an `object System` still wins.
|
||||
*/
|
||||
const EXTERNAL_RECEIVER_NAMES: ReadonlySet<string> = new Set(['System', 'java', 'kotlin']);
|
||||
|
||||
/** Imports from those same reserved roots can never resolve in-workspace. */
|
||||
const isKotlinPlatformModule = (specifier: string): boolean =>
|
||||
/^(?:java|kotlin)(?:\.|$)/.test(specifier);
|
||||
|
||||
export const kotlinProvider = defineLanguage({
|
||||
rename: kotlinRenameProvider,
|
||||
id: SupportedLanguages.Kotlin,
|
||||
|
|
@ -184,6 +200,8 @@ export const kotlinProvider = defineLanguage({
|
|||
variableExtractor: createVariableExtractor(kotlinVariableConfig),
|
||||
classExtractor: createClassExtractor(kotlinClassConfig),
|
||||
builtInNames: BUILT_INS,
|
||||
externalReceiverNames: EXTERNAL_RECEIVER_NAMES,
|
||||
isExternalModuleSpecifier: isKotlinPlatformModule,
|
||||
|
||||
// ── KDoc → description (issue #2270) ──
|
||||
descriptionExtractor: createLeadingDocDescriptionExtractor(),
|
||||
|
|
|
|||
|
|
@ -78,6 +78,117 @@ const BUILT_INS: ReadonlySet<string> = new Set([
|
|||
'abs',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Standard-library modules, for `LanguageProvider.isExternalModuleSpecifier`:
|
||||
* a name bound by `import os` or `from pathlib import Path` — when the import
|
||||
* resolved to no file in the program — is rooted in the interpreter's library,
|
||||
* so `os.environ.setdefault(...)` lost no in-program edge.
|
||||
*
|
||||
* Matched against the specifier's FIRST segment (`os.path` → `os`); a relative
|
||||
* specifier (`.compat`) has an empty one and never matches. Third-party
|
||||
* packages are not listed and cannot be: an unresolved `fastapi` import looks
|
||||
* exactly like an in-program module the resolver missed.
|
||||
*
|
||||
* Under-includes on purpose. The verdict already requires the import to have
|
||||
* resolved nowhere, so a local `os.py` that the resolver finds wins; what this
|
||||
* list guards against is a local module the resolver did NOT find. Generic
|
||||
* nouns an application plausibly names its own modules after — `types`,
|
||||
* `queue`, `signal`, `secrets`, `string`, `email`, `http`, `html`, `xml`,
|
||||
* `operator`, `platform`, `test` — are therefore left out even though the
|
||||
* standard library has them.
|
||||
*/
|
||||
const STDLIB_MODULES: ReadonlySet<string> = new Set([
|
||||
'__future__',
|
||||
'abc',
|
||||
'argparse',
|
||||
'ast',
|
||||
'asyncio',
|
||||
'atexit',
|
||||
'base64',
|
||||
'binascii',
|
||||
'bisect',
|
||||
'builtins',
|
||||
'bz2',
|
||||
'cmath',
|
||||
'codecs',
|
||||
'collections',
|
||||
'concurrent',
|
||||
'configparser',
|
||||
'contextlib',
|
||||
'contextvars',
|
||||
'copy',
|
||||
'csv',
|
||||
'ctypes',
|
||||
'dataclasses',
|
||||
'datetime',
|
||||
'decimal',
|
||||
'difflib',
|
||||
'enum',
|
||||
'errno',
|
||||
'fnmatch',
|
||||
'fractions',
|
||||
'functools',
|
||||
'gc',
|
||||
'getpass',
|
||||
'glob',
|
||||
'gzip',
|
||||
'hashlib',
|
||||
'heapq',
|
||||
'hmac',
|
||||
'importlib',
|
||||
'inspect',
|
||||
'io',
|
||||
'ipaddress',
|
||||
'itertools',
|
||||
'json',
|
||||
'keyword',
|
||||
'linecache',
|
||||
'locale',
|
||||
'logging',
|
||||
'lzma',
|
||||
'math',
|
||||
'mimetypes',
|
||||
'multiprocessing',
|
||||
'os',
|
||||
'pathlib',
|
||||
'pickle',
|
||||
'pkgutil',
|
||||
'pprint',
|
||||
'random',
|
||||
're',
|
||||
'shlex',
|
||||
'shutil',
|
||||
'socket',
|
||||
'sqlite3',
|
||||
'ssl',
|
||||
'stat',
|
||||
'statistics',
|
||||
'struct',
|
||||
'subprocess',
|
||||
'sys',
|
||||
'sysconfig',
|
||||
'tarfile',
|
||||
'tempfile',
|
||||
'textwrap',
|
||||
'threading',
|
||||
'time',
|
||||
'timeit',
|
||||
'traceback',
|
||||
'typing',
|
||||
'unicodedata',
|
||||
'unittest',
|
||||
'urllib',
|
||||
'uuid',
|
||||
'warnings',
|
||||
'weakref',
|
||||
'zipfile',
|
||||
'zlib',
|
||||
'zoneinfo',
|
||||
]);
|
||||
|
||||
const isPythonStdlibModule = (specifier: string): boolean =>
|
||||
STDLIB_MODULES.has(specifier.split('.', 1)[0] ?? '');
|
||||
|
||||
function pythonDescriptionExtractor(
|
||||
nodeLabel: NodeLabel,
|
||||
_nodeName: string,
|
||||
|
|
@ -136,6 +247,7 @@ export const pythonProvider = defineLanguage({
|
|||
classExtractor: createClassExtractor(pythonClassConfig),
|
||||
descriptionExtractor: pythonDescriptionExtractor,
|
||||
builtInNames: BUILT_INS,
|
||||
isExternalModuleSpecifier: isPythonStdlibModule,
|
||||
// Django routing is whole-repo and cross-file (manage.py → settings →
|
||||
// ROOT_URLCONF → root urls.py, then include()s across files), so it runs as
|
||||
// a main-thread pass (see parse-impl's cross-file route extraction) rather
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
*/
|
||||
|
||||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
import { isBuiltin } from 'node:module';
|
||||
import type Parser from 'tree-sitter';
|
||||
import { unboundCallableName } from './typescript/unbound-callables.js';
|
||||
import type { CaptureMatch, NodeLabel, ParsedFile } from 'gitnexus-shared';
|
||||
|
|
@ -401,6 +402,34 @@ export const BUILT_INS: ReadonlySet<string> = new Set([
|
|||
'valueOf',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Host objects a JavaScript runtime supplies as globals, for
|
||||
* `LanguageProvider.externalReceiverNames`: a dropped receiver rooted at one
|
||||
* (`document.body.appendChild`, `process.env.API_BASE`,
|
||||
* `Buffer.from(raw).toString()`) reached code no index contains.
|
||||
*
|
||||
* Separate from `BUILT_INS`, which also filters CALLEE names out of the call
|
||||
* graph — `process` there would drop every call to a program function named
|
||||
* `process`. Here a name counts only when the program binds nothing by it at
|
||||
* the site and declares nothing by it anywhere, so a `document` parameter or a
|
||||
* module-level `function process()` keeps the receiver hedged.
|
||||
*
|
||||
* Host objects only, and only ones an application does not name its own
|
||||
* values after: `location`, `history`, `self` and `crypto` are ordinary local
|
||||
* names, and reaching them through `window` is already covered.
|
||||
*/
|
||||
export const EXTERNAL_RECEIVER_NAMES: ReadonlySet<string> = new Set([
|
||||
// Browser host.
|
||||
'window',
|
||||
'document',
|
||||
'navigator',
|
||||
'localStorage',
|
||||
'sessionStorage',
|
||||
// Node host.
|
||||
'process',
|
||||
'Buffer',
|
||||
]);
|
||||
|
||||
/**
|
||||
* `this` is the only receiver keyword JavaScript and TypeScript bind, and it
|
||||
* is bound by every function form except an arrow (#2701). The query files
|
||||
|
|
@ -502,6 +531,8 @@ export const typescriptProvider = defineLanguage({
|
|||
wrapperNodeTypes: ['export_statement'],
|
||||
}),
|
||||
builtInNames: BUILT_INS,
|
||||
externalReceiverNames: EXTERNAL_RECEIVER_NAMES,
|
||||
isExternalModuleSpecifier: isBuiltin,
|
||||
|
||||
// Member-assignment shapes are not free functions (#2723 follow-up). The
|
||||
// capture arrives anchored on the assignment, so the shape is decided from
|
||||
|
|
@ -603,6 +634,8 @@ export const javascriptProvider = defineLanguage({
|
|||
wrapperNodeTypes: ['export_statement'],
|
||||
}),
|
||||
builtInNames: BUILT_INS,
|
||||
externalReceiverNames: EXTERNAL_RECEIVER_NAMES,
|
||||
isExternalModuleSpecifier: isBuiltin,
|
||||
|
||||
// Member-assignment shapes — see the TypeScript provider above.
|
||||
labelOverride: (functionNode, defaultLabel) =>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
*/
|
||||
|
||||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
import { isBuiltin } from 'node:module';
|
||||
import { createClassExtractor } from '../class-extractors/generic.js';
|
||||
import { vueClassConfig } from '../class-extractors/configs/typescript-javascript.js';
|
||||
import { defineLanguage } from '../language-provider.js';
|
||||
|
|
@ -19,7 +20,10 @@ import { typeConfig as typescriptConfig } from '../type-extractors/typescript.js
|
|||
import { tsExportChecker } from '../export-detection.js';
|
||||
import { TYPESCRIPT_QUERIES } from '../tree-sitter-queries.js';
|
||||
import { typescriptFieldExtractor } from '../field-extractors/typescript.js';
|
||||
import { BUILT_INS as TS_BUILT_INS } from './typescript.js';
|
||||
import {
|
||||
BUILT_INS as TS_BUILT_INS,
|
||||
EXTERNAL_RECEIVER_NAMES as TS_EXTERNAL_RECEIVER_NAMES,
|
||||
} from './typescript.js';
|
||||
import { createVariableExtractor } from '../variable-extractors/generic.js';
|
||||
import { typescriptVariableConfig } from '../variable-extractors/configs/typescript-javascript.js';
|
||||
import { createCallExtractor } from '../call-extractors/generic.js';
|
||||
|
|
@ -89,6 +93,9 @@ export const vueProvider = defineLanguage({
|
|||
variableExtractor: createVariableExtractor(typescriptVariableConfig),
|
||||
classExtractor: vueClassExtractor,
|
||||
builtInNames: VUE_BUILT_INS,
|
||||
// A `<script>` block runs on the same host as any other module.
|
||||
externalReceiverNames: TS_EXTERNAL_RECEIVER_NAMES,
|
||||
isExternalModuleSpecifier: isBuiltin,
|
||||
// Vue SFC <script> blocks use the TypeScript or TSX grammar,
|
||||
// so the TS CFG visitor builds CFGs for the script's functions verbatim —
|
||||
// no Vue-specific visitor needed (#2195).
|
||||
|
|
|
|||
|
|
@ -64,6 +64,17 @@ import {
|
|||
ZIG_CONTAINER_TYPES,
|
||||
} from './zig/index.js';
|
||||
|
||||
/**
|
||||
* The module names Zig reserves, for `LanguageProvider.isExternalModuleSpecifier`:
|
||||
* `@import("std")` is the standard library and `@import("builtin")` is
|
||||
* compiler-generated, so a name bound by either — `const std = @import("std")`
|
||||
* or any other spelling — is rooted outside the program and `std.sort.pdq(...)`
|
||||
* lost no in-program edge. Exact names only: `@import("std.zig")` is a FILE
|
||||
* path, and `root` is the program's own root module. Modules named by
|
||||
* `build.zig` (`addImport`) may be in-program or packages, so none is listed.
|
||||
*/
|
||||
const ZIG_RESERVED_MODULES: ReadonlySet<string> = new Set(['std', 'builtin']);
|
||||
|
||||
export const zigProvider = defineLanguage({
|
||||
rename: zigRenameProvider,
|
||||
id: SupportedLanguages.Zig,
|
||||
|
|
@ -77,6 +88,7 @@ export const zigProvider = defineLanguage({
|
|||
typeConfig: zigTypeConfig,
|
||||
cfgVisitor: createZigCfgVisitor(),
|
||||
exportChecker: zigExportChecker,
|
||||
isExternalModuleSpecifier: (specifier) => ZIG_RESERVED_MODULES.has(specifier),
|
||||
callExtractor: createCallExtractor(zigCallConfig),
|
||||
classExtractor: createClassExtractor(zigClassConfig),
|
||||
fieldExtractor: createFieldExtractor(zigFieldConfig),
|
||||
|
|
|
|||
|
|
@ -756,6 +756,74 @@ export function foldReceiverChain(
|
|||
index: WorkspaceResolutionIndex,
|
||||
options: ResolveCompoundReceiverOptions = {},
|
||||
): SymbolDefinition | undefined {
|
||||
const current = foldReceiverChainState(chain, inScope, scopes, index, options);
|
||||
if (current === undefined) return undefined;
|
||||
// A chain that ended without a class returns undefined naturally — no
|
||||
// separate guard, because `def` IS the signal.
|
||||
//
|
||||
// The receiver-type report is made HERE, from the final `FoldState`, because
|
||||
// that record pairs the class with the spelling that produced it BY
|
||||
// CONSTRUCTION — same step, same lookup. The individual `classOfDeclaredType`
|
||||
// calls inside the fold also report, including from steps that were later
|
||||
// folded past, so the last of those is not reliably about the class the fold
|
||||
// returns. Reporting the final state last makes it the one that stands.
|
||||
if (current.def !== undefined && current.declaredType !== undefined) {
|
||||
options.recordReceiverType?.(current.declaredType, current.def.nodeId);
|
||||
}
|
||||
return current.def;
|
||||
}
|
||||
|
||||
/** Where a fully folded receiver chain landed — see {@link foldReceiverChainPosition}. */
|
||||
export interface FoldedReceiverPosition {
|
||||
/** The class the final position resolved to; absent when its declared type
|
||||
* named no class in the program (`Dictionary<string, User>`, `String`). */
|
||||
readonly def: SymbolDefinition | undefined;
|
||||
/** That position's declared type AS WRITTEN, when a declaration typed it. */
|
||||
readonly declaredType: string | undefined;
|
||||
/** The scope the declared type was written in — its lookup anchor. */
|
||||
readonly declaredAtScope: ScopeId | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* The position {@link foldReceiverChain} reaches, including a final position
|
||||
* whose declared type named no class.
|
||||
*
|
||||
* `foldReceiverChain` answers "which class", so a chain ending at a member
|
||||
* declared as `Dictionary<string, User>` reads as a miss there. That miss is
|
||||
* itself informative: every step was typed by a declaration in the program, and
|
||||
* the last one names a type the program does not contain. Exposed for the
|
||||
* receiver-origin classifier, which needs exactly that to tell a library-typed
|
||||
* receiver from an untypable one. Same fold, same declines — nothing is typed
|
||||
* here that `foldReceiverChain` would not type — and it reports nothing to
|
||||
* `recordReceiverType`.
|
||||
*/
|
||||
export function foldReceiverChainPosition(
|
||||
chain: DecodedReceiverChain,
|
||||
inScope: ScopeId,
|
||||
scopes: ScopeResolutionIndexes,
|
||||
index: WorkspaceResolutionIndex,
|
||||
options: ResolveCompoundReceiverOptions = {},
|
||||
): FoldedReceiverPosition | undefined {
|
||||
const current = foldReceiverChainState(chain, inScope, scopes, index, {
|
||||
...options,
|
||||
recordReceiverType: undefined,
|
||||
});
|
||||
if (current === undefined) return undefined;
|
||||
return {
|
||||
def: current.def,
|
||||
declaredType: current.declaredType,
|
||||
declaredAtScope: current.declaredAtScope,
|
||||
};
|
||||
}
|
||||
|
||||
/** The fold behind {@link foldReceiverChain} and {@link foldReceiverChainPosition}. */
|
||||
function foldReceiverChainState(
|
||||
chain: DecodedReceiverChain,
|
||||
inScope: ScopeId,
|
||||
scopes: ScopeResolutionIndexes,
|
||||
index: WorkspaceResolutionIndex,
|
||||
options: ResolveCompoundReceiverOptions,
|
||||
): FoldState | undefined {
|
||||
// A truncated chain is missing the head that decides the final type. The
|
||||
// producer refuses to mint one, so this is defence in depth against a
|
||||
// future producer that does.
|
||||
|
|
@ -884,19 +952,7 @@ export function foldReceiverChain(
|
|||
if (next === undefined) return undefined;
|
||||
current = next;
|
||||
}
|
||||
// A chain that ended without a class returns undefined naturally — no
|
||||
// separate guard, because `def` IS the signal.
|
||||
//
|
||||
// The receiver-type report is made HERE, from the final `FoldState`, because
|
||||
// that record pairs the class with the spelling that produced it BY
|
||||
// CONSTRUCTION — same step, same lookup. The individual `classOfDeclaredType`
|
||||
// calls inside the fold also report, including from steps that were later
|
||||
// folded past, so the last of those is not reliably about the class the fold
|
||||
// returns. Reporting the final state last makes it the one that stands.
|
||||
if (current.def !== undefined && current.declaredType !== undefined) {
|
||||
options.recordReceiverType?.(current.declaredType, current.def.nodeId);
|
||||
}
|
||||
return current.def;
|
||||
return current;
|
||||
}
|
||||
|
||||
/** A resolved compound receiver, together with the declared spelling that typed
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ import type { KnowledgeGraph } from '../../../graph/types.js';
|
|||
import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js';
|
||||
import type { SemanticModel } from '../../model/semantic-model.js';
|
||||
import type { ScopeResolver } from '../contract/scope-resolver.js';
|
||||
import type { GraphNodeLookup } from '../graph-bridge/node-lookup.js';
|
||||
import { LINKABLE_LABELS, type GraphNodeLookup } from '../graph-bridge/node-lookup.js';
|
||||
import type { WorkspaceResolutionIndex } from '../workspace-index.js';
|
||||
import { createNamespaceTargetCache } from '../scope/namespace-target-cache.js';
|
||||
import {
|
||||
|
|
@ -101,11 +101,17 @@ import { constructionSiteReason } from './free-call-fallback.js';
|
|||
import { bindQualifiedCalleeResolver } from './imported-return-types.js';
|
||||
import type { CalleeIdSink } from '../graph-bridge/callee-id-sink.js';
|
||||
import {
|
||||
foldReceiverChainPosition,
|
||||
classOfQualifiedTypeSpelling,
|
||||
resolveCompoundReceiverClass,
|
||||
resolveCompoundReceiverTyped,
|
||||
type FoldedReceiverPosition,
|
||||
} from '../passes/compound-receiver.js';
|
||||
import { erasedTypeApplication, typeApplicationArguments } from '../../utils/template-arguments.js';
|
||||
import {
|
||||
erasedTypeApplication,
|
||||
stripTemplateArguments,
|
||||
typeApplicationArguments,
|
||||
} from '../../utils/template-arguments.js';
|
||||
import {
|
||||
heritageTypeArgumentsKey,
|
||||
stepHeritageInstantiation,
|
||||
|
|
@ -243,6 +249,206 @@ function isBareTypeName(rawName: string): boolean {
|
|||
return BARE_TYPE_NAME_RE.test(rawName);
|
||||
}
|
||||
|
||||
/**
|
||||
* What {@link classifyReceiverOrigin} may consult beyond the scope indexes.
|
||||
*
|
||||
* Every member is optional, and a missing one can only WITHHOLD an `external`
|
||||
* verdict — never produce one. Built once per pass by
|
||||
* {@link buildReceiverOriginEvidence}, which the classifier unit tests call too,
|
||||
* so the hooks a test exercises are the hooks the pass threads.
|
||||
*/
|
||||
export interface ReceiverOriginEvidence {
|
||||
/** The language's type-preserving decoration stripper. Without it a Go
|
||||
* pointer receiver — `func (h *Host)` binds `h` to the literal `*Host` —
|
||||
* resolves to no class and the whole method body's drops were reported as
|
||||
* external. Same hook the three receiver-chain lookups in
|
||||
* `compound-receiver.ts` already receive. */
|
||||
readonly stripTypePreservingDecoration?: DecorationStripper;
|
||||
/** Classify qualified declared types with the same provider policy as resolution. */
|
||||
readonly qualifiedTypeSpellings?: boolean;
|
||||
/** `LanguageProvider.isBuiltInName`, threaded through the pass options the
|
||||
* same way `emitFreeCallFallback` receives it. Languages that declare no
|
||||
* built-in set simply never produce an `external` verdict from it, which is
|
||||
* the safe default. */
|
||||
readonly isBuiltInName?: (name: string) => boolean;
|
||||
/** `LanguageProvider.externalReceiverNames` — host globals, platform
|
||||
* namespaces and library types, read as evidence only where the program
|
||||
* binds nothing by that name. */
|
||||
readonly externalReceiverNames?: ReadonlySet<string>;
|
||||
/** Is every import binding `localName` in `filePath` an import of a module the
|
||||
* language reserves for its runtime or standard library? Answered from the
|
||||
* import SPECIFIERS (`LanguageProvider.isExternalModuleSpecifier`); whether
|
||||
* the import resolved is the classifier's to check, from the import edges. */
|
||||
readonly importsLibraryModule?: (filePath: string, localName: string) => boolean;
|
||||
/** `foldReceiverChainPosition` under the pass's own compound-receiver
|
||||
* options, minus ancestor-hoisted bindings: where a minted receiver chain
|
||||
* lands when every step is typed by a declaration its owner makes. */
|
||||
readonly foldChainPosition?: (
|
||||
chain: DecodedReceiverChain,
|
||||
inScope: ScopeId,
|
||||
) => FoldedReceiverPosition | undefined;
|
||||
/** Does the program declare anything by this simple name? A program member
|
||||
* can be the target of a call on a LIBRARY type wherever the language lets
|
||||
* code add members to a type it does not own (extension methods, trait
|
||||
* impls), so a type-derived verdict declines whenever the dropped member's
|
||||
* name is one the program declares. */
|
||||
readonly programDeclaresName?: (name: string) => boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the {@link ReceiverOriginEvidence} for one language run.
|
||||
*
|
||||
* The per-file import table and the declared-name set are built on first use,
|
||||
* not here: both exist only to answer questions about DROPPED sites, and a run
|
||||
* that drops nothing should not pay a walk over every import and definition.
|
||||
*/
|
||||
export function buildReceiverOriginEvidence(input: {
|
||||
/** Complete workspace graph, including declarations from other language passes. */
|
||||
readonly graph?: KnowledgeGraph;
|
||||
readonly parsedFiles: readonly ParsedFile[];
|
||||
readonly scopes: ScopeResolutionIndexes;
|
||||
readonly index: WorkspaceResolutionIndex;
|
||||
/** The options the pass hands `resolveCompoundReceiverClass`, so the fold
|
||||
* here types a subset of what the fold there types — see `foldChainPosition`
|
||||
* below for the one branch it leaves out. */
|
||||
readonly foldOptions: NonNullable<Parameters<typeof foldReceiverChainPosition>[4]>;
|
||||
readonly stripTypePreservingDecoration?: DecorationStripper;
|
||||
readonly qualifiedTypeSpellings?: boolean;
|
||||
readonly isBuiltInName?: (name: string) => boolean;
|
||||
readonly externalReceiverNames?: ReadonlySet<string>;
|
||||
readonly isExternalModuleSpecifier?: (specifier: string) => boolean;
|
||||
}): ReceiverOriginEvidence {
|
||||
const { parsedFiles, scopes, index, foldOptions, isExternalModuleSpecifier } = input;
|
||||
let libraryImports: ReadonlyMap<string, ReadonlyMap<string, boolean>> | undefined;
|
||||
let declaredNames: ReadonlySet<string> | undefined;
|
||||
return {
|
||||
stripTypePreservingDecoration: input.stripTypePreservingDecoration,
|
||||
qualifiedTypeSpellings: input.qualifiedTypeSpellings,
|
||||
isBuiltInName: input.isBuiltInName,
|
||||
externalReceiverNames: input.externalReceiverNames,
|
||||
importsLibraryModule:
|
||||
isExternalModuleSpecifier === undefined
|
||||
? undefined
|
||||
: (filePath, localName) =>
|
||||
(libraryImports ??= indexLibraryImports(parsedFiles, isExternalModuleSpecifier))
|
||||
.get(filePath)
|
||||
?.get(localName) === true,
|
||||
// Never through bindings hoisted onto an ancestor scope. That branch serves
|
||||
// languages that store method return types on the module, but it answers
|
||||
// with ANY same-named binding there — another class's member in the same
|
||||
// file, or another file's in a shared package scope — so it can type a
|
||||
// step against the wrong owner. Resolution tolerates that (a wrong owner
|
||||
// that is a program class is still checked against its members); an
|
||||
// `external` verdict built on it would not be checked at all.
|
||||
foldChainPosition: (chain, inScope) =>
|
||||
foldReceiverChainPosition(chain, inScope, scopes, index, {
|
||||
...foldOptions,
|
||||
hoistTypeBindingsToModule: false,
|
||||
}),
|
||||
programDeclaresName: (name) =>
|
||||
(declaredNames ??= indexDeclaredNames(scopes, input.graph)).has(name),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Per file, per local name: is EVERY import binding that name an import of a
|
||||
* library module? One non-library import of the same name (a relative module,
|
||||
* a third-party package, a dynamic import) answers `false` for all of them.
|
||||
*/
|
||||
function indexLibraryImports(
|
||||
parsedFiles: readonly ParsedFile[],
|
||||
isExternalModuleSpecifier: (specifier: string) => boolean,
|
||||
): ReadonlyMap<string, ReadonlyMap<string, boolean>> {
|
||||
const byFile = new Map<string, Map<string, boolean>>();
|
||||
for (const parsed of parsedFiles) {
|
||||
let byName: Map<string, boolean> | undefined;
|
||||
for (const imp of parsed.parsedImports) {
|
||||
// Wildcard, side-effect and resolved dynamic imports bind no local name.
|
||||
if (!('localName' in imp)) continue;
|
||||
const library = imp.kind !== 'dynamic-unresolved' && isExternalModuleSpecifier(imp.targetRaw);
|
||||
byName ??= new Map();
|
||||
byName.set(imp.localName, (byName.get(imp.localName) ?? true) && library);
|
||||
}
|
||||
if (byName !== undefined) byFile.set(parsed.filePath, byName);
|
||||
}
|
||||
return byFile;
|
||||
}
|
||||
|
||||
/** Every simple name the program declares, from the qualified-name index. */
|
||||
function indexDeclaredNames(
|
||||
scopes: ScopeResolutionIndexes,
|
||||
graph?: KnowledgeGraph,
|
||||
): ReadonlySet<string> {
|
||||
const names = new Set<string>();
|
||||
for (const qualifiedName of scopes.qualifiedNames.byQualifiedName.keys()) {
|
||||
names.add(
|
||||
qualifiedName.slice(
|
||||
Math.max(qualifiedName.lastIndexOf('.'), qualifiedName.lastIndexOf(':')) + 1,
|
||||
),
|
||||
);
|
||||
}
|
||||
// Scope indexes cover one language only. Interoperating files can declare
|
||||
// the program member that a library-rooted receiver eventually reaches.
|
||||
graph?.forEachNode((node) => {
|
||||
if (LINKABLE_LABELS.has(node.label)) names.add(node.properties.name);
|
||||
});
|
||||
return names;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this declared type spelling name a LIBRARY type the program does not
|
||||
* declare? Asked of the head only — `Dictionary<string, User>` is a
|
||||
* `Dictionary`, whatever its arguments — and only after the language's own
|
||||
* decoration stripper, so `List<Post>?` and `*Build` are read by their types.
|
||||
*/
|
||||
function namesLibraryType(
|
||||
spelling: string,
|
||||
declaredAtScope: ScopeId,
|
||||
scopes: ScopeResolutionIndexes,
|
||||
evidence: ReceiverOriginEvidence,
|
||||
): boolean {
|
||||
const strip = evidence.stripTypePreservingDecoration;
|
||||
const head = stripTemplateArguments(strip?.(spelling) ?? spelling).trim();
|
||||
if (!isBareTypeName(head)) return false;
|
||||
if (evidence.isBuiltInName?.(head) !== true && evidence.externalReceiverNames?.has(head) !== true)
|
||||
return false;
|
||||
// The program's own type by that name wins, wherever it is declared.
|
||||
if (scopes.qualifiedNames.has(head)) return false;
|
||||
return resolveClassBindingForName(declaredAtScope, head, scopes, strip) === undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* How the program binds a receiver's base name at the site — the question
|
||||
* base-name evidence must answer first, because a name the program binds is
|
||||
* the program's own however familiar it looks.
|
||||
*
|
||||
* - `unbound` — nothing on the scope chain claims the name: a global.
|
||||
* - `library-import` — claimed only by imports that resolved to no file in the
|
||||
* program and that name a library module (`import os`,
|
||||
* `const std = @import("std")`).
|
||||
* - `unresolved-import` — claimed only by imports that resolved to no file, at
|
||||
* least one from a module the language does not reserve. Not evidence: an
|
||||
* unresolved third-party package and an in-program module the resolver
|
||||
* missed look the same here.
|
||||
* - `bound` — anything else: a parameter, a local, a resolved import.
|
||||
*/
|
||||
function baseRooting(
|
||||
inScope: ScopeId,
|
||||
base: string,
|
||||
scopes: ScopeResolutionIndexes,
|
||||
evidence: ReceiverOriginEvidence,
|
||||
): 'unbound' | 'library-import' | 'unresolved-import' | 'bound' {
|
||||
const claim = lookupNameClaim(inScope, base, scopes);
|
||||
if (claim.status === 'absent') return 'unbound';
|
||||
const owner = claim.scope;
|
||||
if (owner === undefined || claim.bindings.length > 0) return 'bound';
|
||||
const edges = claim.imports;
|
||||
if (edges.length === 0 || edges.some((edge) => edge.targetFile !== null)) return 'bound';
|
||||
return evidence.importsLibraryModule?.(owner.filePath, base) === true
|
||||
? 'library-import'
|
||||
: 'unresolved-import';
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this dropped receiver rooted inside the analyzed program?
|
||||
*
|
||||
|
|
@ -253,10 +459,21 @@ function isBareTypeName(rawName: string): boolean {
|
|||
* - `in-program` — the base's declared type resolves here, or the base itself is
|
||||
* a class, a qualified name, or a value this program declares. A real edge was
|
||||
* lost; the hedge must fire.
|
||||
* - `external` — POSITIVE evidence that the target is outside: the language
|
||||
* itself names the base (or its bare declared type) a built-in. `console.log`,
|
||||
* `fetch(...)`, `JSON.stringify` reach code no index contains, so there is no
|
||||
* node an edge could have pointed at and nothing was lost.
|
||||
* - `external` — POSITIVE evidence that the target is outside. Four kinds, each
|
||||
* supplied by the language provider and each void wherever the program binds
|
||||
* the name itself:
|
||||
* - the base is a name the language owns and the program binds nothing by
|
||||
* (`console.log`, `document.body.appendChild`, `System.out.println`);
|
||||
* - the base is bound only by an unresolved import of a library module
|
||||
* (`os.environ.setdefault`, `std.sort.pdq`);
|
||||
* - the base's bare declared type is a built-in (`String raw` ⇒ `raw.trim()`);
|
||||
* - every step of the receiver chain is typed by a program declaration and the
|
||||
* last one is declared as a library type (`this.data.Keys` with
|
||||
* `Dictionary<string, User> data`) — provided the program declares no member
|
||||
* by the dropped name, which in a language with extension members could be
|
||||
* the real target.
|
||||
* These reach code no index contains, so there is no node an edge could have
|
||||
* pointed at and nothing was lost.
|
||||
* - `unknown` — everything else. An absence of evidence is NOT evidence of
|
||||
* externality: an unannotated parameter (`function f(svc) { svc.a().b(); }`)
|
||||
* is recorded nowhere in the scope model at all, and calling that "external"
|
||||
|
|
@ -275,31 +492,40 @@ export function classifyReceiverOrigin(
|
|||
decoded: DecodedReceiverChain | undefined,
|
||||
inScope: string,
|
||||
receiverName: string,
|
||||
memberName: string,
|
||||
scopes: ScopeResolutionIndexes,
|
||||
options: {
|
||||
/** The language's type-preserving decoration stripper. Without it a Go
|
||||
* pointer receiver — `func (h *Host)` binds `h` to the literal `*Host` —
|
||||
* resolves to no class and the whole method body's drops were reported as
|
||||
* external. Same hook the three receiver-chain lookups in
|
||||
* `compound-receiver.ts` already receive. */
|
||||
readonly stripTypePreservingDecoration?: DecorationStripper;
|
||||
/** `ScopeResolver.resolveQualifiedTypeSpellings`, so a base declared with a
|
||||
* qualified type (`rp::C x`) is classified by the same lookup that typed
|
||||
* (or failed to type) it. */
|
||||
readonly qualifiedTypeSpellings?: boolean;
|
||||
/** `LanguageProvider.isBuiltInName`, threaded through the pass options the
|
||||
* same way `emitFreeCallFallback` receives it. THE only source of positive
|
||||
* external evidence available here; languages that declare no built-in set
|
||||
* simply never produce an `external` verdict, which is the safe default. */
|
||||
readonly isBuiltInName?: (name: string) => boolean;
|
||||
} = {},
|
||||
evidence: ReceiverOriginEvidence = {},
|
||||
): ReceiverOrigin {
|
||||
// The chain's base is authoritative. Without one, take the head of the
|
||||
// receiver text up to the first member/call punctuation.
|
||||
const base = decoded?.baseReceiverName ?? /^[A-Za-z_$][\w$]*/.exec(receiverName)?.[0];
|
||||
if (base === undefined || base.length === 0) return 'unknown';
|
||||
const strip = options.stripTypePreservingDecoration;
|
||||
const isBuiltIn = options.isBuiltInName;
|
||||
const strip = evidence.stripTypePreservingDecoration;
|
||||
const isBuiltIn = evidence.isBuiltInName;
|
||||
|
||||
// A chain the program typed all the way to a library type: every hop was a
|
||||
// declaration the program wrote, and the last names a type it does not have.
|
||||
// Asked first because the base of such a chain is usually in-program (`this`).
|
||||
// The verdict says the member lives on a LIBRARY type, and a program member of
|
||||
// the same name may have been added to that type (an extension method, a
|
||||
// trait impl), so it holds only when the program declares no such name.
|
||||
if (
|
||||
decoded !== undefined &&
|
||||
decoded.steps.length > 0 &&
|
||||
evidence.foldChainPosition !== undefined
|
||||
) {
|
||||
const position = evidence.foldChainPosition(decoded, inScope);
|
||||
if (
|
||||
position !== undefined &&
|
||||
position.def === undefined &&
|
||||
position.declaredType !== undefined &&
|
||||
position.declaredAtScope !== undefined &&
|
||||
namesLibraryType(position.declaredType, position.declaredAtScope, scopes, evidence) &&
|
||||
evidence.programDeclaresName?.(memberName) === false
|
||||
) {
|
||||
return 'external';
|
||||
}
|
||||
}
|
||||
|
||||
// The base's declared TYPE, when it has one, is the strongest signal about
|
||||
// where the member lives: `inputs.stream()` has an in-program base bound to
|
||||
|
|
@ -311,7 +537,7 @@ export function classifyReceiverOrigin(
|
|||
// resolves instead of being mislabelled and dropped from the hedge.
|
||||
if (
|
||||
resolveClassBindingForName(binding.declaredAtScope, binding.rawName, scopes, strip, {
|
||||
qualifiedTypeSpellings: options.qualifiedTypeSpellings,
|
||||
qualifiedTypeSpellings: evidence.qualifiedTypeSpellings,
|
||||
}) !== undefined
|
||||
) {
|
||||
return 'in-program';
|
||||
|
|
@ -340,9 +566,19 @@ export function classifyReceiverOrigin(
|
|||
// same population.
|
||||
if (findValueBindingInScope(inScope, base, scopes) !== undefined) return 'in-program';
|
||||
|
||||
// Positive external evidence, and the only kind reachable from this pass.
|
||||
if (isBuiltIn?.(base) === true) return 'external';
|
||||
|
||||
// Everything below is evidence about the base's NAME, which says nothing once
|
||||
// the program has bound that name itself: a parameter declared `document: Doc`
|
||||
// or left untyped is the program's `document`, not the host's.
|
||||
if (binding !== undefined) return 'unknown';
|
||||
const rooting = baseRooting(inScope, base, scopes, evidence);
|
||||
if (rooting === 'library-import')
|
||||
return evidence.programDeclaresName?.(memberName) === false ? 'external' : 'unknown';
|
||||
// An import the resolver could not follow is the program naming SOMETHING —
|
||||
// possibly a module of its own — so a familiar spelling proves nothing there.
|
||||
if (rooting !== 'unbound') return 'unknown';
|
||||
if (isBuiltIn?.(base) === true || evidence.externalReceiverNames?.has(base) === true) {
|
||||
return evidence.programDeclaresName?.(memberName) === false ? 'external' : 'unknown';
|
||||
}
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
|
|
@ -436,6 +672,12 @@ export function emitReceiverBoundCalls(
|
|||
* degrades a drop's label to `unknown` (the safe direction) and changes no
|
||||
* edge. */
|
||||
readonly isBuiltInName?: (name: string) => boolean;
|
||||
/** `LanguageProvider.externalReceiverNames`. Same channel and same contract
|
||||
* as `isBuiltInName`: read only by `classifyReceiverOrigin`, so unset means
|
||||
* fewer `external` labels and never a different edge. */
|
||||
readonly externalReceiverNames?: ReadonlySet<string>;
|
||||
/** `LanguageProvider.isExternalModuleSpecifier`. Same contract again. */
|
||||
readonly isExternalModuleSpecifier?: (specifier: string) => boolean;
|
||||
/** The generic arguments each heritage clause instantiated its base with,
|
||||
* from the passes that emitted those heritage edges — the inheritance
|
||||
* pre-pass, and the language resolvers that emit their own (Rust `impl T
|
||||
|
|
@ -703,13 +945,22 @@ export function emitReceiverBoundCalls(
|
|||
qualifiedTypeSpellings: provider.resolveQualifiedTypeSpellings === true,
|
||||
resolveQualifiedCallee,
|
||||
};
|
||||
// Loop-invariant: both hooks come off the pass arguments, so the options bag
|
||||
// for `classifyReceiverOrigin` is built once here rather than per dropped site.
|
||||
const receiverOriginOpts = {
|
||||
// Loop-invariant: every hook comes off the pass arguments, so the evidence for
|
||||
// `classifyReceiverOrigin` is built once here rather than per dropped site.
|
||||
// The fold reuses `compoundOpts`, so the classifier never types a chain that
|
||||
// resolution could not have typed.
|
||||
const receiverOriginEvidence = buildReceiverOriginEvidence({
|
||||
graph,
|
||||
parsedFiles,
|
||||
scopes,
|
||||
index,
|
||||
foldOptions: compoundOpts,
|
||||
stripTypePreservingDecoration: provider.stripTypePreservingDecoration,
|
||||
isBuiltInName: options.isBuiltInName,
|
||||
externalReceiverNames: options.externalReceiverNames,
|
||||
isExternalModuleSpecifier: options.isExternalModuleSpecifier,
|
||||
qualifiedTypeSpellings: provider.resolveQualifiedTypeSpellings === true,
|
||||
};
|
||||
});
|
||||
|
||||
// Maps class-like graph ids back to ALL scope definitions that resolved to
|
||||
// them. Same-file partial declarations share one graph id but keep distinct
|
||||
|
|
@ -3289,8 +3540,9 @@ export function emitReceiverBoundCalls(
|
|||
receiverShape: classifyReceiverShape(decodedChain),
|
||||
// Whether anything was actually lost. An external target has no node
|
||||
// to point at, so its absence is completeness, not uncertainty — but
|
||||
// ONLY a positive built-in match may say so. Everything the index
|
||||
// cannot demonstrate stays `unknown` and keeps hedging.
|
||||
// ONLY positive evidence from the language provider may say so.
|
||||
// Everything the index cannot demonstrate stays `unknown` and keeps
|
||||
// hedging.
|
||||
receiverOrigin:
|
||||
receiverNamespace?.callResultOrigin.isDefinite === false
|
||||
? 'unknown'
|
||||
|
|
@ -3298,8 +3550,9 @@ export function emitReceiverBoundCalls(
|
|||
receiverNamespace === undefined ? decodedChain : undefined,
|
||||
receiverNamespace?.callResultOrigin.inScope ?? site.inScope,
|
||||
receiverNamespace?.callResultOrigin.name ?? receiverName,
|
||||
site.name,
|
||||
scopes,
|
||||
receiverOriginOpts,
|
||||
receiverOriginEvidence,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1251,10 +1251,13 @@ export function runScopeResolution(
|
|||
recordSourceBinding,
|
||||
allFilePaths,
|
||||
calleeIdSink: calleeIdAccumulator,
|
||||
// The pass's only source of positive EXTERNAL evidence for a dropped
|
||||
// receiver (`console.log`, `fetch(...)`). Same hook, same spelling as
|
||||
// the `emitFreeCallFallback` wiring below.
|
||||
// The pass's sources of positive EXTERNAL evidence for a dropped
|
||||
// receiver (`console.log`, `document.body`, `import os`). The first is
|
||||
// the same hook, same spelling as the `emitFreeCallFallback` wiring
|
||||
// below; the other two are read by nothing but the origin label.
|
||||
isBuiltInName: provider.languageProvider.isBuiltInName,
|
||||
externalReceiverNames: provider.languageProvider.externalReceiverNames,
|
||||
isExternalModuleSpecifier: provider.languageProvider.isExternalModuleSpecifier,
|
||||
// What each heritage clause instantiated its base with, so the
|
||||
// interface-dispatch fan-out can refuse an incompatible instantiation
|
||||
// (#2912). Empty under `callableFlowOnly`, which emits no dispatch.
|
||||
|
|
@ -1354,6 +1357,8 @@ export function runScopeResolution(
|
|||
calleeIdSink: calleeIdAccumulator,
|
||||
recordSourceBinding,
|
||||
isBuiltInName: provider.languageProvider.isBuiltInName,
|
||||
externalReceiverNames: provider.languageProvider.externalReceiverNames,
|
||||
isExternalModuleSpecifier: provider.languageProvider.isExternalModuleSpecifier,
|
||||
allFilePaths,
|
||||
heritageTypeArguments,
|
||||
unresolvedInheritanceByClass,
|
||||
|
|
|
|||
|
|
@ -130,7 +130,8 @@ export function summarizeUnresolvedReceivers(
|
|||
// auditable and reversible.
|
||||
//
|
||||
// `external` is a POSITIVE determination made by `classifyReceiverOrigin`
|
||||
// from a language built-in match, never a fallthrough: a receiver the
|
||||
// from provider evidence about built-ins, host names, library imports or
|
||||
// library type roots, never a fallthrough: a receiver the
|
||||
// classifier could not place lands in `unknown`, which counts here WITH
|
||||
// `in-program`, because assuming a completeness we cannot demonstrate is
|
||||
// the unsafe direction.
|
||||
|
|
|
|||
|
|
@ -17,7 +17,11 @@
|
|||
* must flag that the true blast radius is higher.
|
||||
*/
|
||||
import { describe, it, expect, beforeAll, beforeEach, vi } from 'vitest';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { LocalBackend } from '../../src/mcp/local/local-backend.js';
|
||||
import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js';
|
||||
import { listRegisteredRepos, loadMeta } from '../../src/storage/repo-manager.js';
|
||||
import {
|
||||
MAX_UNRESOLVED_RECEIVER_MEMBERS,
|
||||
|
|
@ -325,6 +329,61 @@ withTestLbugDB(
|
|||
);
|
||||
});
|
||||
|
||||
// The summaries above are hand-built. This one comes from a real analysis,
|
||||
// so the verdict is the classifier's, end to end: a drop rooted at a host
|
||||
// global must leave `impact()` exact, while the same member dropped on an
|
||||
// untyped value — or on a parameter that merely shares the global's name —
|
||||
// must still make it a lower bound (#3498 triage).
|
||||
it.each([
|
||||
[
|
||||
'a drop rooted at a library global',
|
||||
'exact',
|
||||
'export function render(): void {\n document.body.formatDate();\n}\n',
|
||||
{ receiverTyping: 0, externalBoundary: 1 },
|
||||
],
|
||||
[
|
||||
'a drop on an untyped parameter',
|
||||
'lower-bound',
|
||||
'export function render(svc): void {\n svc.load().formatDate();\n}\n',
|
||||
{ receiverTyping: 1, externalBoundary: 0 },
|
||||
],
|
||||
[
|
||||
'a drop on a parameter named like the library global',
|
||||
'lower-bound',
|
||||
'export function render(document: Doc): void {\n document.body.formatDate();\n}\n',
|
||||
{ receiverTyping: 1, externalBoundary: 0 },
|
||||
],
|
||||
] as const)(
|
||||
'impact() reads %s from an analysed summary as %s',
|
||||
async (_label, epistemic, source, causes) => {
|
||||
const repoDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-epistemic-origin-'));
|
||||
try {
|
||||
fs.mkdirSync(path.join(repoDir, 'src'), { recursive: true });
|
||||
fs.writeFileSync(path.join(repoDir, 'src', 'caller.ts'), source, 'utf8');
|
||||
const analysed = await runPipelineFromRepo(repoDir, () => {}, {});
|
||||
const summary = JSON.parse(
|
||||
JSON.stringify(summarizeUnresolvedReceivers(analysed.resolutionOutcomes)),
|
||||
);
|
||||
vi.mocked(loadMeta).mockResolvedValue({
|
||||
scopeExtractionReceipt: 1,
|
||||
unresolvedReceiverMembers: summary,
|
||||
} as Awaited<ReturnType<typeof loadMeta>>);
|
||||
|
||||
const result = await backend.callTool('impact', {
|
||||
target: 'formatDate',
|
||||
file_path: 'src/util.ts',
|
||||
direction: 'upstream',
|
||||
});
|
||||
|
||||
expect(result.epistemic).toBe(epistemic);
|
||||
expect(result.causes).toMatchObject(causes);
|
||||
} finally {
|
||||
fs.rmSync(repoDir, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
60000,
|
||||
);
|
||||
|
||||
it('marks impact as a lower bound when scope extraction omitted files', async () => {
|
||||
vi.mocked(loadMeta).mockResolvedValue({
|
||||
repoPath: '/test/repo',
|
||||
|
|
|
|||
|
|
@ -103,9 +103,12 @@ def rejected_callers():
|
|||
parameterized().target()
|
||||
m = rebound()
|
||||
m.target()
|
||||
def positive_external():
|
||||
def builtin_member_collision():
|
||||
m = list()
|
||||
m.target()
|
||||
def positive_external():
|
||||
m = list()
|
||||
m.count(0)
|
||||
`,
|
||||
'pkg/unsupported.py': `def mixed():
|
||||
from pkg import facade
|
||||
|
|
@ -242,14 +245,16 @@ def existing():
|
|||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('keeps rejected namespace proofs observable and positively external calls separate', () => {
|
||||
it('keeps namespace failures and library member collisions uncertain, with external calls separate', () => {
|
||||
const summary = summarizeUnresolvedReceivers(
|
||||
result.resolutionOutcomes.filter((outcome) => outcome.filePath === 'pkg/rejected.py'),
|
||||
);
|
||||
expect(summary).toMatchObject({
|
||||
counts: { target: 5 },
|
||||
totalSites: 5,
|
||||
externalCounts: { target: 1 },
|
||||
// The workspace declares target(), so list().target() must keep hedging.
|
||||
// count() has no program declaration and supplies genuine library evidence.
|
||||
counts: { target: 6 },
|
||||
totalSites: 6,
|
||||
externalCounts: { count: 1 },
|
||||
externalSites: 1,
|
||||
});
|
||||
expect(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,166 @@
|
|||
/**
|
||||
* Library and runtime receivers name the program boundary (#3498 triage).
|
||||
*
|
||||
* A call whose receiver is a host global (`document`), a stdlib import (`os`,
|
||||
* `std`), a platform package root (`java.util`) or a value declared as a library
|
||||
* type (`Dictionary<…>`) reaches code no index contains. Its drop is
|
||||
* completeness, not uncertainty, so it must be recorded `external` — and the
|
||||
* same spelling must keep hedging wherever the program binds the name itself.
|
||||
*
|
||||
* The classifier unit tests (`unresolved-receivers.test.ts`) pin the verdicts on
|
||||
* real scope extraction. This exercises the wiring they deliberately skip: each
|
||||
* provider's data reaching the pass through `runScopeResolution`, for every
|
||||
* language that supplies it, in one multi-language repository.
|
||||
*/
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import {
|
||||
getResolutionOutcomes,
|
||||
runPipelineFromRepo,
|
||||
writeFixtureRepo,
|
||||
type PipelineResult,
|
||||
} from './helpers.js';
|
||||
|
||||
const FILES: Record<string, string> = {
|
||||
'web/dom.ts': `export function attach(link: unknown): void {
|
||||
document.body.appendChild(link);
|
||||
}
|
||||
|
||||
export function navigate(path: string): void {
|
||||
window.history.pushState(null, '', path);
|
||||
}
|
||||
|
||||
export function decode(token: string): string {
|
||||
return Buffer.from(token, 'base64').toString();
|
||||
}
|
||||
|
||||
export function typedParameter(document: Doc): void {
|
||||
document.body.render();
|
||||
}
|
||||
`,
|
||||
'web/interop.js': `export function invoke() { window.app.interopSave(); }\n`,
|
||||
'web/program.ts': `export class Service { interopSave(): void {} }\n`,
|
||||
'web/env.ts': `export const API_BASE = process.env.API_BASE || '';
|
||||
export const ENDPOINT = process.env.SERVICE_URL || '';
|
||||
`,
|
||||
'py/settings.py': `import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def configure():
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
|
||||
|
||||
|
||||
def base_dir():
|
||||
return Path(__file__).resolve()
|
||||
|
||||
|
||||
def shadowed(os):
|
||||
os.environ.update()
|
||||
`,
|
||||
'zig/src/sorter.zig': `const std = @import("std");
|
||||
|
||||
pub fn sortItems(items: []u32) void {
|
||||
std.sort.pdq(u32, items, {}, lessThan);
|
||||
}
|
||||
|
||||
fn lessThan(_: void, a: u32, b: u32) bool {
|
||||
return a < b;
|
||||
}
|
||||
`,
|
||||
'java/src/App.java': `public class App {
|
||||
public void run() {
|
||||
java.util.Optional<String> opt = java.util.Optional.of("x");
|
||||
}
|
||||
}
|
||||
`,
|
||||
'cs/App.cs': `using System.Collections.Generic;
|
||||
|
||||
namespace Demo
|
||||
{
|
||||
public class User { }
|
||||
|
||||
public class App
|
||||
{
|
||||
private Dictionary<string, User> data = new Dictionary<string, User>();
|
||||
|
||||
public void Greet()
|
||||
{
|
||||
System.Console.WriteLine("hello");
|
||||
}
|
||||
|
||||
public void ReadKeys()
|
||||
{
|
||||
foreach (var key in this.data.Keys) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
'kt/src/Worker.kt': `class Worker {
|
||||
fun run() {
|
||||
System.out.println("working")
|
||||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
describe('Library and runtime receivers name the program boundary (#3498)', () => {
|
||||
let repoDir: string;
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
repoDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-receiver-origin-library-'));
|
||||
writeFixtureRepo(repoDir, FILES);
|
||||
result = await runPipelineFromRepo(repoDir, () => {}, {});
|
||||
}, 120000);
|
||||
|
||||
afterAll(() => {
|
||||
if (repoDir !== undefined) fs.rmSync(repoDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
const dropsAt = (filePath: string, line: number, name: string) =>
|
||||
getResolutionOutcomes(result).filter(
|
||||
(outcome) =>
|
||||
outcome.kind === 'suppressed' &&
|
||||
outcome.reason === 'receiver-unresolved' &&
|
||||
outcome.filePath === filePath &&
|
||||
outcome.range.startLine === line &&
|
||||
outcome.name === name,
|
||||
);
|
||||
|
||||
it.each([
|
||||
['a DOM global', 'web/dom.ts', 2, 'appendChild'],
|
||||
['a browser global', 'web/dom.ts', 6, 'pushState'],
|
||||
['the Node Buffer global', 'web/dom.ts', 10, 'toString'],
|
||||
['the Node process global', 'web/env.ts', 2, 'SERVICE_URL'],
|
||||
['a Python stdlib namespace import', 'py/settings.py', 6, 'setdefault'],
|
||||
['a Python stdlib named import', 'py/settings.py', 10, 'resolve'],
|
||||
['the Zig standard library', 'zig/src/sorter.zig', 4, 'pdq'],
|
||||
['the Java platform package root', 'java/src/App.java', 3, 'of'],
|
||||
['the C# System namespace', 'cs/App.cs', 13, 'WriteLine'],
|
||||
['a C# field declared as a library type', 'cs/App.cs', 18, 'Keys'],
|
||||
['Kotlin java.lang.System', 'kt/src/Worker.kt', 3, 'println'],
|
||||
] as const)('records a drop rooted at %s as external', (_label, filePath, line, name) => {
|
||||
const drops = dropsAt(filePath, line, name);
|
||||
expect(drops.length).toBeGreaterThan(0);
|
||||
expect(drops).toEqual(drops.map(() => expect.objectContaining({ receiverOrigin: 'external' })));
|
||||
});
|
||||
|
||||
it.each([
|
||||
['a TypeScript parameter named document', 'web/dom.ts', 14, 'render'],
|
||||
['a Python parameter named os', 'py/settings.py', 14, 'update'],
|
||||
['a same-named program declaration', 'web/env.ts', 1, 'API_BASE'],
|
||||
[
|
||||
'a TypeScript member used through a JavaScript library receiver',
|
||||
'web/interop.js',
|
||||
1,
|
||||
'interopSave',
|
||||
],
|
||||
] as const)('keeps a drop on %s hedged', (_label, filePath, line, name) => {
|
||||
const drops = dropsAt(filePath, line, name);
|
||||
expect(drops.length).toBeGreaterThan(0);
|
||||
expect(drops).not.toContainEqual(expect.objectContaining({ receiverOrigin: 'external' }));
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
import { it, expect } from 'vitest';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import {
|
||||
writeFixtureRepo,
|
||||
runPipelineFromRepo,
|
||||
getResolutionOutcomes,
|
||||
getRelationships,
|
||||
} from './helpers.js';
|
||||
const cases = [
|
||||
[
|
||||
'Node process import in Vue',
|
||||
'App.vue',
|
||||
`<script lang="ts">
|
||||
import process from 'node:process';
|
||||
export const CURRENT = process.env.RUNTIME_TEST_KEY;
|
||||
export default {};
|
||||
</script>`,
|
||||
'RUNTIME_TEST_KEY',
|
||||
true,
|
||||
],
|
||||
[
|
||||
'unknown import named process in Vue',
|
||||
'App.vue',
|
||||
`<script lang="ts">
|
||||
import process from 'not-node-runtime';
|
||||
export const CURRENT = process.env.RUNTIME_TEST_KEY;
|
||||
export default {};
|
||||
</script>`,
|
||||
'RUNTIME_TEST_KEY',
|
||||
false,
|
||||
],
|
||||
[
|
||||
'Node process import in TypeScript',
|
||||
'main.ts',
|
||||
"import process from 'node:process';\nexport const CURRENT = process.env.RUNTIME_TEST_KEY;\n",
|
||||
'RUNTIME_TEST_KEY',
|
||||
true,
|
||||
],
|
||||
[
|
||||
'Node process import in JavaScript',
|
||||
'main.js',
|
||||
"import runtime from 'process';\nexport const CURRENT = runtime.env.RUNTIME_TEST_KEY;\n",
|
||||
'RUNTIME_TEST_KEY',
|
||||
true,
|
||||
],
|
||||
[
|
||||
'unknown import named process',
|
||||
'main.ts',
|
||||
"import process from 'not-node-runtime';\nexport const CURRENT = process.env.RUNTIME_TEST_KEY;\n",
|
||||
'RUNTIME_TEST_KEY',
|
||||
false,
|
||||
],
|
||||
...[
|
||||
'Dictionary',
|
||||
'SortedDictionary',
|
||||
'SortedList',
|
||||
'ConcurrentDictionary',
|
||||
'ReadOnlyDictionary',
|
||||
'IDictionary',
|
||||
'IReadOnlyDictionary',
|
||||
].map(
|
||||
(type) =>
|
||||
[
|
||||
type,
|
||||
'main.cs',
|
||||
`using System.Collections.Generic;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.ObjectModel;
|
||||
class User {}
|
||||
class Service {
|
||||
private ${type}<string,User> data;
|
||||
public void Run() { foreach (var key in this.data.Keys) {} }
|
||||
}
|
||||
`,
|
||||
'Keys',
|
||||
true,
|
||||
] as const,
|
||||
),
|
||||
] as const;
|
||||
it.each(cases)(
|
||||
'%s uses provider evidence without overriding program claims',
|
||||
async (_label, file, source, member, external) => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'origin-provider-review-'));
|
||||
try {
|
||||
writeFixtureRepo(root, { [file]: source });
|
||||
const result = await runPipelineFromRepo(root, () => {});
|
||||
const drops = getResolutionOutcomes(result).filter(
|
||||
(o) => o.kind === 'suppressed' && o.reason === 'receiver-unresolved' && o.name === member,
|
||||
);
|
||||
expect(drops.length).toBeGreaterThan(0);
|
||||
if (external)
|
||||
expect(drops.every((o) => o.kind === 'suppressed' && o.receiverOrigin === 'external')).toBe(
|
||||
true,
|
||||
);
|
||||
else
|
||||
expect(drops.some((o) => o.kind === 'suppressed' && o.receiverOrigin === 'external')).toBe(
|
||||
false,
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
180000,
|
||||
);
|
||||
|
||||
it('keeps a program collection authoritative', async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'origin-program-collection-'));
|
||||
try {
|
||||
writeFixtureRepo(root, {
|
||||
'main.cs': `class SortedDictionary<K,V> { public string[] Keys; }
|
||||
class User {}
|
||||
class Service {
|
||||
private SortedDictionary<string,User> data;
|
||||
public void Run() { foreach (var key in this.data.Keys) {} }
|
||||
}
|
||||
`,
|
||||
});
|
||||
const r = await runPipelineFromRepo(root, () => {});
|
||||
expect(
|
||||
getRelationships(r, 'ACCESSES').filter(
|
||||
(e) => e.target === 'Keys' && e.targetFilePath === 'main.cs',
|
||||
).length,
|
||||
).toBeGreaterThan(0);
|
||||
expect(
|
||||
getResolutionOutcomes(r).some(
|
||||
(o) => o.kind === 'suppressed' && o.name === 'Keys' && o.receiverOrigin === 'external',
|
||||
),
|
||||
).toBe(false);
|
||||
} finally {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
}, 180000);
|
||||
|
|
@ -17,6 +17,7 @@ import type {
|
|||
ResolutionOutcome,
|
||||
} from '../../../src/core/ingestion/scope-resolution/resolution-outcome.js';
|
||||
import {
|
||||
buildReceiverOriginEvidence,
|
||||
classifyReceiverOrigin,
|
||||
recordSiteMember,
|
||||
type EarlierSiteAtKey,
|
||||
|
|
@ -25,6 +26,10 @@ import { decodeReceiverChain } from '../../../src/core/ingestion/utils/receiver-
|
|||
import { typescriptScopeResolver } from '../../../src/core/ingestion/languages/typescript/scope-resolver.js';
|
||||
import { goScopeResolver } from '../../../src/core/ingestion/languages/go/scope-resolver.js';
|
||||
import { javaScopeResolver } from '../../../src/core/ingestion/languages/java/scope-resolver.js';
|
||||
import { pythonScopeResolver } from '../../../src/core/ingestion/languages/python/scope-resolver.js';
|
||||
import { zigScopeResolver } from '../../../src/core/ingestion/languages/zig/scope-resolver.js';
|
||||
import { csharpScopeResolver } from '../../../src/core/ingestion/languages/csharp/scope-resolver.js';
|
||||
import { kotlinScopeResolver } from '../../../src/core/ingestion/languages/kotlin/scope-resolver.js';
|
||||
import { buildScopeModel, type ScopeModelFixture } from '../../helpers/scope-model.js';
|
||||
|
||||
const range = { startLine: 1, startCol: 0, endLine: 1, endCol: 1 };
|
||||
|
|
@ -242,7 +247,7 @@ describe('summarizeUnresolvedReceivers origin routing', () => {
|
|||
|
||||
/** The provider-hook bag `classifyReceiverOrigin` reads. Derived from the
|
||||
* function so the two cannot drift. */
|
||||
type OriginHooks = Parameters<typeof classifyReceiverOrigin>[4];
|
||||
type OriginHooks = Parameters<typeof classifyReceiverOrigin>[5];
|
||||
|
||||
/** Classify the receiver of the (unique) reference site invoking `memberName`
|
||||
* under `hooks`, through exactly the arguments the pass threads at its drop
|
||||
|
|
@ -252,26 +257,47 @@ function classifyOriginOf(
|
|||
memberName: string,
|
||||
hooks: OriginHooks,
|
||||
): ReceiverOrigin {
|
||||
const site = fixture.sites.find(
|
||||
(candidate) => candidate.name === memberName && candidate.explicitReceiver !== undefined,
|
||||
);
|
||||
expect(site).toBeDefined();
|
||||
const site =
|
||||
fixture.sites.find(
|
||||
(candidate) => candidate.name === memberName && candidate.explicitReceiver !== undefined,
|
||||
) ?? expect.unreachable(`no receiver site invokes \`${memberName}\``);
|
||||
const receiver =
|
||||
site.explicitReceiver ?? expect.unreachable(`\`${memberName}\` has no explicit receiver`);
|
||||
return classifyReceiverOrigin(
|
||||
decodeReceiverChain(site!.receiverChain),
|
||||
site!.inScope,
|
||||
site!.explicitReceiver!.name,
|
||||
decodeReceiverChain(site.receiverChain),
|
||||
site.inScope,
|
||||
receiver.name,
|
||||
site.name,
|
||||
fixture.scopes,
|
||||
hooks,
|
||||
);
|
||||
}
|
||||
|
||||
/** The normal path: the language's own contract hooks, exactly as the pass
|
||||
* supplies them. */
|
||||
/** The normal path: the language's own contract hooks, built by the same
|
||||
* factory the pass calls. The fold flags mirror the ones the pass hands
|
||||
* `resolveCompoundReceiverClass`, read off the resolver. */
|
||||
function originOf(fixture: ScopeModelFixture, memberName: string): ReceiverOrigin {
|
||||
return classifyOriginOf(fixture, memberName, {
|
||||
stripTypePreservingDecoration: fixture.resolver.stripTypePreservingDecoration,
|
||||
isBuiltInName: fixture.resolver.languageProvider.isBuiltInName,
|
||||
});
|
||||
const { resolver } = fixture;
|
||||
return classifyOriginOf(
|
||||
fixture,
|
||||
memberName,
|
||||
buildReceiverOriginEvidence({
|
||||
parsedFiles: [fixture.parsed],
|
||||
scopes: fixture.scopes,
|
||||
index: fixture.index,
|
||||
foldOptions: {
|
||||
fieldFallback: resolver.fieldFallbackOnMethodLookup ?? true,
|
||||
elementTypeOf: resolver.elementTypeOf,
|
||||
hoistTypeBindingsToModule: resolver.hoistTypeBindingsToModule === true,
|
||||
stripTypePreservingDecoration: resolver.stripTypePreservingDecoration,
|
||||
resolveThisViaEnclosingClass: resolver.resolveThisViaEnclosingClass,
|
||||
},
|
||||
stripTypePreservingDecoration: resolver.stripTypePreservingDecoration,
|
||||
isBuiltInName: resolver.languageProvider.isBuiltInName,
|
||||
externalReceiverNames: resolver.languageProvider.externalReceiverNames,
|
||||
isExternalModuleSpecifier: resolver.languageProvider.isExternalModuleSpecifier,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** The degradation path: the same site with the language's provider hooks
|
||||
|
|
@ -361,6 +387,7 @@ public class Probe {
|
|||
names.iterator();
|
||||
List.of("a");
|
||||
Map.entry("a", "b");
|
||||
Set.copyOf(names);
|
||||
Helper.assist();
|
||||
svc.loadUser();
|
||||
repo.findAll();
|
||||
|
|
@ -461,13 +488,21 @@ describe('classifyReceiverOrigin', () => {
|
|||
expect(originOf(javaFixture, 'findAll')).toBe('unknown');
|
||||
});
|
||||
|
||||
it('hedges a platform name deliberately kept OUT of the set', () => {
|
||||
// `Map.entry(...)` is the exact syntactic twin of the `List.of(...)` above;
|
||||
// the only thing separating the two verdicts is set membership. `Map` is a
|
||||
// name applications really do declare (and in Java a same-package type needs
|
||||
// no import to shadow it), so it stays out and its drops keep hedging. This
|
||||
// pins the under-inclusion choice as a choice, not an oversight.
|
||||
expect(originOf(javaFixture, 'entry')).toBe('unknown');
|
||||
it('hedges an unimported platform name deliberately kept OUT of the set', () => {
|
||||
// `Set.copyOf(...)` is the exact syntactic twin of the `List.of(...)` above,
|
||||
// and nothing in the file imports `Set`. `Set` is a name applications really
|
||||
// do declare, and in Java an unimported one can only be a type of this
|
||||
// package, so it stays out and its drops keep hedging. This pins the
|
||||
// under-inclusion choice as a choice, not an oversight.
|
||||
expect(originOf(javaFixture, 'copyOf')).toBe('unknown');
|
||||
});
|
||||
|
||||
it('reads an explicit platform import as evidence whatever the set says', () => {
|
||||
// `import java.util.Map` shadows any same-package `Map` (JLS §6.4.1) and can
|
||||
// never resolve in-workspace, so the import itself places `Map.entry(...)`
|
||||
// in the JDK. Before the import counted, this drop hedged on set membership
|
||||
// alone.
|
||||
expect(originOf(javaFixture, 'entry')).toBe('external');
|
||||
});
|
||||
|
||||
it('degrades every Java verdict to unknown when the hook is withheld', () => {
|
||||
|
|
@ -476,6 +511,350 @@ describe('classifyReceiverOrigin', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Library and runtime receivers (#3498 triage).
|
||||
//
|
||||
// Every row below is a receiver the language's runtime or standard library
|
||||
// supplies — a host global, a stdlib import, a platform package root, or a
|
||||
// value whose declared type is a library type — next to a control where the
|
||||
// program binds the same name itself. The control is the point: positive
|
||||
// evidence means the program declares nothing by that name at the site, so a
|
||||
// parameter called `document`, a local `class System` or an unresolved import
|
||||
// from a non-library module must keep hedging. Built from real extraction for
|
||||
// the reason given above: what a binding contains is exactly what intuition
|
||||
// gets wrong.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const tsLibraryFixture = buildScopeModel(
|
||||
typescriptScopeResolver,
|
||||
`export function viaDocument(): void {
|
||||
document.body.appendChild(null);
|
||||
}
|
||||
|
||||
export function viaWindow(): void {
|
||||
window.history.pushState(null, '', '/');
|
||||
}
|
||||
|
||||
export function viaBuffer(token: string): string {
|
||||
return Buffer.from(token, 'base64').toString();
|
||||
}
|
||||
|
||||
export function viaProcess(): string | undefined {
|
||||
return process.env.HOME;
|
||||
}
|
||||
|
||||
// The program binds the name at the site: never the host global.
|
||||
export function typedParameter(document: Doc): void {
|
||||
document.body.render();
|
||||
}
|
||||
|
||||
export function untypedParameter(window): void {
|
||||
window.history.back();
|
||||
}
|
||||
|
||||
export function localValue(): void {
|
||||
const navigator = makeNavigator();
|
||||
navigator.clipboard.writeText('x');
|
||||
}
|
||||
`,
|
||||
'main.ts',
|
||||
);
|
||||
|
||||
// A program class by a host-global name shadows the host global everywhere it
|
||||
// is visible. Its own file, because the class is visible module-wide.
|
||||
const tsShadowFixture = buildScopeModel(
|
||||
typescriptScopeResolver,
|
||||
`export class Buffer {
|
||||
static from(raw: string): Buffer {
|
||||
return new Buffer();
|
||||
}
|
||||
}
|
||||
|
||||
export function viaProgramBuffer(raw: string): void {
|
||||
Buffer.from(raw).encode();
|
||||
}
|
||||
`,
|
||||
'buffer.ts',
|
||||
);
|
||||
|
||||
// TypeScript stores method return types on the MODULE scope, keyed by method
|
||||
// name alone. `Status` declares no `name()`; the module's `name` binding is
|
||||
// `Labels.name`'s. Reading it would type `this.name()` as a library `String`
|
||||
// and call an in-program receiver external.
|
||||
const tsHoistedFixture = buildScopeModel(
|
||||
typescriptScopeResolver,
|
||||
`export class Labels {
|
||||
name(): String {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export class Status {
|
||||
label(): void {
|
||||
this.name().toLowerCase();
|
||||
}
|
||||
}
|
||||
`,
|
||||
'status.ts',
|
||||
);
|
||||
|
||||
const pythonLibraryFixture = buildScopeModel(
|
||||
pythonScopeResolver,
|
||||
`import os
|
||||
from pathlib import Path
|
||||
from vendorlib import Client
|
||||
from .compat import json
|
||||
|
||||
|
||||
def via_os():
|
||||
os.environ.setdefault("A", "b")
|
||||
|
||||
|
||||
def via_path():
|
||||
return Path(__file__).resolve()
|
||||
|
||||
|
||||
def via_third_party():
|
||||
Client().session.close()
|
||||
|
||||
|
||||
def via_relative():
|
||||
json.codec.dumps({})
|
||||
|
||||
|
||||
def via_parameter(os):
|
||||
os.environ.update()
|
||||
`,
|
||||
'app/main.py',
|
||||
);
|
||||
|
||||
const zigLibraryFixture = buildScopeModel(
|
||||
zigScopeResolver,
|
||||
`const std = @import("std");
|
||||
const vendored = @import("std.zig");
|
||||
|
||||
pub fn sortItems(items: []u32) void {
|
||||
std.sort.pdq(u32, items, {}, lessThan);
|
||||
}
|
||||
|
||||
pub fn viaVendored() void {
|
||||
vendored.sort.insertion();
|
||||
}
|
||||
|
||||
fn lessThan(_: void, a: u32, b: u32) bool {
|
||||
return a < b;
|
||||
}
|
||||
`,
|
||||
'src/main.zig',
|
||||
);
|
||||
|
||||
const javaLibraryFixture = buildScopeModel(
|
||||
javaScopeResolver,
|
||||
`public class Probe {
|
||||
public void qualified() {
|
||||
java.util.Optional.of("x");
|
||||
}
|
||||
|
||||
public void shadowed(Holder java) {
|
||||
java.util.run();
|
||||
}
|
||||
}
|
||||
|
||||
class Holder {
|
||||
Util util;
|
||||
}
|
||||
|
||||
class Util {
|
||||
void run() {}
|
||||
}
|
||||
`,
|
||||
'src/Probe.java',
|
||||
);
|
||||
|
||||
const csharpLibraryFixture = buildScopeModel(
|
||||
csharpScopeResolver,
|
||||
`using System.Collections.Generic;
|
||||
|
||||
namespace App
|
||||
{
|
||||
public class User { }
|
||||
|
||||
public class Registry
|
||||
{
|
||||
public int Count;
|
||||
}
|
||||
|
||||
public static class DictionaryExtensions
|
||||
{
|
||||
public static int Size(this Dictionary<string, User> d) { return 0; }
|
||||
}
|
||||
|
||||
public class Probe
|
||||
{
|
||||
private Dictionary<string, User> data = new Dictionary<string, User>();
|
||||
private Registry registry = new Registry();
|
||||
|
||||
public void Platform()
|
||||
{
|
||||
System.Console.WriteLine("x");
|
||||
}
|
||||
|
||||
public void ReadKeys()
|
||||
{
|
||||
foreach (var key in this.data.Keys) { }
|
||||
}
|
||||
|
||||
public void ReadProgramField()
|
||||
{
|
||||
foreach (var item in this.registry.Items) { }
|
||||
}
|
||||
|
||||
public void CallExtension()
|
||||
{
|
||||
this.data.Size();
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
'App/Probe.cs',
|
||||
);
|
||||
|
||||
const csharpShadowFixture = buildScopeModel(
|
||||
csharpScopeResolver,
|
||||
`namespace App
|
||||
{
|
||||
public class Printer
|
||||
{
|
||||
public void Print(string s) { }
|
||||
}
|
||||
|
||||
public class System
|
||||
{
|
||||
public static Printer Console = new Printer();
|
||||
}
|
||||
|
||||
public class Probe
|
||||
{
|
||||
public void Run()
|
||||
{
|
||||
System.Console.WriteLine("x");
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
'App/Shadow.cs',
|
||||
);
|
||||
|
||||
const kotlinLibraryFixture = buildScopeModel(
|
||||
kotlinScopeResolver,
|
||||
`class Probe {
|
||||
fun platform() {
|
||||
System.out.println("x")
|
||||
}
|
||||
}
|
||||
`,
|
||||
'src/Probe.kt',
|
||||
);
|
||||
|
||||
const kotlinShadowFixture = buildScopeModel(
|
||||
kotlinScopeResolver,
|
||||
`class Printer {
|
||||
fun write(s: String) {}
|
||||
}
|
||||
|
||||
object System {
|
||||
val out = Printer()
|
||||
}
|
||||
|
||||
class Probe {
|
||||
fun run() {
|
||||
System.out.println("x")
|
||||
}
|
||||
}
|
||||
`,
|
||||
'src/Shadow.kt',
|
||||
);
|
||||
|
||||
describe('classifyReceiverOrigin — library and runtime receivers', () => {
|
||||
it.each([
|
||||
[
|
||||
'program global properties',
|
||||
typescriptScopeResolver,
|
||||
'export class Service { save() {} } export function run() { globalThis.service.save(); }',
|
||||
'save',
|
||||
'caller.ts',
|
||||
],
|
||||
[
|
||||
'program members reached through window',
|
||||
typescriptScopeResolver,
|
||||
'export class Service { save() {} } export function run() { window.service.save(); }',
|
||||
'save',
|
||||
'caller.ts',
|
||||
],
|
||||
[
|
||||
'a library function returning a program instance',
|
||||
pythonScopeResolver,
|
||||
'import copy\nclass User:\n def save(self): pass\ndef run(user):\n copy.copy(user).save()\n',
|
||||
'save',
|
||||
'caller.py',
|
||||
],
|
||||
[
|
||||
'a blocked parameter and library import claim',
|
||||
pythonScopeResolver,
|
||||
'def run(os):\n os.child.missing()\n import os\n',
|
||||
'missing',
|
||||
'caller.py',
|
||||
],
|
||||
] as const)('keeps %s hedged', (_name, resolver, source, member, file) => {
|
||||
expect(originOf(buildScopeModel(resolver, source, file), member)).not.toBe('external');
|
||||
});
|
||||
it.each([
|
||||
['TypeScript document', tsLibraryFixture, 'appendChild'],
|
||||
['TypeScript window', tsLibraryFixture, 'pushState'],
|
||||
['TypeScript Buffer', tsLibraryFixture, 'toString'],
|
||||
['TypeScript process', tsLibraryFixture, 'HOME'],
|
||||
['Python stdlib namespace import', pythonLibraryFixture, 'setdefault'],
|
||||
['Python stdlib named import', pythonLibraryFixture, 'resolve'],
|
||||
['Zig std import', zigLibraryFixture, 'pdq'],
|
||||
['Java platform package root', javaLibraryFixture, 'of'],
|
||||
['C# System namespace', csharpLibraryFixture, 'WriteLine'],
|
||||
['C# field declared as a library type', csharpLibraryFixture, 'Keys'],
|
||||
['Kotlin java.lang.System', kotlinLibraryFixture, 'println'],
|
||||
] as const)('reports %s as external', (_label, fixture, memberName) => {
|
||||
expect(originOf(fixture, memberName)).toBe('external');
|
||||
});
|
||||
|
||||
it.each([
|
||||
['a TypeScript parameter named document', 'unknown', tsLibraryFixture, 'render'],
|
||||
['an untyped TypeScript parameter named window', 'unknown', tsLibraryFixture, 'back'],
|
||||
['a TypeScript local named navigator', 'in-program', tsLibraryFixture, 'writeText'],
|
||||
['a TypeScript class named Buffer', 'in-program', tsShadowFixture, 'encode'],
|
||||
["a step typed only by another class's member", 'in-program', tsHoistedFixture, 'toLowerCase'],
|
||||
['an unresolved third-party Python import', 'unknown', pythonLibraryFixture, 'close'],
|
||||
['an unresolved relative Python import', 'unknown', pythonLibraryFixture, 'dumps'],
|
||||
['a Python parameter named os', 'unknown', pythonLibraryFixture, 'update'],
|
||||
['a Zig path import spelled like std', 'unknown', zigLibraryFixture, 'insertion'],
|
||||
['a Java parameter named java', 'in-program', javaLibraryFixture, 'run'],
|
||||
['a C# field of a program type', 'in-program', csharpLibraryFixture, 'Items'],
|
||||
['a C# extension method the program declares', 'in-program', csharpLibraryFixture, 'Size'],
|
||||
['a C# class named System', 'in-program', csharpShadowFixture, 'WriteLine'],
|
||||
['a Kotlin object named System', 'in-program', kotlinShadowFixture, 'println'],
|
||||
] as const)('keeps %s %s', (_label, expected, fixture, memberName) => {
|
||||
expect(originOf(fixture, memberName)).toBe(expected);
|
||||
});
|
||||
|
||||
// The evidence lives in the provider. A language that supplies none of it
|
||||
// must never reach an `external` verdict through these routes.
|
||||
it.each([
|
||||
['TypeScript document', tsLibraryFixture, 'appendChild'],
|
||||
['Python stdlib import', pythonLibraryFixture, 'setdefault'],
|
||||
['Zig std import', zigLibraryFixture, 'pdq'],
|
||||
['C# library-typed field', csharpLibraryFixture, 'Keys'],
|
||||
] as const)('degrades %s to non-external without the provider hooks', (_label, fixture, name) => {
|
||||
expect(originOfWithoutHooks(fixture, name)).not.toBe('external');
|
||||
});
|
||||
});
|
||||
|
||||
describe('classifyReceiverShape', () => {
|
||||
it('reports no-chain when the site carried no chain', () => {
|
||||
expect(classifyReceiverShape(undefined)).toBe('no-chain');
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue