@mnemonica/tactica 0.3.8 → 0.3.10
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/README.md +23 -5
- package/lib/analyzer.d.ts +106 -4
- package/lib/analyzer.js +546 -46
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -169,6 +169,8 @@ declare module 'mnemonica' {
|
|
|
169
169
|
|
|
170
170
|
**Recommended for new projects.** Explicit imports, better tree-shaking, no global namespace pollution.
|
|
171
171
|
|
|
172
|
+
**Instance-type inheritance.** Nested instance types flatten the WHOLE ancestor chain: each nested type is `ProtoFlat<ParentInstance, Self>` (mnemonica's `ProtoFlat` keeps every parent key except the ones `Self` overrides), and the parent instance type is itself a `ProtoFlat` over its own parent — so the ROOT's own constructor-arg fields (`uuid` in `PaymentRoot`) are directly present on every descendant instance type, at any depth, alongside intermediate ancestors' fields. If a root field appears missing on a nested type, the usual cause is a stale `.tactica` — regenerate; the composition itself is transitive by construction.
|
|
173
|
+
|
|
172
174
|
### Legacy global-augmentation mode (`--module-augmentation`)
|
|
173
175
|
|
|
174
176
|
Emits one `.tactica/index.d.ts` whose contents live inside `declare global { … }`. Reference it from your code with:
|
|
@@ -222,7 +224,9 @@ const AdminType = UserType.define('AdminType', function (this: { role: string })
|
|
|
222
224
|
});
|
|
223
225
|
```
|
|
224
226
|
|
|
225
|
-
Chained calls (`define('A').define('B')`) and nested calls via variable references (`const A = define('A', …); A.define('B', …)`) are both supported.
|
|
227
|
+
Chained calls (`define('A').define('B')`) and nested calls via variable references (`const A = define('A', …); A.define('B', …)`) are both supported. A const holding a multi-hop initializer binds the LAST hop — `define()` returns the defined type's constructor, so `const Branch = Root.define('A', …).define('B', …)` makes `Branch` denote `Root.A.B` and `Branch.define('C', …)` nests C under B, exactly as the runtime does.
|
|
228
|
+
|
|
229
|
+
**Call-site ergonomics (recommended pattern):** annotate the root constructor's `this` with the generated instance type alias — `define('Widget', function (this: TWidgetInstance, data: …) {…})`, with `TWidgetInstance` imported from the generated types file. The common form is an intersection — `type TWidgetInstance = TWidgetArgs & TGeneratedRootInstance`. Tactica tolerates the self-reference: the annotation is never emitted into the generated type (so no circularity can form), and it is **ergonomic-only** — its members are never expanded, so the root's fields must be carried by the constructor body: `Object.assign(this, args)` (the identifier form extracts every field from the named args type) or explicit `this.x = …` assignments. Property extraction falls back to the body / data parameter whenever the alias is not analyzable (it lives in the excluded `.tactica/`). With the generated `types.ts`/`registry.ts` in place, `new (lookup('Widget'))(args)` — and `new Widget(args)` where the const merges with the generated type — is directly assignable to the instance type at call sites, zero casts. Regenerate `.tactica` after adding the annotation.
|
|
226
230
|
|
|
227
231
|
### `lazy()` definitions
|
|
228
232
|
|
|
@@ -237,6 +241,18 @@ const AdminType = UserType.lazy(() => class AdminType {
|
|
|
237
241
|
|
|
238
242
|
All forms are recognized: free `lazy('Name', getter)`, method `Type.lazy(...)`, and chained `define('A').lazy('B', getter)`. The getter is followed, and the returned constructor is analyzed like a direct `define()` handler — properties and constructor parameters are extracted and emitted in `types.ts` / `registry.ts` like any other type.
|
|
239
243
|
|
|
244
|
+
### Construction shapes beyond plain `new`
|
|
245
|
+
|
|
246
|
+
These usage shapes are recognized and attributed within the existing output contract (the `instantiation` usage kind and value-scope bindings — no new output fields):
|
|
247
|
+
|
|
248
|
+
- **Chain construction** — `new Root(...).Child(...)` and the single-await form `await new Root(...).A(...).B(...)`: the chain-tip call records an `instantiation` of the tip type (`Root.A.B`), supplying the creation-graph anchor, and the result variable binds to the TIP's type, not the root's. `await` is transparent everywhere on this page — `await call(...)`, `await apply(...)`, `await entry.fork(...)`, and async `bind` recognition all work identically.
|
|
249
|
+
- **`instance.fork(...)` / `instance.clone()`** (call forms) and **`instance.clone`** (the property form — core types it `readonly clone: this`) — runtime returns `this`, so the result variable binds to the source instance's type. Fork re-runs construction (hooks fire, a distinct instance on a distinct line), so these record an **`instantiation` usage** at the site — byte-indistinguishable from `new` until the deferred mechanism-kind revision; the result binding and the generic `methodCall`/`propertyRead` flow entries stay.
|
|
250
|
+
- **`utils.merge(a, b, ...)`** (also the direct named import, and the curried `utils.fork(instance)(...)`) — the result binds to **arg 0's type**: the runtime answer is a's lineage over b's context, and a's fullPath is the honest approximation within the contract. Merge is `fork(a)` over b's context, so it records an `instantiation` for a's type too; the curried `utils.fork` records at the invocation site, and the inner `utils.fork(instance)` binding line records its own entry — one per distinct source line.
|
|
251
|
+
- **`call(entity, Ctor, ...)` / `apply(entity, Ctor, args)`** — the mnemonica construction exports (import-aware: only actual `'mnemonica'` imports or members of a tracked module-object alias match; userland `call`/`apply` never do). The Ctor argument (arg 1) records an `instantiation`, and the result variable binds to the Ctor's type (runtime `InstanceResult<Merge<E, T>>` approximated by T). **Decorated classes resolve as the Ctor** through the same graph tiers — `@decorate() class C` is a known type, so `call(parent, C)` records and binds; an undecorated plain class has no graph entry, so the call binds nothing and records no usage (never a bare name). The free-call `decorate(Class)` form is not tracked (tactica recognizes the `@decorate` decorator syntax only).
|
|
252
|
+
- **`bind(entity, Ctor)`** — records no usage (it constructs nothing); the bound variable binds to the Ctor's type. Invoking the bound function later (`f(...)`, including `await f(...)`) is not followed.
|
|
253
|
+
|
|
254
|
+
Two boundaries stay deliberate: bindings surface only for references lexically AFTER the binding statement (a binding made inside a constructor body is not yet visible to that same handler's property extraction), and the mechanism distinction (`new` vs fork vs call) is not carried in the outputs — fork/clone/merge record as plain `instantiation` entries, byte-indistinguishable from `new`, until the deferred mechanism-kind contract revision lands. Consumers counting constructions should expect fork sites among them.
|
|
255
|
+
|
|
240
256
|
### Builder pattern on the imported module object
|
|
241
257
|
|
|
242
258
|
The analyzer also recognizes the chainable `mnemonica` module object and aliases of it:
|
|
@@ -467,7 +483,9 @@ The analyzer infers property types from constructor bodies and class members. Su
|
|
|
467
483
|
| `this.x = data.field` where `data: SomeNamedType` (alias/interface/class) | type of the field from the resolved declaration — import-aware, inherited members included; a bare `this.x = data` keeps the full expanded shape |
|
|
468
484
|
| `this.x = data.field \|\| []` | type of the fallback expression |
|
|
469
485
|
| `this.x = data.field ? a : b` | type of the truthy branch |
|
|
470
|
-
| `
|
|
486
|
+
| `this.x = (<const>['a', 'b'])[0]` / `this.x = ([…] as const)[1]` / `this.x = trackedList[2]` | the element's literal type (`'a'`) — const-asserted literal arrays either spelling, or a tracked module const array (`const trackedList = <const>[…]`); non-numeric/out-of-range/spread/non-literal elements and general `<T>` assertions stay `unknown` |
|
|
487
|
+
| `Object.assign(this, data)` | all fields of `data`'s type annotation — inline literal or named alias/interface/class, resolved import-aware (F14 machinery) |
|
|
488
|
+
| `this.x = boundVar` where `boundVar` holds a construction result (`new`/`lookup()`/chain/fork/merge/call) | the bound type's instance type (value-scope binding) — bindings surface lexically AFTER the binding statement; within the same constructor body a fresh binding is not yet visible to the handler's own extraction |
|
|
471
489
|
| async / sync constructor functions | same rules |
|
|
472
490
|
|
|
473
491
|
An existing annotation always beats a weaker inference: `unknown` (and `unknown`-bearing guesses like `Array<unknown>`) never overwrites a known type — `Record<string, unknown>` IS a known type — and an overwrite never drops an optionality modifier.
|
|
@@ -476,7 +494,7 @@ When inference fails the property's type falls back to `unknown` (or `any` in so
|
|
|
476
494
|
|
|
477
495
|
### Referenced types in constructor signatures
|
|
478
496
|
|
|
479
|
-
A constructor parameter that references a named type (`record: SharedShape`) resolves **through the importing file's own import statements** — relative and tsconfig-paths imports are followed to the declaration (chasing re-export barrels), and imported classes/interfaces/type aliases with extractable fields are expanded inline, **inherited members included**: `extends` chains are walked (depth-bounded, cycle-guarded) and parent fields merge with the declaration's own fields overriding on name clash. A field typed by a `typeof` query over a module-level const array (`status?: typeof statusList[number]`) expands to the element literal union (`status?: 'active' | 'closed'`); any other `typeof` source degrades that field to `unknown` — a bare `typeof name` is never emitted, since the generated file has no imports to resolve it against. An import is never shadowed by a same-named declaration elsewhere in the program. A reference that resolves not at all (a ghost name, declared nowhere) emits `unknown` — generated `.tactica/types.ts` carries no import statements of its own, so a bare unresolvable name is never emitted. A reference that resolves *ambiguously* — several of your project's files declare the name and no import chooses between them — is **fatal** like the mnemonica-graph identity law below: tactica prints the reference site and every declaration site, exits non-zero, and writes no output. An import anchoring the reference is never ambiguity (the import wins, duplicate declarations or not), and package-declared same-named types never count — a user-local declaration always wins over a node_modules `.d.ts`. Two same-named interfaces in one file are legal TypeScript merging, not ambiguity (see Known Limitations for the last-wins caveat).
|
|
497
|
+
A constructor parameter that references a named type (`record: SharedShape`) resolves **through the importing file's own import statements** — relative and tsconfig-paths imports are followed to the declaration (chasing re-export barrels), and imported classes/interfaces/type aliases with extractable fields are expanded inline, **inherited members included**: `extends` chains are walked (depth-bounded, cycle-guarded) and parent fields merge with the declaration's own fields overriding on name clash. A field typed by a `typeof` query over a module-level const array (`status?: typeof statusList[number]`) expands to the element literal union (`status?: 'active' | 'closed'`); the value-level twin — element access over a const-asserted literal array (`(<const>['active', 'not_active', 'hold'])[0]`, `([…] as const)[1]`, or a tracked module const used as `x[0]`) — infers the element's literal type the same way; any other `typeof` source degrades that field to `unknown` — a bare `typeof name` is never emitted, since the generated file has no imports to resolve it against. An import is never shadowed by a same-named declaration elsewhere in the program. A reference that resolves not at all (a ghost name, declared nowhere) emits `unknown` — generated `.tactica/types.ts` carries no import statements of its own, so a bare unresolvable name is never emitted. A reference that resolves *ambiguously* — several of your project's files declare the name and no import chooses between them — is **fatal** like the mnemonica-graph identity law below: tactica prints the reference site and every declaration site, exits non-zero, and writes no output. An import anchoring the reference is never ambiguity (the import wins, duplicate declarations or not), and package-declared same-named types never count — a user-local declaration always wins over a node_modules `.d.ts`. Two same-named interfaces in one file are legal TypeScript merging, not ambiguity (see Known Limitations for the last-wins caveat).
|
|
480
498
|
|
|
481
499
|
**Ambiguous plain-TypeScript reference — fails, and how to fix it.** Two plain interfaces named `SharedShape` exist in different files, and a third file references a bare `SharedShape` with nothing anchoring it:
|
|
482
500
|
|
|
@@ -787,7 +805,7 @@ When enabled, tactica detects execution-flow patterns alongside type definitions
|
|
|
787
805
|
- `scopeId` — the scopeId of the scope holding the wrap call site (fallback join).
|
|
788
806
|
- `wrapsTypePath` — the mnemonica fullPath of the instance argument, resolved through the scope-variable chain (innermost binding wins; an untyped local shadows a typed outer one rather than being guessed).
|
|
789
807
|
- `via` — the location of the enclosing wrap site when the call is nested inside another wrapped body (or returns a function): the wrappers-graph generation chain is built from it.
|
|
790
|
-
- `scope` — the enclosing mnemonica type path. Lexical scoping (the owning `define()`/`lazy()` handler or `@decorate()`-ed class) wins; a wrap site outside any handler — the fire-and-forget-wrapper shape — is attributed through its instance/context argument (a tracked assignment like `const holder = new Holder(…)`,
|
|
808
|
+
- `scope` — the enclosing mnemonica type path. Lexical scoping (the owning `define()`/`lazy()` handler or `@decorate()`-ed class) wins; a wrap site outside any handler — the fire-and-forget-wrapper shape — is attributed through its instance/context argument (a tracked assignment like `const holder = new Holder(…)`, the enclosing function's parameter annotation resolved through the graph law, or a `let`/`var`/`const` declaration with an EXPLICIT type annotation like `let updateCommitted: LedgerUpdate;` — declaration-site typing only, no flow analysis), and nested sites (function-valued returns, lexically nested wraps) inherit the causing wrap site's scope down the `via` chain. An UNANNOTATED `let`/`var` still buckets under `unknown` (the analyzer does not track assignments flow-sensitively) — a `const` with an analyzable initializer stays the recommended discipline. Sites attributable to nothing stay under the `unknown` key with `scope` absent.
|
|
791
809
|
|
|
792
810
|
## Instrumentation Points
|
|
793
811
|
|
|
@@ -942,7 +960,7 @@ Mnemonica accepts the `exposeInstanceMethods` option at runtime, but tactica's `
|
|
|
942
960
|
|
|
943
961
|
The analyzer does not use `ts.Program.getTypeChecker()` for resolution. Plain TypeScript references in constructor signatures resolve through the importing file's import statements first, then its local declarations, and only then a unique same-named declaration across the scanned files — so a reference whose name is genuinely absent (or lives behind unresolvable specifiers) resolves to `unknown`, while a name several of your own files declare with no import to disambiguate fails hard (see above). References to mnemonica graph types follow the path-aware lookup law described above and fail hard instead of degrading to `unknown`.
|
|
944
962
|
|
|
945
|
-
Two expansion boundaries stay name-based and deliberate: **heritage** — `extends` chains are walked through the same import-aware machinery (depth-capped, cycle-guarded), but bases that resolve to an external/node_modules declaration or to a non-identifier expression (mixin calls like `extends mixin(X)`) are not followed — their inherited fields simply stay absent, same as before the walk existed. And **`typeof` fields** — only module-level const arrays with statically visible literal elements expand to the literal union (
|
|
963
|
+
Two expansion boundaries stay name-based and deliberate: **heritage** — `extends` chains are walked through the same import-aware machinery (depth-capped, cycle-guarded), but bases that resolve to an external/node_modules declaration or to a non-identifier expression (mixin calls like `extends mixin(X)`) are not followed — their inherited fields simply stay absent, same as before the walk existed. And **`typeof` fields** — only module-level const arrays with statically visible literal elements expand to the literal union, with the index suffix consumed entirely (`typeof statusList[number]` emits exactly `'active' | 'not_active' | 'hold'`, never `'hold'[number]`). The value-level twin — element access over a const-asserted literal array (`(<const>[…])[0]`, `([…] as const)[1]`, tracked `x[0]`) — infers the element literal under the same rules; non-numeric, out-of-range, spread, or non-literal elements and general `<T>` assertions (as opposed to `<const>`) stay `unknown`. Any assertion spelling is tracked (`[…] as const`, `satisfies`, `<const>[…]`), and signed numeric literals keep their sign (`-1 | 1`). Cross-file only when the const is imported by the declaring file. Every other `typeof` source — and any indexed-access target that fails to resolve — degrades the whole expression to `unknown`: an index suffix glued onto a fallback type (`unknown[number]`) is invalid TypeScript and is never emitted.
|
|
946
964
|
|
|
947
965
|
### Same-file interface merging is last-wins
|
|
948
966
|
|
package/lib/analyzer.d.ts
CHANGED
|
@@ -22,6 +22,7 @@ export declare class MnemonicaAnalyzer {
|
|
|
22
22
|
private wrapEntryByNode;
|
|
23
23
|
private variableToTypeMap;
|
|
24
24
|
private moduleObjectVariables;
|
|
25
|
+
private mnemonicaNamedImports;
|
|
25
26
|
private createTypesCollectionVariables;
|
|
26
27
|
private collectionVariables;
|
|
27
28
|
private collectionInfo;
|
|
@@ -137,12 +138,35 @@ export declare class MnemonicaAnalyzer {
|
|
|
137
138
|
private findReferencedConstArray;
|
|
138
139
|
/**
|
|
139
140
|
* Element literal types of a tracked const array: every element must be
|
|
140
|
-
* a plain literal (optionally wrapped in `as const` / `satisfies`
|
|
141
|
-
* string, numeric
|
|
142
|
-
*
|
|
143
|
-
*
|
|
141
|
+
* a plain literal (optionally wrapped in `as const` / `satisfies` /
|
|
142
|
+
* `<const>` assertions) — string, numeric (unary `-`/`+` preserved),
|
|
143
|
+
* boolean, or null. Spreads, identifiers, and nested arrays mean the
|
|
144
|
+
* union is not statically visible and yield undefined, so the caller
|
|
145
|
+
* degrades the field to `unknown` rather than guessing.
|
|
144
146
|
*/
|
|
145
147
|
private literalTypesOfArray;
|
|
148
|
+
/**
|
|
149
|
+
* The literal type of one array element: a plain literal (optionally
|
|
150
|
+
* wrapped in `as const` / `satisfies` / assertion expressions) —
|
|
151
|
+
* string, numeric (unary `-`/`+` preserved), boolean, or null.
|
|
152
|
+
* Anything else yields undefined.
|
|
153
|
+
*/
|
|
154
|
+
private literalTypeOfExpression;
|
|
155
|
+
/**
|
|
156
|
+
* F22: the const-assertion check shared by the value-level and
|
|
157
|
+
* declaration-level paths — `expr as const` and `<const>expr` parse
|
|
158
|
+
* identically (a TypeReferenceNode named 'const'). General `<T>expr`
|
|
159
|
+
* assertions never match.
|
|
160
|
+
*/
|
|
161
|
+
private isConstAssertionType;
|
|
162
|
+
/**
|
|
163
|
+
* The array literal behind a value-level element access: inline
|
|
164
|
+
* (`(<const>[…])[0]`, `([…] as const)[1]`), parenthesized, or a
|
|
165
|
+
* tracked module const array (`const x = <const>[…]` / `x[0]`, F17
|
|
166
|
+
* tracking). Only const assertions are unwrapped — general
|
|
167
|
+
* assertions stay unknown (F22 scope boundary).
|
|
168
|
+
*/
|
|
169
|
+
private constArrayLiteralOf;
|
|
146
170
|
/**
|
|
147
171
|
* Emit-type for `typeof name` when `name` is a tracked const array: the
|
|
148
172
|
* union of its element literal types (`'active' | 'closed'`). Every
|
|
@@ -467,6 +491,13 @@ export declare class MnemonicaAnalyzer {
|
|
|
467
491
|
* For chained calls like const X = define('A').define('B'), we map X -> A (the root type)
|
|
468
492
|
*/
|
|
469
493
|
private trackVariableAssignment;
|
|
494
|
+
/**
|
|
495
|
+
* A `.define(...)` hop wrapped by another `.define(...)` call is not
|
|
496
|
+
* the value its const ends up holding — the OUTERMOST hop of the
|
|
497
|
+
* initializer chain is (define() returns the defined type's
|
|
498
|
+
* constructor). Only the outermost hop may bind the variable.
|
|
499
|
+
*/
|
|
500
|
+
private isDeeperDefineHop;
|
|
470
501
|
/**
|
|
471
502
|
* Mirror a variable -> mnemonica fullPath binding into the per-file
|
|
472
503
|
* value-scope map (graph identity law: `typeof X` and bare references
|
|
@@ -483,6 +514,59 @@ export declare class MnemonicaAnalyzer {
|
|
|
483
514
|
* e.g., const user = new UserType() maps "user" -> "UserType"
|
|
484
515
|
*/
|
|
485
516
|
private trackNewAssignment;
|
|
517
|
+
/**
|
|
518
|
+
* Bind the nearest enclosing `const/let/var X = …` to a mnemonica
|
|
519
|
+
* fullPath — the shared result-variable walker behind new/lookup/
|
|
520
|
+
* chain/fork/merge/call tracking (value scope: downstream references
|
|
521
|
+
* and `this.x = x` assignments resolve through the same binding).
|
|
522
|
+
*/
|
|
523
|
+
private bindResultVariable;
|
|
524
|
+
/**
|
|
525
|
+
* Record an `instantiation` usage for a construction-shape call
|
|
526
|
+
* (chain tip / call / apply / fork / clone / merge —
|
|
527
|
+
* byte-indistinguishable from `new` until the deferred
|
|
528
|
+
* mechanism-kind revision). `constructorText` defaults to the callee
|
|
529
|
+
* expression text so the site stays readable without new fields;
|
|
530
|
+
* call/apply override it with the Ctor argument text.
|
|
531
|
+
*/
|
|
532
|
+
private recordConstructionUsage;
|
|
533
|
+
/**
|
|
534
|
+
* Resolve the type a construction-chain tip call constructs:
|
|
535
|
+
* `new R(...).A(...)` constructs R.A; `await new R(...).A(...).B(...)`
|
|
536
|
+
* constructs R.A.B. The receiver is the nested chain (NewExpression
|
|
537
|
+
* base, then tip calls); exact fullPath first, and only when the root
|
|
538
|
+
* itself is unknown does the prop-name fallback law apply (so plain
|
|
539
|
+
* method calls on fresh instances never record a construction).
|
|
540
|
+
*/
|
|
541
|
+
private resolveChainTipTypePath;
|
|
542
|
+
/**
|
|
543
|
+
* True when `expr` denotes a construction function imported from
|
|
544
|
+
* 'mnemonica' — the named-import form (`import { call } from
|
|
545
|
+
* 'mnemonica'`, aliases included) or a member of a tracked
|
|
546
|
+
* module-object alias (`mnemonica.call`). Userland call/apply/bind
|
|
547
|
+
* functions never match.
|
|
548
|
+
*/
|
|
549
|
+
private isMnemonicaConstructionFn;
|
|
550
|
+
/**
|
|
551
|
+
* mnemonica call/apply(entity, Ctor, ...) / bind(entity, Ctor):
|
|
552
|
+
* resolve the Ctor argument (arg 1) to a graph fullPath through the
|
|
553
|
+
* same tiers as the `new` branch (value scope for identifiers,
|
|
554
|
+
* chain resolution for property accesses).
|
|
555
|
+
*/
|
|
556
|
+
private resolveConstructionFnTypePath;
|
|
557
|
+
/**
|
|
558
|
+
* instance.fork(...) / instance.clone(...) on a tracked variable —
|
|
559
|
+
* runtime returns `this`, so the result carries the source type.
|
|
560
|
+
*/
|
|
561
|
+
private resolveForkLikeTypePath;
|
|
562
|
+
/**
|
|
563
|
+
* Free utils forms: utils.merge(a, b, ...) (also the direct named
|
|
564
|
+
* import `merge(a, b)`) and the curried utils.fork(instance)(...).
|
|
565
|
+
* The result binds to arg 0's type — runtime returns a's lineage over
|
|
566
|
+
* b's context; a's fullPath is the honest approximation within the
|
|
567
|
+
* output contract (documented in README).
|
|
568
|
+
*/
|
|
569
|
+
private resolveUtilsFnTypePath;
|
|
486
570
|
/**
|
|
487
571
|
* Process a @decorate() decorator
|
|
488
572
|
*/
|
|
@@ -666,6 +750,24 @@ export declare class MnemonicaAnalyzer {
|
|
|
666
750
|
* undefined.
|
|
667
751
|
*/
|
|
668
752
|
private resolveParameterAnnotationTypePath;
|
|
753
|
+
/**
|
|
754
|
+
* F20 cheap tier: the wrap argument is an identifier declared with an
|
|
755
|
+
* EXPLICIT type annotation (`let updateCommitted: LedgerUpdate;`
|
|
756
|
+
* assigned later in a flow the analyzer does not track). The
|
|
757
|
+
* annotation resolves through the same graph tiers as parameter
|
|
758
|
+
* annotations. Deliberately NOT flow-sensitive: an UNANNOTATED
|
|
759
|
+
* let/var still buckets unknown, and a const with an analyzable
|
|
760
|
+
* initializer stays the recommended discipline. The lookup walks the
|
|
761
|
+
* enclosing statement containers innermost-out, so a shadowing inner
|
|
762
|
+
* declaration wins.
|
|
763
|
+
*/
|
|
764
|
+
private resolveVariableAnnotationTypePath;
|
|
765
|
+
/**
|
|
766
|
+
* First variable declaration carrying an explicit bare-identifier type
|
|
767
|
+
* annotation for `name` in the given statement list, resolved through
|
|
768
|
+
* the graph law.
|
|
769
|
+
*/
|
|
770
|
+
private findAnnotatedVariableTypePath;
|
|
669
771
|
/**
|
|
670
772
|
* Resolve a wrap() argument to its function node without the type
|
|
671
773
|
* checker: direct function expressions/arrows, or same-file bindings
|