candor-ts 0.5.18 → 0.5.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/scan.mjs +269 -7
package/package.json
CHANGED
package/scan.mjs
CHANGED
|
@@ -347,6 +347,20 @@ const nodeName = new WeakMap(); // declaration node -> qualified name
|
|
|
347
347
|
const entityTables = new Map(); // ClassDeclaration node -> table name
|
|
348
348
|
const interfaceImpls = new Map(); // InterfaceDeclaration node -> implementing ClassDeclarations (CHA universe)
|
|
349
349
|
const classOverrides = new Map(); // base-method MemberDeclaration node -> overriding subclass member nodes (class-CHA)
|
|
350
|
+
// `Object.defineProperty(target, key, { get/set })` runtime accessors (the silent-pure defineProperty
|
|
351
|
+
// hole): the TS checker types `target.key` as a plain DATA property (defineProperty is a runtime
|
|
352
|
+
// construct), so `accessorAt` finds no get-accessor and the forcing site `target.key` reads
|
|
353
|
+
// silent-pure. We index, keyed by the TARGET's symbol → key string → { get, set } descriptor function
|
|
354
|
+
// node, every such accessor seen in the project. The forcing-site arm consults this when the type-level
|
|
355
|
+
// accessor resolution comes up empty (precise edge when target+key resolve; else honest Unknown).
|
|
356
|
+
const definePropAccessors = new Map(); // targetSymbol -> Map(key -> { get?: fnNode, set?: fnNode })
|
|
357
|
+
// A descriptor accessor with a COMPUTED key (`Object.defineProperty(o, k, {get})`) on a RESOLVABLE
|
|
358
|
+
// target: the target symbol is known but the key isn't, so a forcing site `o.anything` MIGHT hit it. We
|
|
359
|
+
// record the target symbol → kinds present, and disclose Unknown at any access onto that target whose
|
|
360
|
+
// type-level / precise-key resolution missed — never silent-pure (matching the syntactic object-literal-
|
|
361
|
+
// getter posture). A descriptor whose TARGET itself is unresolvable can't be tied to any forcing site;
|
|
362
|
+
// its unit is still minted (effects classified, callgraph-visible), and there is nothing more to disclose.
|
|
363
|
+
const definePropDynamicKey = new Map(); // targetSymbol -> Set("get"|"set")
|
|
350
364
|
// Resolve `extends X` to X's LOCAL ClassDeclaration (through an import alias), or null. Module-level
|
|
351
365
|
// so both the class-CHA INDEX (below) and the dispatch site's RECEIVER-SUBTREE scoping share one
|
|
352
366
|
// definition of the local inheritance edge.
|
|
@@ -377,6 +391,79 @@ function moduleOf(sf) {
|
|
|
377
391
|
const rel = path.relative(rootDir, path.resolve(sf.fileName)).replace(/\.[mc]?[tj]sx?$/, "");
|
|
378
392
|
return rel.split(path.sep).join(".");
|
|
379
393
|
}
|
|
394
|
+
// Is `node` (a function-expression / method-declaration / arrow) the `get` or `set` member of an
|
|
395
|
+
// accessor DESCRIPTOR object passed to `Object.defineProperty(target, key, desc)` /
|
|
396
|
+
// `Object.defineProperties(target, { key: desc, … })` / `Object.create(proto, { key: desc, … })`?
|
|
397
|
+
// Returns { kind:"get"|"set", targetExpr, keyText } when so (keyText is null for a non-literal key),
|
|
398
|
+
// or null. Only an accessor (`get`/`set`) descriptor qualifies — a `value:` (data) descriptor is NOT
|
|
399
|
+
// a function-property-named get/set, so it never matches (no fabrication on data props). The descriptor
|
|
400
|
+
// member may be `get(){}` (method), `get: function(){}` / `get: () => {}` (property-assignment): both
|
|
401
|
+
// have a parent PropertyAssignment-or-MethodDeclaration whose name is the identifier `get`/`set`.
|
|
402
|
+
function definePropertyAccessor(node) {
|
|
403
|
+
let memberName = null, propParent = null;
|
|
404
|
+
const p = node.parent;
|
|
405
|
+
if (!p) return null;
|
|
406
|
+
if ((ts.isMethodDeclaration(node) || ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node))
|
|
407
|
+
&& ts.isObjectLiteralExpression(node.parent)) {
|
|
408
|
+
// `{ get(){…} }` / `{ get x(){…} }` — but a real get/set-accessor here is the SYNTACTIC object
|
|
409
|
+
// literal getter (already handled honestly); only a plain METHOD named `get`/`set` is a descriptor
|
|
410
|
+
// member. A GetAccessor/SetAccessor inside a descriptor object is not how defineProperty descriptors
|
|
411
|
+
// are written, so restrict to a method whose name is literally `get`/`set`.
|
|
412
|
+
if (ts.isMethodDeclaration(node)) { memberName = node.name?.getText?.(); propParent = node; }
|
|
413
|
+
} else if (ts.isPropertyAssignment(p) && p.initializer === node && ts.isObjectLiteralExpression(p.parent)) {
|
|
414
|
+
memberName = p.name?.getText?.(); propParent = p;
|
|
415
|
+
}
|
|
416
|
+
if (memberName !== "get" && memberName !== "set") return null;
|
|
417
|
+
const descObj = ts.isMethodDeclaration(propParent) ? propParent.parent : propParent.parent; // ObjectLiteral
|
|
418
|
+
// Two shapes for the enclosing call:
|
|
419
|
+
// defineProperty(target, key, descObj) — descObj is arg #2
|
|
420
|
+
// defineProperties(target, { key: descObj }) — descObj is a property value of arg #1
|
|
421
|
+
// create(proto, { key: descObj }) — descObj is a property value of arg #1
|
|
422
|
+
const callOf = (n) => {
|
|
423
|
+
let c = n.parent;
|
|
424
|
+
while (c && !ts.isCallExpression(c)) c = c.parent;
|
|
425
|
+
return c;
|
|
426
|
+
};
|
|
427
|
+
// Walk out at most: descObj -> (its parent is either the defineProperty call's arg, OR a
|
|
428
|
+
// PropertyAssignment in a properties-map -> ObjectLiteral -> defineProperties/create call).
|
|
429
|
+
const fnName = (call) => call && call.expression && call.expression.getText().replace(/\s+/g, "");
|
|
430
|
+
// Case A: descObj is the 3rd argument of Object.defineProperty(target, key, descObj).
|
|
431
|
+
if (descObj.parent && ts.isCallExpression(descObj.parent)) {
|
|
432
|
+
const call = descObj.parent;
|
|
433
|
+
if (fnName(call) === "Object.defineProperty" && call.arguments[2] === descObj) {
|
|
434
|
+
const keyArg = call.arguments[1];
|
|
435
|
+
const keyText = keyArg && ts.isStringLiteralLike(keyArg) ? keyArg.text : null;
|
|
436
|
+
return { kind: memberName, targetExpr: call.arguments[0], keyText };
|
|
437
|
+
}
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
// Case B: descObj is a property value in a properties-map for defineProperties / create.
|
|
441
|
+
if (descObj.parent && ts.isPropertyAssignment(descObj.parent)
|
|
442
|
+
&& ts.isObjectLiteralExpression(descObj.parent.parent)) {
|
|
443
|
+
const keyProp = descObj.parent; // `key: descObj`
|
|
444
|
+
const propsMap = descObj.parent.parent; // `{ key: descObj, … }`
|
|
445
|
+
const call = callOf(propsMap);
|
|
446
|
+
const fn = fnName(call);
|
|
447
|
+
if (call && (fn === "Object.defineProperties" || fn === "Object.create")
|
|
448
|
+
&& (call.arguments[1] === propsMap)) {
|
|
449
|
+
const keyText = ts.isStringLiteralLike(keyProp.name) ? keyProp.name.text
|
|
450
|
+
: ts.isIdentifier(keyProp.name) ? keyProp.name.text : null;
|
|
451
|
+
// For defineProperties the target is arg0. For create the NEW object IS the call's result; the
|
|
452
|
+
// forcing site reads it through the binding the call is assigned to (`const o = Object.create(…)`),
|
|
453
|
+
// so the stable target is that VariableDeclaration's name. When create's result isn't bound to a
|
|
454
|
+
// simple identifier, there's no joinable target — targetExpr stays null (unit still minted; the
|
|
455
|
+
// unpinnable marker drives the Unknown disclosure, never silent-pure).
|
|
456
|
+
let targetExpr = null;
|
|
457
|
+
if (fn === "Object.defineProperties") targetExpr = call.arguments[0];
|
|
458
|
+
else if (fn === "Object.create" && call.parent
|
|
459
|
+
&& ts.isVariableDeclaration(call.parent) && call.parent.initializer === call
|
|
460
|
+
&& ts.isIdentifier(call.parent.name))
|
|
461
|
+
targetExpr = call.parent.name;
|
|
462
|
+
return { kind: memberName, targetExpr, keyText };
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
380
467
|
// `_lastCjs` is set by markCjs when localName() returns a CJS export-surface name, read right after
|
|
381
468
|
// the call to tag THAT unit (spec 0.5 draft unitKind: "export"). Keyed to the unit, not a project-
|
|
382
469
|
// wide name set — a same-named ordinary TS function in another file must NOT be mislabeled.
|
|
@@ -415,6 +502,19 @@ function localName(node) {
|
|
|
415
502
|
// invisible — same dogfood.
|
|
416
503
|
if (ts.isConstructorDeclaration(node) && ts.isClassDeclaration(node.parent) && node.parent.name)
|
|
417
504
|
return `${node.parent.name.text}.constructor`;
|
|
505
|
+
// `Object.defineProperty(target,"key",{ get(){…}/set(){…} })` descriptor accessors — the runtime
|
|
506
|
+
// accessor the TS checker can't see as a get/set (it types target.key as a data prop). Mint the
|
|
507
|
+
// descriptor body as a UNIT so its effects classify normally instead of being a silent-pure hole;
|
|
508
|
+
// the forcing-site arm edges target.key to it. Name keyed by target + key + kind so the same name a
|
|
509
|
+
// forcing site computes joins here. Both shapes (`get(){}` method, `get:fn` property) land here.
|
|
510
|
+
{
|
|
511
|
+
const da = definePropertyAccessor(node);
|
|
512
|
+
if (da) {
|
|
513
|
+
const tn = da.targetExpr ? da.targetExpr.getText().replace(/\s+/g, "") : "<create>";
|
|
514
|
+
const key = da.keyText ?? `[computed@${node.getStart()}]`;
|
|
515
|
+
return `defineProperty(${tn}).${da.kind} ${key}`;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
418
518
|
// CJS export units (--allow-js, the npm half of report chaining): dist JS exports through
|
|
419
519
|
// assignment, not declarations, so `module.exports = function …` / `exports.foo = …` /
|
|
420
520
|
// `module.exports = { sign: fn }` were not units at all — a dep scan of jsonwebtoken yielded 4
|
|
@@ -519,6 +619,26 @@ for (const sf of sources) {
|
|
|
519
619
|
nodeName.set(node, qual);
|
|
520
620
|
if ((ts.isVariableDeclaration(node) || ts.isPropertyDeclaration(node)) && node.initializer)
|
|
521
621
|
nodeName.set(node.initializer, qual);
|
|
622
|
+
// Index a `Object.defineProperty` descriptor accessor by its target SYMBOL + key, so a forcing
|
|
623
|
+
// site `target.key` (which the checker types as a plain data prop) can edge to this unit. When
|
|
624
|
+
// the target/key can't be pinned to a static symbol/literal, the unit still exists (named above)
|
|
625
|
+
// but no precise edge is possible — record an UNRESOLVED marker so an access onto such a target
|
|
626
|
+
// is disclosed Unknown rather than silently dropped.
|
|
627
|
+
const da = definePropertyAccessor(node);
|
|
628
|
+
if (da) {
|
|
629
|
+
const tsym = da.targetExpr ? checker.getSymbolAtLocation(da.targetExpr) : null;
|
|
630
|
+
if (tsym && da.keyText !== null) {
|
|
631
|
+
if (!definePropAccessors.has(tsym)) definePropAccessors.set(tsym, new Map());
|
|
632
|
+
const byKey = definePropAccessors.get(tsym);
|
|
633
|
+
if (!byKey.has(da.keyText)) byKey.set(da.keyText, {});
|
|
634
|
+
byKey.get(da.keyText)[da.kind] = node;
|
|
635
|
+
} else if (tsym) {
|
|
636
|
+
// computed key on a known target — any access onto this target may hit it: disclose Unknown.
|
|
637
|
+
if (!definePropDynamicKey.has(tsym)) definePropDynamicKey.set(tsym, new Set());
|
|
638
|
+
definePropDynamicKey.get(tsym).add(da.kind);
|
|
639
|
+
}
|
|
640
|
+
// (a wholly-unresolvable target leaves only the minted unit — nothing to join a forcing site to.)
|
|
641
|
+
}
|
|
522
642
|
}
|
|
523
643
|
ts.forEachChild(node, collect);
|
|
524
644
|
})(sf);
|
|
@@ -672,6 +792,33 @@ function accessorAt(propNode, kind /* "get" | "set" */) {
|
|
|
672
792
|
}
|
|
673
793
|
return accessorFromSym(sym, kind);
|
|
674
794
|
}
|
|
795
|
+
// A `Object.defineProperty` descriptor accessor for the forcing site `recv.key` (read → get, assign →
|
|
796
|
+
// set), consulted ONLY when the type-level `accessorAt` came up empty (the checker types target.key as
|
|
797
|
+
// a data prop, so defineProperty accessors are invisible to it). Resolve the receiver expression to its
|
|
798
|
+
// binding symbol and the key to a static string; look both up in `definePropAccessors`. Returns the
|
|
799
|
+
// descriptor function NODE (a minted unit) when found, or null. NO fabrication: a data (`value:`)
|
|
800
|
+
// descriptor was never indexed, an absent target/key returns null.
|
|
801
|
+
function definePropForceTarget(propNode, kind /* "get" | "set" */) {
|
|
802
|
+
if (definePropAccessors.size === 0) return null;
|
|
803
|
+
let recvExpr, keyText;
|
|
804
|
+
if (ts.isElementAccessExpression(propNode)) {
|
|
805
|
+
recvExpr = propNode.expression;
|
|
806
|
+
const arg = propNode.argumentExpression;
|
|
807
|
+
keyText = arg && ts.isStringLiteralLike(arg) ? arg.text : null;
|
|
808
|
+
} else if (ts.isPropertyAccessExpression(propNode)) {
|
|
809
|
+
recvExpr = propNode.expression;
|
|
810
|
+
keyText = propNode.name?.getText?.();
|
|
811
|
+
} else return null;
|
|
812
|
+
if (keyText == null) return null;
|
|
813
|
+
// Resolve the receiver to the SAME symbol the defineProperty target identifier resolved to. Follow an
|
|
814
|
+
// import alias so a cross-module `import { config }` access joins the defining module's index entry.
|
|
815
|
+
const rsym0 = checker.getSymbolAtLocation(recvExpr);
|
|
816
|
+
if (!rsym0) return null;
|
|
817
|
+
const rsym = rsym0.flags & ts.SymbolFlags.Alias ? (() => { try { return checker.getAliasedSymbol(rsym0); } catch { return rsym0; } })() : rsym0;
|
|
818
|
+
const byKey = definePropAccessors.get(rsym) ?? definePropAccessors.get(rsym0);
|
|
819
|
+
const entry = byKey?.get(keyText);
|
|
820
|
+
return entry?.[kind] ?? null;
|
|
821
|
+
}
|
|
675
822
|
// Record a resolved accessor HIT (read or write) as an edge from `owner`: into the accessor UNIT when
|
|
676
823
|
// it's a local declaration we minted; otherwise Unknown (a resolved-but-unseen accessor body — never
|
|
677
824
|
// silent-pure, SPEC §4). `label` tags the §-why disclosure.
|
|
@@ -787,6 +934,63 @@ function iterationTargets(expr, isAsync) {
|
|
|
787
934
|
function edgeToTargets(rec, decls) {
|
|
788
935
|
for (const d of decls) { const t = nodeName.get(d); if (t) rec.edges.add(t); }
|
|
789
936
|
}
|
|
937
|
+
// Does iterating `expr` FORCE an OPAQUE (caller-supplied) iterable? Forcing an iterable runs its
|
|
938
|
+
// `[Symbol.iterator]`/`next` body, which — when the iterable is a PARAMETER / `any` / a bare type-
|
|
939
|
+
// parameter — is caller-chosen code that can perform arbitrary I/O. This is epistemically identical to
|
|
940
|
+
// invoking an opaque callback parameter (the `call:param` → Unknown posture, scan.mjs ~1145-1158): a
|
|
941
|
+
// silent-pure verdict would be the cardinal sin. Mirror that decision exactly. Returns a `why` string
|
|
942
|
+
// (→ record Unknown) or null. By design this fires ONLY for genuinely caller-supplied iterables; a
|
|
943
|
+
// CONCRETE built-in (array/string/Map/Set: the value resolves to a concrete type, not a param) stays
|
|
944
|
+
// PURE, and a LOCAL iterable/generator (a local call result, a local class instance) is handled by the
|
|
945
|
+
// existing local-edge path (iterationTargets / the call machinery) — neither is flagged here.
|
|
946
|
+
// Opaque iterable INTERFACE names: a value whose TYPE is literally one of these is caller-supplied
|
|
947
|
+
// iterator code (its `next` body is unknowable). A CONCRETE built-in (Array/Set/Map/String) has its own
|
|
948
|
+
// symbol (`Array`/…) and runs a built-in iterator (no user code) → NOT opaque; a LOCAL class implementing
|
|
949
|
+
// Iterable is handled by the local-edge path. So we key on the type's SYMBOL NAME, NOT on "is a parameter"
|
|
950
|
+
// (the earlier param-identity check fabricated Unknown when iterating a concrete-typed array PARAM — the
|
|
951
|
+
// conformance `loop_elem` regression: `(items: T[]) => { for (const c of items) … }`).
|
|
952
|
+
const OPAQUE_ITERABLE_TYPES = new Set([
|
|
953
|
+
"Iterable", "Iterator", "IterableIterator", "Generator",
|
|
954
|
+
"AsyncIterable", "AsyncIterator", "AsyncIterableIterator", "AsyncGenerator",
|
|
955
|
+
]);
|
|
956
|
+
function opaqueIterableWhy(expr) {
|
|
957
|
+
const t = checker.getTypeAtLocation(expr);
|
|
958
|
+
if (!t) return null;
|
|
959
|
+
// (a) `any` or a bare TYPE PARAMETER (`<T extends Iterable<…>>(x: T)`): the concrete iterable is
|
|
960
|
+
// indeterminate, so its iterator body is unknowable — never silently pure.
|
|
961
|
+
if (t.flags & ts.TypeFlags.Any) return "iterate:any";
|
|
962
|
+
if (t.flags & ts.TypeFlags.TypeParameter) return "iterate:typeparam";
|
|
963
|
+
// (b) the type IS an opaque iterable INTERFACE *and* the value is CALLER-SUPPLIED (a parameter / binding
|
|
964
|
+
// element): `collect(source: Iterable<T>)`, `nexts(it: Iterator<T>)`, `drain(g: Generator<T>)`. The
|
|
965
|
+
// caller chooses the concrete iterator (arbitrary I/O), identical to invoking an opaque callback. BOTH
|
|
966
|
+
// conditions are required: a LOCAL generator/iterable CALL result (`for (const x of gen())`) also has a
|
|
967
|
+
// Generator/IterableIterator type but is NOT a param → excluded, so the local-edge path edges its real
|
|
968
|
+
// effect (no spurious Unknown); a concrete Array/Set/Map/String PARAM has its own symbol (not in the
|
|
969
|
+
// set) → excluded → PURE (built-in iterator, no user code; the conformance `loop_elem` case).
|
|
970
|
+
const sym = t.getSymbol && t.getSymbol();
|
|
971
|
+
if (sym && OPAQUE_ITERABLE_TYPES.has(sym.name) && ts.isIdentifier(expr)) {
|
|
972
|
+
const d = realDecl(checker.getSymbolAtLocation(expr));
|
|
973
|
+
if (d && (ts.isParameter(d) || ts.isBindingElement(d))) {
|
|
974
|
+
const idx = ts.isParameter(d) && d.parent ? d.parent.parameters.indexOf(d) : -1;
|
|
975
|
+
return idx >= 0 ? `iterate:param#${idx}` : "iterate:param";
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
return null;
|
|
979
|
+
}
|
|
980
|
+
// Record an opaque-iterable force as Unknown on the enclosing fn, UNLESS iteration already resolved a
|
|
981
|
+
// LOCAL desugar target (then the real effect is edged — no Unknown needed). `localResolved` = the
|
|
982
|
+
// non-empty result of iterationTargets (a local `[Symbol.iterator]`/`next` unit was found).
|
|
983
|
+
function noteOpaqueIteration(node, iterExpr, localResolved) {
|
|
984
|
+
if (localResolved) return;
|
|
985
|
+
const owner = enclosing(node);
|
|
986
|
+
if (!owner) return;
|
|
987
|
+
const why = opaqueIterableWhy(iterExpr);
|
|
988
|
+
if (!why) return;
|
|
989
|
+
const rec = fns.get(owner);
|
|
990
|
+
if (!rec) return;
|
|
991
|
+
rec.direct.add("Unknown");
|
|
992
|
+
rec.why.add(why);
|
|
993
|
+
}
|
|
790
994
|
|
|
791
995
|
// Callee names that INVOKE a function/method argument (so a fn-reference passed to one is reachable
|
|
792
996
|
// through it). Array/iterable HOFs, the timer/microtask schedulers, and Promise continuations. A
|
|
@@ -921,6 +1125,28 @@ function visitCalls(node) {
|
|
|
921
1125
|
rec.why.add(`call:${recvText.slice(0, 40)}.${m}`);
|
|
922
1126
|
}
|
|
923
1127
|
}
|
|
1128
|
+
// EXPLICIT iterator force: `it.next()` / `it.return()` / `it.throw()` on an OPAQUE iterator
|
|
1129
|
+
// (a parameter / `any` / type-parameter typed as the `Iterator`/`Generator` protocol) runs
|
|
1130
|
+
// caller-supplied iterator code — epistemically identical to forcing a for-of over an opaque
|
|
1131
|
+
// iterable, and to invoking an opaque callback. The method resolves to the non-local es-lib
|
|
1132
|
+
// `Iterator.next` signature, so the desugar above never sees it and the call lands here pure.
|
|
1133
|
+
// Disclose Unknown (cardinal-sin guard). Gated on the iterator-protocol type symbol so an
|
|
1134
|
+
// unrelated `.next()` on some other opaque param is not flagged; a LOCAL iterator's `next`
|
|
1135
|
+
// resolves `<local>` (edged below), never reaching this non-local arm.
|
|
1136
|
+
if ((m === "next" || m === "return" || m === "throw")) {
|
|
1137
|
+
const why = opaqueIterableWhy(recv);
|
|
1138
|
+
const rt = checker.getTypeAtLocation(recv);
|
|
1139
|
+
const sn = rt?.getSymbol?.()?.getName?.()
|
|
1140
|
+
?? (rt?.flags & ts.TypeFlags.TypeParameter ? checker.getBaseConstraintOfType(rt)?.getSymbol?.()?.getName?.() : undefined);
|
|
1141
|
+
const ITER_PROTO = new Set([
|
|
1142
|
+
"Iterator", "AsyncIterator", "Iterable", "AsyncIterable",
|
|
1143
|
+
"IterableIterator", "AsyncIterableIterator", "Generator", "AsyncGenerator",
|
|
1144
|
+
]);
|
|
1145
|
+
if (why && sn && ITER_PROTO.has(sn)) {
|
|
1146
|
+
rec.direct.add("Unknown");
|
|
1147
|
+
rec.why.add(why); // `iterate:param#i` / `iterate:any` / `iterate:typeparam`
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
924
1150
|
}
|
|
925
1151
|
if (mod === "<local>") {
|
|
926
1152
|
const targetName = nodeName.get(decl);
|
|
@@ -1327,12 +1553,41 @@ function visitCalls(node) {
|
|
|
1327
1553
|
&& p.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && p.operatorToken.kind <= ts.SyntaxKind.LastAssignment;
|
|
1328
1554
|
const recordKind = (kind) => {
|
|
1329
1555
|
const hit = accessorAt(node, kind);
|
|
1330
|
-
if (
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1556
|
+
if (hit) {
|
|
1557
|
+
const owner = enclosing(node);
|
|
1558
|
+
if (!owner) return;
|
|
1559
|
+
const an = hit.decl.parent?.name?.getText?.() ?? "?";
|
|
1560
|
+
const pn = node.name?.getText?.() ?? node.argumentExpression?.getText?.() ?? "?";
|
|
1561
|
+
recordAccessorHit(owner, hit, `${an}.${pn}`);
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
// No type-level accessor — try the `Object.defineProperty` runtime-accessor index. The checker
|
|
1565
|
+
// types target.key as a data prop, so an effectful defineProperty getter/setter is invisible to
|
|
1566
|
+
// accessorAt; consult definePropForceTarget so the forcing site edges to the descriptor unit
|
|
1567
|
+
// (precise) instead of reading silent-pure (the cardinal sin). A descriptor we minted is always
|
|
1568
|
+
// local, so this is an EDGE; never Unknown for a resolved-and-seen descriptor.
|
|
1569
|
+
const dpNode = definePropForceTarget(node, kind);
|
|
1570
|
+
if (dpNode) {
|
|
1571
|
+
const owner = enclosing(node);
|
|
1572
|
+
const t = owner && nodeName.get(dpNode);
|
|
1573
|
+
if (t) fns.get(owner).edges.add(t);
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1576
|
+
// A computed-key descriptor accessor on this receiver's target means `recv.<anything>` MIGHT
|
|
1577
|
+
// invoke an effectful accessor whose key we couldn't pin — disclose Unknown (never silent-pure),
|
|
1578
|
+
// matching the syntactic object-literal-getter posture. Only when the receiver binds to a target
|
|
1579
|
+
// that carries a dynamic-key descriptor of the right kind.
|
|
1580
|
+
if (definePropDynamicKey.size > 0) {
|
|
1581
|
+
const rsym0 = ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)
|
|
1582
|
+
? checker.getSymbolAtLocation(node.expression) : null;
|
|
1583
|
+
const rsym = rsym0 && (rsym0.flags & ts.SymbolFlags.Alias)
|
|
1584
|
+
? (() => { try { return checker.getAliasedSymbol(rsym0); } catch { return rsym0; } })() : rsym0;
|
|
1585
|
+
const kinds = (rsym && definePropDynamicKey.get(rsym)) || (rsym0 && definePropDynamicKey.get(rsym0));
|
|
1586
|
+
if (kinds && kinds.has(kind)) {
|
|
1587
|
+
const owner = enclosing(node);
|
|
1588
|
+
if (owner) { fns.get(owner).direct.add("Unknown"); fns.get(owner).why.add(`defineProperty:dynamic-key`); }
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1336
1591
|
};
|
|
1337
1592
|
if (simpleAssign || isDestructuringAssignTarget(node)) recordKind("set");
|
|
1338
1593
|
else if (compoundAssign) { recordKind("get"); recordKind("set"); }
|
|
@@ -1381,7 +1636,14 @@ function visitCalls(node) {
|
|
|
1381
1636
|
iterExpr = node.arguments[0]; // Array.from(bag) — the iterable form (arg0 is iterated)
|
|
1382
1637
|
if (iterExpr) {
|
|
1383
1638
|
const owner = enclosing(node);
|
|
1384
|
-
if (owner)
|
|
1639
|
+
if (owner) {
|
|
1640
|
+
const targets = iterationTargets(iterExpr, iterAsync);
|
|
1641
|
+
edgeToTargets(fns.get(owner), targets);
|
|
1642
|
+
// Opaque-iterable honesty: a param/`any`/type-parameter iterable runs caller-supplied iterator
|
|
1643
|
+
// code — disclose Unknown, mirroring the opaque-callback `call:param` posture (cardinal-sin
|
|
1644
|
+
// guard). Skipped when iteration already resolved a LOCAL unit (real effect already edged).
|
|
1645
|
+
noteOpaqueIteration(node, iterExpr, targets.length > 0);
|
|
1646
|
+
}
|
|
1385
1647
|
}
|
|
1386
1648
|
}
|
|
1387
1649
|
// `using r = expr` / `await using r = expr` (MED): the scope-exit guarantees `r[Symbol.dispose]()` /
|