tangled
alpha
login
or
join now
danabra.mov
/
typelex
56
fork
atom
An experimental TypeSpec syntax for Lexicon
56
fork
atom
overview
issues
1
pulls
2
pipelines
wip
danabra.mov
5 months ago
2359e07c
c8da387a
+363
-17
2 changed files
expand all
collapse all
unified
split
PLAN.md
packages
emitter
src
emitter.ts
+360
PLAN.md
···
1
1
+
# Emitter Refactoring Plan
2
2
+
3
3
+
**Goal:** Improve robustness, correctness, and maintainability while keeping tests passing throughout.
4
4
+
5
5
+
**Constraints:**
6
6
+
- Work incrementally in small, testable chunks
7
7
+
- Preserve input/output behavior (no breaking changes)
8
8
+
- Structure matters more than brevity
9
9
+
- Throw early on unsupported patterns rather than fail silently
10
10
+
11
11
+
---
12
12
+
13
13
+
## Phase 1: Type Safety & Helper Usage (Low Risk)
14
14
+
15
15
+
### 1.1 Replace custom array detection with TypeSpec helper
16
16
+
17
17
+
**Current (emitter.ts:1456-1469):**
18
18
+
```typescript
19
19
+
private isArrayType(model: Model): boolean {
20
20
+
if (model.name === "Array" && model.namespace?.name === "TypeSpec") {
21
21
+
return true;
22
22
+
}
23
23
+
if (model.sourceModel) {
24
24
+
return this.isArrayType(model.sourceModel);
25
25
+
}
26
26
+
return false;
27
27
+
}
28
28
+
```
29
29
+
30
30
+
**Issue:** Custom recursive check that only follows `sourceModel` chain.
31
31
+
32
32
+
**Action:**
33
33
+
- Import `isArrayModelType` from `@typespec/compiler` (used in openapi3 emitter)
34
34
+
- Replace `this.isArrayType(model)` calls with `isArrayModelType(this.program, model)`
35
35
+
- Remove `isArrayType` method
36
36
+
- **Test checkpoint:** `npm test`
37
37
+
38
38
+
**Risk:** Low - TypeSpec helper is more robust
39
39
+
**Lines affected:** 355, 1105, 1456-1469
40
40
+
41
41
+
---
42
42
+
43
43
+
### 1.2 Fix diagnostic type signatures (remove `as any`)
44
44
+
45
45
+
**Current pattern:**
46
46
+
```typescript
47
47
+
this.program.reportDiagnostic({
48
48
+
code: "invalid-model-name",
49
49
+
message: "...",
50
50
+
target: model,
51
51
+
} as any);
52
52
+
```
53
53
+
54
54
+
**Issue:** 8+ diagnostics cast to `as any` - hiding type errors
55
55
+
56
56
+
**Action:**
57
57
+
- Check TypeSpec diagnostic API - likely just needs `severity` field
58
58
+
- Add proper type signature or create helper:
59
59
+
```typescript
60
60
+
private reportError(code: string, message: string, target: any) {
61
61
+
this.program.reportDiagnostic({
62
62
+
code,
63
63
+
message,
64
64
+
target,
65
65
+
severity: "error"
66
66
+
});
67
67
+
}
68
68
+
```
69
69
+
- Replace all `as any` diagnostic casts
70
70
+
- **Test checkpoint:** `npm test`
71
71
+
72
72
+
**Risk:** Very low - just fixing types
73
73
+
**Lines affected:** 327, 530, 540, 562, 570, 580, 697, 1237, 1241
74
74
+
75
75
+
---
76
76
+
77
77
+
### 1.3 Simplify Blob detection
78
78
+
79
79
+
**Current (emitter.ts:996-1005):** 5 different checks for Blob
80
80
+
81
81
+
**Analysis needed:**
82
82
+
- Understand when each condition actually triggers
83
83
+
- Check if `isBlob(this.program, model)` decorator check is sufficient
84
84
+
- If templates, check `isTemplateInstance(model)` and model.name/baseModel
85
85
+
86
86
+
**Action:**
87
87
+
- Add logging to see which conditions actually trigger in tests
88
88
+
- Simplify to canonical pattern:
89
89
+
```typescript
90
90
+
const isBlobModel = isBlob(this.program, model) ||
91
91
+
(isTemplateInstance(model) && model.templateNode && isBlob(this.program, model.templateNode));
92
92
+
```
93
93
+
- **Test checkpoint:** `npm test`
94
94
+
- If tests fail, understand why and adjust
95
95
+
96
96
+
**Risk:** Low-medium - but need to verify against test cases
97
97
+
**Lines affected:** 996-1005
98
98
+
99
99
+
---
100
100
+
101
101
+
## Phase 2: Union Handling (Medium Risk)
102
102
+
103
103
+
### 2.1 Audit union processing against lexicon spec
104
104
+
105
105
+
**Current issue:** Union loop (lines 1146-1254) only handles two cases:
106
106
+
1. String literals + `string` type → `knownValues`
107
107
+
2. Model refs → `union`
108
108
+
109
109
+
**What about:**
110
110
+
- Model refs + string literals?
111
111
+
- Integer literals + `integer` type?
112
112
+
- Boolean literals?
113
113
+
- Nested unions?
114
114
+
- Empty unions?
115
115
+
116
116
+
**Action:**
117
117
+
- Read lexicon spec sections on `union` and `string.knownValues`
118
118
+
- Map all possible TypeSpec union patterns to lexicon output
119
119
+
- Create decision tree:
120
120
+
```
121
121
+
Union variants:
122
122
+
├─ All string literals + string type? → knownValues
123
123
+
├─ All integer literals + integer type? → enum (if supported)
124
124
+
├─ All model refs? → union
125
125
+
├─ Model refs + literals? → ERROR (unsupported) or handle
126
126
+
├─ Contains unknown/never? → open union
127
127
+
└─ Other? → throw "Unsupported union pattern"
128
128
+
```
129
129
+
130
130
+
**Test checkpoint:** `npm test` after each case added
131
131
+
132
132
+
---
133
133
+
134
134
+
### 2.2 Implement explicit union handling
135
135
+
136
136
+
**Pattern:**
137
137
+
```typescript
138
138
+
// Collect variants by category
139
139
+
const variants = {
140
140
+
modelRefs: [] as string[],
141
141
+
stringLiterals: [] as string[],
142
142
+
integerLiterals: [] as number[],
143
143
+
hasStringType: false,
144
144
+
hasIntegerType: false,
145
145
+
hasUnknown: false,
146
146
+
};
147
147
+
148
148
+
for (const variant of unionType.variants.values()) {
149
149
+
// Categorize each variant
150
150
+
}
151
151
+
152
152
+
// Decision logic with explicit throws
153
153
+
if (variants.stringLiterals.length > 0 && variants.hasStringType && variants.modelRefs.length === 0) {
154
154
+
// knownValues case
155
155
+
return { type: "string", knownValues: variants.stringLiterals };
156
156
+
}
157
157
+
158
158
+
if (variants.modelRefs.length > 0 && variants.stringLiterals.length === 0) {
159
159
+
// union case
160
160
+
return { type: "union", refs: variants.modelRefs };
161
161
+
}
162
162
+
163
163
+
// Unsupported combinations
164
164
+
if (variants.modelRefs.length > 0 && variants.stringLiterals.length > 0) {
165
165
+
throw new Error(`Union contains both model refs and string literals - unsupported pattern at ${unionType}`);
166
166
+
}
167
167
+
168
168
+
// Unknown pattern
169
169
+
throw new Error(`Unsupported union pattern: ${JSON.stringify(variants)}`);
170
170
+
```
171
171
+
172
172
+
**Test checkpoint:** `npm test` - may reveal unsupported patterns in tests
173
173
+
174
174
+
**Risk:** Medium - may expose edge cases
175
175
+
**Lines affected:** 1123-1255
176
176
+
177
177
+
---
178
178
+
179
179
+
## Phase 3: Mutable State Refactoring (Higher Risk)
180
180
+
181
181
+
### 3.1 Research TypeSpec context patterns
182
182
+
183
183
+
**Action:**
184
184
+
- Read openapi3 and json-schema emitters for context passing
185
185
+
- Look for how they track "current scope" for reference generation
186
186
+
- Understand if they use:
187
187
+
- Context objects passed through call stack?
188
188
+
- Builder pattern with scoped state?
189
189
+
- Other pattern?
190
190
+
191
191
+
**Deliverable:** Document the pattern used by reference emitters
192
192
+
193
193
+
---
194
194
+
195
195
+
### 3.2 Design context-based reference system
196
196
+
197
197
+
**Current problem:**
198
198
+
```typescript
199
199
+
private currentLexiconId: string | null = null; // Set/unset during traversal
200
200
+
201
201
+
// In processNamespace:
202
202
+
this.currentLexiconId = lexiconId;
203
203
+
// ... do work ...
204
204
+
this.currentLexiconId = null;
205
205
+
206
206
+
// In getModelReference:
207
207
+
if (this.currentLexiconId) {
208
208
+
// Use for local vs global ref decision
209
209
+
}
210
210
+
```
211
211
+
212
212
+
**Issue:** Reference generation depends on traversal order and state timing
213
213
+
214
214
+
**Proposed solution:**
215
215
+
```typescript
216
216
+
// Pass lexicon context explicitly
217
217
+
private typeToLexiconDefinition(
218
218
+
type: Type,
219
219
+
prop?: ModelProperty,
220
220
+
isDefining?: boolean,
221
221
+
context?: { currentLexiconId: string } // NEW
222
222
+
): LexiconDefinition | null {
223
223
+
// Use context.currentLexiconId instead of this.currentLexiconId
224
224
+
}
225
225
+
```
226
226
+
227
227
+
**Alternative:** Create a builder class per lexicon
228
228
+
```typescript
229
229
+
class LexiconBuilder {
230
230
+
constructor(private lexiconId: string, private program: Program) {}
231
231
+
232
232
+
addModel(model: Model) { /* uses this.lexiconId for context */ }
233
233
+
typeToDefinition(type: Type) { /* knows its lexicon scope */ }
234
234
+
build(): LexiconDocument { /* returns final document */ }
235
235
+
}
236
236
+
```
237
237
+
238
238
+
**Action:**
239
239
+
- Choose pattern based on TypeSpec conventions research
240
240
+
- **Test checkpoint:** `npm test` after refactoring each method
241
241
+
242
242
+
**Risk:** High - touches reference generation throughout
243
243
+
**Lines affected:** 56-57, 108, 139, 207, 227, 255, 265, 307, and all reference generation methods
244
244
+
245
245
+
---
246
246
+
247
247
+
## Phase 4: Edge Cases & Validation (Lower Priority)
248
248
+
249
249
+
### 4.1 Add validation for unsupported namespace patterns
250
250
+
251
251
+
**Current:** Heuristics try to guess intent (lines 77-315)
252
252
+
253
253
+
**Action:**
254
254
+
- Document supported patterns explicitly
255
255
+
- Add early validation:
256
256
+
```typescript
257
257
+
// After determining what namespace contains
258
258
+
if (hasModels && hasOperations) {
259
259
+
throw new Error(`Namespace ${fullName} contains both models and operations - unclear intent. Put operations in separate namespace or models in .defs namespace.`);
260
260
+
}
261
261
+
```
262
262
+
263
263
+
**Test checkpoint:** May fail on edge cases - decide whether to support or reject
264
264
+
265
265
+
---
266
266
+
267
267
+
### 4.2 Template argument extraction cleanup
268
268
+
269
269
+
**Current (lines 1012-1042):** Handles multiple representations inconsistently
270
270
+
271
271
+
**Action:**
272
272
+
- Research correct TypeSpec API for template value extraction
273
273
+
- Use standard serialization (see `serializeValueAsJson` in json-schema emitter)
274
274
+
- Simplify to canonical approach
275
275
+
276
276
+
**Risk:** Medium - but after mutable state is fixed
277
277
+
**Lines affected:** 1012-1042
278
278
+
279
279
+
---
280
280
+
281
281
+
## Phase 5: Structural Improvements (Optional)
282
282
+
283
283
+
### 5.1 Consider decomposing typeToLexiconDefinition
284
284
+
285
285
+
**Only if:** Pattern emerges naturally from other refactorings
286
286
+
287
287
+
**Current:** 400-line switch statement
288
288
+
289
289
+
**Possible structure:**
290
290
+
```typescript
291
291
+
private typeToLexiconDefinition(type: Type, prop?: ModelProperty, context?: Context) {
292
292
+
switch (type.kind) {
293
293
+
case "Model":
294
294
+
return this.modelToDefinition(type as Model, prop, context);
295
295
+
case "Union":
296
296
+
return this.unionToDefinition(type as Union, prop, context);
297
297
+
// etc
298
298
+
}
299
299
+
}
300
300
+
```
301
301
+
302
302
+
**Don't force it** - only if it improves clarity
303
303
+
304
304
+
---
305
305
+
306
306
+
## Execution Strategy
307
307
+
308
308
+
### Iteration Pattern:
309
309
+
1. Make small, focused change
310
310
+
2. Run `npm test`
311
311
+
3. If tests fail:
312
312
+
- Understand why
313
313
+
- Either fix the change or understand the case and throw explicitly
314
314
+
4. Commit mentally (or actually) before next change
315
315
+
5. If stuck, revert and try smaller change
316
316
+
317
317
+
### Change Order:
318
318
+
1. **Phase 1.1** (array helper) - safest, establishes pattern
319
319
+
2. **Phase 1.2** (diagnostic types) - cleanup, low risk
320
320
+
3. **Phase 1.3** (blob detection) - need to understand the shape
321
321
+
4. **Phase 2.1** (union audit) - research and planning
322
322
+
5. **Phase 2.2** (union implementation) - may expose edge cases
323
323
+
6. **Phase 3.1-3.2** (mutable state) - highest value, highest risk
324
324
+
7. **Phase 4+** - as needed based on findings
325
325
+
326
326
+
### Recovery Strategy:
327
327
+
- If a phase gets too complex, STOP
328
328
+
- Document what was learned
329
329
+
- Try smaller scope
330
330
+
- Ask for guidance if unclear
331
331
+
332
332
+
---
333
333
+
334
334
+
## Success Criteria
335
335
+
336
336
+
✅ **Must have:**
337
337
+
- All tests pass
338
338
+
- No `currentLexiconId` mutable state
339
339
+
- Explicit throws on unsupported patterns (no silent failures)
340
340
+
- Union handling matches lexicon spec
341
341
+
342
342
+
✅ **Nice to have:**
343
343
+
- Reduced `as any` usage
344
344
+
- Use TypeSpec helpers where appropriate
345
345
+
- Clearer structure (without forcing it)
346
346
+
347
347
+
✅ **Avoid:**
348
348
+
- Breaking existing functionality
349
349
+
- Over-engineering "nice" code
350
350
+
- Getting stuck in large refactorings
351
351
+
352
352
+
---
353
353
+
354
354
+
## Next Steps
355
355
+
356
356
+
1. Read this plan
357
357
+
2. Start with Phase 1.1 (array helper replacement)
358
358
+
3. Work incrementally
359
359
+
4. Report findings and blockers
360
360
+
5. Adjust plan as understanding improves
+3
-17
packages/emitter/src/emitter.ts
···
15
15
getMaxValue,
16
16
getMaxItems,
17
17
getMinItems,
18
18
+
isArrayModelType,
18
19
} from "@typespec/compiler";
19
20
import { join, dirname } from "path";
20
21
import type {
···
352
353
353
354
// Check if this model is actually an array type (via `is` declaration)
354
355
// e.g., `model Preferences is SomeUnion[]`
355
355
-
if (this.isArrayType(model)) {
356
356
+
if (isArrayModelType(this.program, model)) {
356
357
const arrayDef = this.modelToLexiconArray(model);
357
358
if (arrayDef) {
358
359
const description = getDoc(this.program, model);
···
1102
1103
}
1103
1104
1104
1105
// Check for anonymous array types (inline arrays like `SomeModel[]` in property)
1105
1105
-
if (this.isArrayType(type as Model)) {
1106
1106
+
if (isArrayModelType(this.program, type as Model)) {
1106
1107
const array = this.modelToLexiconArray(type as Model, prop);
1107
1108
if (array && prop) {
1108
1109
const propDesc = getDoc(this.program, prop);
···
1451
1452
}
1452
1453
1453
1454
return primitive;
1454
1454
-
}
1455
1455
-
1456
1456
-
private isArrayType(model: Model): boolean {
1457
1457
-
// Direct check: is this the TypeSpec.Array model?
1458
1458
-
if (model.name === "Array" && model.namespace?.name === "TypeSpec") {
1459
1459
-
return true;
1460
1460
-
}
1461
1461
-
1462
1462
-
// Follow the sourceModel chain (for `is` declarations)
1463
1463
-
// e.g., `model Preferences is SomeUnion[]` -> sourceModel is Array
1464
1464
-
if (model.sourceModel) {
1465
1465
-
return this.isArrayType(model.sourceModel);
1466
1466
-
}
1467
1467
-
1468
1468
-
return false;
1469
1455
}
1470
1456
1471
1457
private getModelReference(model: Model): string | null {