@mnemonica/tactica 0.3.5 → 0.3.8
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 +6 -1
- package/lib/analyzer.d.ts +55 -0
- package/lib/analyzer.js +279 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -464,16 +464,19 @@ The analyzer infers property types from constructor bodies and class members. Su
|
|
|
464
464
|
| `this.x = a + b` (where `a, b: number`) | `number` |
|
|
465
465
|
| `` this.x = `${a} ${b}` `` | `string` |
|
|
466
466
|
| `this.x = data.field` | type of `data.field` from the parameter annotation |
|
|
467
|
+
| `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 |
|
|
467
468
|
| `this.x = data.field \|\| []` | type of the fallback expression |
|
|
468
469
|
| `this.x = data.field ? a : b` | type of the truthy branch |
|
|
469
470
|
| `Object.assign(this, data)` | all fields of `data`'s type annotation |
|
|
470
471
|
| async / sync constructor functions | same rules |
|
|
471
472
|
|
|
473
|
+
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.
|
|
474
|
+
|
|
472
475
|
When inference fails the property's type falls back to `unknown` (or `any` in some Topologica paths) — safe, and you can refine manually.
|
|
473
476
|
|
|
474
477
|
### Referenced types in constructor signatures
|
|
475
478
|
|
|
476
|
-
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. 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).
|
|
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).
|
|
477
480
|
|
|
478
481
|
**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:
|
|
479
482
|
|
|
@@ -939,6 +942,8 @@ Mnemonica accepts the `exposeInstanceMethods` option at runtime, but tactica's `
|
|
|
939
942
|
|
|
940
943
|
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`.
|
|
941
944
|
|
|
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 (cross-file only when the const is imported by the declaring file); every other `typeof` source degrades the field to `unknown`.
|
|
946
|
+
|
|
942
947
|
### Same-file interface merging is last-wins
|
|
943
948
|
|
|
944
949
|
Two same-named interfaces in ONE module are legal TypeScript declaration merging, and tactica does not treat them as ambiguity (the fatal class is same-named declarations across several files, unanchored by an import). Resolution keeps a single declaration per name per file, and it is the last one — members from earlier declarations are not combined:
|
package/lib/analyzer.d.ts
CHANGED
|
@@ -37,6 +37,7 @@ export declare class MnemonicaAnalyzer {
|
|
|
37
37
|
private referencedTypeNamespaces;
|
|
38
38
|
private referencedTypeNamespaceStars;
|
|
39
39
|
private referencedTypeResolutionCache;
|
|
40
|
+
private referencedTypeConstArrays;
|
|
40
41
|
private referencedTypeCompilerOptions;
|
|
41
42
|
private currentReferencedTypeFile;
|
|
42
43
|
private expandingReferencedAliases;
|
|
@@ -117,6 +118,40 @@ export declare class MnemonicaAnalyzer {
|
|
|
117
118
|
* interface) for the file currently being visited.
|
|
118
119
|
*/
|
|
119
120
|
private trackReferencedTypeDeclaration;
|
|
121
|
+
/**
|
|
122
|
+
* Record consts initialized with an array literal (optionally wrapped in
|
|
123
|
+
* `as const` / `satisfies`), so a `typeof statusList[number]` field type
|
|
124
|
+
* expands to the element literal union — the generated file carries no
|
|
125
|
+
* imports, so emitting the bare `typeof statusList` query would be an
|
|
126
|
+
* unresolvable name downstream. First binding wins: a nested shadow
|
|
127
|
+
* must not replace the module-level const the typeof refers to.
|
|
128
|
+
*/
|
|
129
|
+
private trackReferencedTypeConstArray;
|
|
130
|
+
/**
|
|
131
|
+
* Find the array literal behind a module const referenced through
|
|
132
|
+
* `typeof`: the declaring file's own consts first (the F13 case is a
|
|
133
|
+
* NON-exported const in the same module as the expanded class), then —
|
|
134
|
+
* when the file imports the name — the imported module's consts.
|
|
135
|
+
* External modules are never analyzed, so those yield nothing.
|
|
136
|
+
*/
|
|
137
|
+
private findReferencedConstArray;
|
|
138
|
+
/**
|
|
139
|
+
* 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, boolean, or null. Spreads, identifiers, and nested
|
|
142
|
+
* arrays mean the union is not statically visible and yield undefined,
|
|
143
|
+
* so the caller degrades the field to `unknown` rather than guessing.
|
|
144
|
+
*/
|
|
145
|
+
private literalTypesOfArray;
|
|
146
|
+
/**
|
|
147
|
+
* Emit-type for `typeof name` when `name` is a tracked const array: the
|
|
148
|
+
* union of its element literal types (`'active' | 'closed'`). Every
|
|
149
|
+
* other typeof source — non-array consts, functions, classes, names not
|
|
150
|
+
* tracked at all — yields undefined, so the caller degrades the field
|
|
151
|
+
* to `unknown`: a bare `typeof name` emitted into types.ts has no
|
|
152
|
+
* import to resolve against downstream.
|
|
153
|
+
*/
|
|
154
|
+
private typeOfConstArrayUnion;
|
|
120
155
|
/**
|
|
121
156
|
* Record the importing file's named/namespace/default import bindings so
|
|
122
157
|
* referenced-type names resolve through the file's own import statements
|
|
@@ -162,8 +197,28 @@ export declare class MnemonicaAnalyzer {
|
|
|
162
197
|
/**
|
|
163
198
|
* Properties of a referenced class/interface/alias-of-literal declaration,
|
|
164
199
|
* shared by `this:`-parameter expansion and inline type emission.
|
|
200
|
+
* Inherited members are included: the extends chain is walked
|
|
201
|
+
* (depth-capped, cycle-guarded) and parent fields merge first, the
|
|
202
|
+
* declaration's own fields overriding on name clash.
|
|
165
203
|
*/
|
|
166
204
|
private referencedDeclarationProperties;
|
|
205
|
+
private referencedDeclarationPropertiesInner;
|
|
206
|
+
/**
|
|
207
|
+
* Property signatures of interface/alias type-literal members, into
|
|
208
|
+
* the given map.
|
|
209
|
+
*/
|
|
210
|
+
private collectTypeElementProperties;
|
|
211
|
+
/**
|
|
212
|
+
* Resolve the heritage clause of a class (`extends Base`) or interface
|
|
213
|
+
* (`extends A, B`) to referenced-type declarations through the SAME
|
|
214
|
+
* import-aware machinery as plain references (the declaring file's own
|
|
215
|
+
* imports first, then its locals, then the unique program-wide
|
|
216
|
+
* declaration). Unresolvable or external bases yield nothing — their
|
|
217
|
+
* inherited fields simply stay absent, same as before this walk
|
|
218
|
+
* existed. Mixin calls (`extends mixin(X)`) and namespace access are
|
|
219
|
+
* not followed.
|
|
220
|
+
*/
|
|
221
|
+
private resolveHeritageDeclarations;
|
|
167
222
|
/**
|
|
168
223
|
* Expand a referenced-type declaration to a self-contained type string
|
|
169
224
|
* for emission into generated files: type aliases through inferType,
|