candor-ts 0.5.2 → 0.5.3
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 +104 -0
package/package.json
CHANGED
package/scan.mjs
CHANGED
|
@@ -542,6 +542,60 @@ function rootsAtStdStream(expr) {
|
|
|
542
542
|
}
|
|
543
543
|
}
|
|
544
544
|
|
|
545
|
+
// ---- the implicit/desugared-call surface (the silent-pure holes the AST walk misses) -------------
|
|
546
|
+
// CLASSIFIER §1 says resolve, don't pattern-match — but the walk only sees CallExpression/
|
|
547
|
+
// NewExpression (+ accessor access). Effects reached through a DESUGARED call (a `for-of` lowering to
|
|
548
|
+
// `it[Symbol.iterator]().next()`, a `using` to `r[Symbol.dispose]()`, a tagged template to `tag(...)`)
|
|
549
|
+
// were invisible: reported concrete-PURE (omitted), not even Unknown. We model the desugaring exactly
|
|
550
|
+
// as the spec demands — resolve the implicit target via the compiler API and edge to it when LOCAL.
|
|
551
|
+
// A resolved-but-unseen target follows the existing external/κ posture (OPAQUE + ledger), and a
|
|
552
|
+
// BUILT-IN iterator/disposer (es-lib/@types/node — a plain array's iterator, a stdlib disposable)
|
|
553
|
+
// resolves to a non-local declaration and edges nothing, so it correctly stays pure.
|
|
554
|
+
|
|
555
|
+
// The member symbol for a WELL-KNOWN symbol (`Symbol.iterator`, `Symbol.dispose`, …) on a type. The
|
|
556
|
+
// checker mangles these to an escaped name `__@iterator@<globalId>`; match by the `__@<name>@` prefix
|
|
557
|
+
// (the trailing id is the unique Symbol's identity, not part of the name). `prefixes` is tried in
|
|
558
|
+
// order so a sync site prefers the sync method and an async site its async twin (falling back to sync).
|
|
559
|
+
function wellKnownSymbolMember(type, prefixes) {
|
|
560
|
+
if (!type || !type.getProperties) return null;
|
|
561
|
+
for (const p of type.getProperties()) {
|
|
562
|
+
const n = p.getName();
|
|
563
|
+
for (const pre of prefixes) if (n === pre || n.startsWith(pre + "@")) return p;
|
|
564
|
+
}
|
|
565
|
+
return null;
|
|
566
|
+
}
|
|
567
|
+
function declOfSym(sym) { return sym && (sym.valueDeclaration ?? sym.declarations?.[0]); }
|
|
568
|
+
const declIsLocal = (decl) => decl && projectFiles.has(path.resolve(decl.getSourceFile().fileName));
|
|
569
|
+
|
|
570
|
+
// The LOCAL units an ITERATION over `expr` implicitly calls: the iterable's `[Symbol.iterator]` (or
|
|
571
|
+
// `[Symbol.asyncIterator]` for `for await`) method AND the produced iterator's `next()`. The generator
|
|
572
|
+
// case rolls `next`'s body into the iterator-method unit (lexical attribution), and the self-iterator
|
|
573
|
+
// case (`[Symbol.iterator]() { return this }` + a separate effectful `next()`) needs the `next` edge —
|
|
574
|
+
// so we edge to BOTH whenever each is a LOCAL unit. A built-in iterable (plain array/string/Map: the
|
|
575
|
+
// es-lib/@types iterator) resolves non-local → no edge → stays pure (the precision invariant).
|
|
576
|
+
function iterationTargets(expr, isAsync) {
|
|
577
|
+
const t = checker.getTypeAtLocation(expr);
|
|
578
|
+
const iterPrefixes = isAsync ? ["__@asyncIterator", "__@iterator"] : ["__@iterator"];
|
|
579
|
+
const iterDecl = declOfSym(wellKnownSymbolMember(t, iterPrefixes));
|
|
580
|
+
if (!iterDecl) return [];
|
|
581
|
+
const out = [];
|
|
582
|
+
if (declIsLocal(iterDecl)) out.push(iterDecl);
|
|
583
|
+
// the iterator's next(): the return type of the [Symbol.iterator] method
|
|
584
|
+
try {
|
|
585
|
+
const sig = checker.getSignatureFromDeclaration(iterDecl);
|
|
586
|
+
const ret = sig && checker.getReturnTypeOfSignature(sig);
|
|
587
|
+
const nextDecl = declOfSym(ret && ret.getProperties().find((p) => p.getName() === "next"));
|
|
588
|
+
if (nextDecl && declIsLocal(nextDecl) && !out.includes(nextDecl)) out.push(nextDecl);
|
|
589
|
+
} catch { /* unresolved iterator shape — the iterator-method edge already covers the common case */ }
|
|
590
|
+
return out;
|
|
591
|
+
}
|
|
592
|
+
// Edge `rec` to each LOCAL desugared target that is a minted unit. Local-only by design: an external
|
|
593
|
+
// iterable/disposer is OPAQUE (the curated-κ caveat — same as an unmatched external call), never a
|
|
594
|
+
// fabricated edge; the existing call machinery + κ ledger already cover any EXPLICIT calls into it.
|
|
595
|
+
function edgeToTargets(rec, decls) {
|
|
596
|
+
for (const d of decls) { const t = nodeName.get(d); if (t) rec.edges.add(t); }
|
|
597
|
+
}
|
|
598
|
+
|
|
545
599
|
// ---- pass 2: per call site, the (CLASSIFY)/(EDGE)/(UNKNOWN) resolution of SEMANTICS §4 ------------
|
|
546
600
|
function visitCalls(node) {
|
|
547
601
|
if (ts.isCallExpression(node) || ts.isNewExpression(node)) {
|
|
@@ -820,6 +874,56 @@ function visitCalls(node) {
|
|
|
820
874
|
}
|
|
821
875
|
}
|
|
822
876
|
}
|
|
877
|
+
// ITERATION desugaring (HIGH): `for (const x of bag)`, `for await (…)`, `[...bag]`, `const [a]=bag`,
|
|
878
|
+
// `Array.from(bag)` all lower to `bag[Symbol.iterator]().next()`. Edge the enclosing fn to the
|
|
879
|
+
// iterable's local `[Symbol.iterator]`/`[Symbol.asyncIterator]` method (and the produced iterator's
|
|
880
|
+
// local `next`). A built-in iterable (array/string/Map) resolves non-local → no edge → stays pure.
|
|
881
|
+
{
|
|
882
|
+
let iterExpr = null, iterAsync = false;
|
|
883
|
+
if (ts.isForOfStatement(node)) { iterExpr = node.expression; iterAsync = !!node.awaitModifier; }
|
|
884
|
+
else if (ts.isSpreadElement(node)) iterExpr = node.expression; // [...bag] / f(...bag)
|
|
885
|
+
else if (ts.isSpreadAssignment(node)) iterExpr = node.expression; // {...bag} — object spread is NOT
|
|
886
|
+
// iteration (it copies own enumerable props, no [Symbol.iterator]); wellKnownSymbolMember simply
|
|
887
|
+
// finds none and edges nothing. Listed for clarity; the resolution self-guards.
|
|
888
|
+
else if (ts.isVariableDeclaration(node) && ts.isArrayBindingPattern(node.name) && node.initializer)
|
|
889
|
+
iterExpr = node.initializer; // const [a] = bag
|
|
890
|
+
else if (ts.isCallExpression(node) && node.arguments?.[0]
|
|
891
|
+
&& node.expression.getText() === "Array.from")
|
|
892
|
+
iterExpr = node.arguments[0]; // Array.from(bag) — the iterable form (arg0 is iterated)
|
|
893
|
+
if (iterExpr) {
|
|
894
|
+
const owner = enclosing(node);
|
|
895
|
+
if (owner) edgeToTargets(fns.get(owner), iterationTargets(iterExpr, iterAsync));
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
// `using r = expr` / `await using r = expr` (MED): the scope-exit guarantees `r[Symbol.dispose]()` /
|
|
899
|
+
// `r[Symbol.asyncDispose]()`. Edge the enclosing fn to the resolved LOCAL dispose method.
|
|
900
|
+
if (ts.isVariableStatement(node)) {
|
|
901
|
+
const fl = node.declarationList.flags;
|
|
902
|
+
const isUsing = (fl & ts.NodeFlags.Using) || (fl & ts.NodeFlags.AwaitUsing);
|
|
903
|
+
if (isUsing) {
|
|
904
|
+
const isAwait = !!(fl & ts.NodeFlags.AwaitUsing);
|
|
905
|
+
const prefixes = isAwait ? ["__@asyncDispose", "__@dispose"] : ["__@dispose"];
|
|
906
|
+
const owner = enclosing(node);
|
|
907
|
+
for (const d of node.declarationList.declarations) {
|
|
908
|
+
if (!d.initializer || !owner) continue;
|
|
909
|
+
const t = checker.getTypeAtLocation(d.initializer);
|
|
910
|
+
const disposeDecl = declOfSym(wellKnownSymbolMember(t, prefixes));
|
|
911
|
+
if (disposeDecl && declIsLocal(disposeDecl)) edgeToTargets(fns.get(owner), [disposeDecl]);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
// TAGGED TEMPLATE (LOW): `` tag`…` `` calls `tag(strings, ...subs)`. getResolvedSignature resolves
|
|
916
|
+
// the TaggedTemplateExpression to the tag fn cleanly — a node form the CallExpression walk never
|
|
917
|
+
// visits. Edge to the tag when LOCAL; a built-in/external tag (`String.raw`) resolves non-local and
|
|
918
|
+
// edges nothing (pure), matching the external-call posture.
|
|
919
|
+
if (ts.isTaggedTemplateExpression(node)) {
|
|
920
|
+
const owner = enclosing(node);
|
|
921
|
+
if (owner) {
|
|
922
|
+
const sig = checker.getResolvedSignature(node);
|
|
923
|
+
const decl = sig && sig.declaration;
|
|
924
|
+
if (decl && declIsLocal(decl)) edgeToTargets(fns.get(owner), [decl]);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
823
927
|
ts.forEachChild(node, visitCalls);
|
|
824
928
|
}
|
|
825
929
|
for (const sf of sources) visitCalls(sf);
|