candor-ts 0.5.16 → 0.5.17

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/scan.mjs +21 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "candor-ts",
3
- "version": "0.5.16",
3
+ "version": "0.5.17",
4
4
  "description": "candor for TypeScript — per-function side effects, transitively, with a policy gate (candor-spec 0.5)",
5
5
  "type": "module",
6
6
  "dependencies": {
package/scan.mjs CHANGED
@@ -442,6 +442,19 @@ function localName(node) {
442
442
  }
443
443
  return null;
444
444
  }
445
+ // True when `node` is lexically inside a FUNCTION body — so its bare local name can collide with a
446
+ // module-level or sibling-scope unit of the same name (see the qual disambiguation below). A namespace/
447
+ // module block and the source file are NOT function scopes (their members are top-level units).
448
+ function isFunctionScoped(node) {
449
+ for (let p = node.parent; p; p = p.parent) {
450
+ if (ts.isSourceFile(p) || ts.isModuleBlock(p)) return false;
451
+ if (ts.isFunctionDeclaration(p) || ts.isFunctionExpression(p) || ts.isArrowFunction(p)
452
+ || ts.isMethodDeclaration(p) || ts.isConstructorDeclaration(p)
453
+ || ts.isGetAccessorDeclaration(p) || ts.isSetAccessorDeclaration(p))
454
+ return true;
455
+ }
456
+ return false;
457
+ }
445
458
  for (const sf of sources) {
446
459
  const mod = moduleOf(sf);
447
460
  (function collect(node) {
@@ -491,8 +504,15 @@ for (const sf of sources) {
491
504
  const n = localName(node);
492
505
  const isCjsExport = _lastCjs; // captured immediately: localName set it for THIS node only
493
506
  if (n) {
494
- const qual = `${mod}.${n}`;
495
507
  const { line, character } = sf.getLineAndCharacterOfPosition(node.getStart());
508
+ // Disambiguate FUNCTION-SCOPED local function units by position. A `const persist = () => …` (or a
509
+ // nested `function persist(){}`) inside a fn body shares the bare `mod.name` key with a module-level
510
+ // OR sibling-scope unit of the same name, so the second `fns.set` CLOBBERS the first and a call that
511
+ // resolves (correctly, via the checker) to the LOCAL decl reads the OTHER unit's effects off the
512
+ // shared entry — FABRICATING them onto a pure caller. `nodeName` is keyed by NODE identity, so a
513
+ // per-node-unique key keeps resolution exact; only TOP-LEVEL units need the stable bare name a
514
+ // consumer's hash-join targets (a function-scoped local is never an export, so nothing joins to it).
515
+ const qual = isFunctionScoped(node) ? `${mod}.${n}#${line + 1}:${character + 1}` : `${mod}.${n}`;
496
516
  fns.set(qual, { local: n, direct: new Set(), edges: new Set(), hosts: new Set(), tables: new Set(),
497
517
  cmds: new Set(), paths: new Set(), blind: new Set(), incomplete: new Set(), why: new Set(), entry: false, isCjsExport,
498
518
  loc: `${path.relative(rootDir, sf.fileName)}:${line + 1}:${character + 1}` });