@di-framework/tsc 4.3.3 → 5.0.0

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 (3) hide show
  1. package/README.md +28 -8
  2. package/package.json +1 -1
  3. package/plugin/main.go +1028 -61
package/README.md CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  [`ttsc`](https://ttsc.dev) transform plugin that injects **runtime parameter checks** from your TypeScript types.
4
4
 
5
- Source stays plain TypeScript — no `assert()`, schemas, or decorators. On emit, function bodies get `typeof` / shape guards synthesized from parameter types.
5
+ Source stays plain TypeScript — no `assert()`, schemas, or decorators. On emit, function bodies get `typeof`, equality, and shape guards synthesized from parameter types.
6
6
 
7
- > MVP: function declarations with required identifier params; primitives + plain object/interface props. See [Limitations](#limitations).
7
+ Function declarations and expressions, arrows (including concise and async arrows), methods, and constructors are supported.
8
8
 
9
9
  `di-framework init` wires this by default. Use the steps below for an existing app.
10
10
 
@@ -69,18 +69,38 @@ function greet(user) {
69
69
  ## How it works
70
70
 
71
71
  1. `ttsc` loads the program (parse + typecheck).
72
- 2. This plugin walks `FunctionDeclaration` nodes.
72
+ 2. This plugin walks functions, arrows, methods, and constructors.
73
73
  3. For each required parameter, it reads the type (syntax keywords, else checker).
74
74
  4. It synthesizes `if` / `throw` AST nodes with typescript-go `NodeFactory` and prepends them to the body.
75
75
  5. `EmitAllRaw` prints JavaScript from the mutated AST.
76
76
 
77
- ## Limitations
77
+ ## Supported boundaries and types
78
+
79
+ The transformer handles function declarations/expressions, methods, constructors, and block- or expression-bodied arrows at any nesting level. Required, optional, default, rest, and simple destructured parameters are supported. Runtime predicates cover primitives, nullish and literal types, enums, accessible local class instances, sound bounded unions, plain structural objects, non-callable object-only intersections, erased primitive brands, template-literal strings, string index signatures, `Record<string, V>` and literal-key records, arrays/readonly arrays, and fixed tuples with optional tails.
78
80
 
79
- Skipped today:
81
+ ## Limitations
80
82
 
81
- - methods, arrow functions, function expressions
82
- - optional / default / rest / destructured parameters
83
- - unions, arrays, tuples, classes, branded types, typia-style tags
83
+ Unsupported paths are skipped as a whole rather than emitting a predicate that can reject valid input. Current intentional gaps are:
84
+
85
+ - defaults and rest elements nested inside destructuring patterns
86
+ - variadic tuples, imported class bindings, callable/constructable intersections, typia-style tags, and custom nominal predicates
87
+ - number-only and symbol index signatures, and advanced mapped/conditional types
88
+ - unions with unsupported members or more than 12 members
89
+ - `void`, `never`, and `unique symbol`
90
+
91
+ Arrays and `ReadonlyArray<T>` are checked with `Array.isArray` and a full element scan.
92
+ Fixed tuples enforce their allowed length and validate each position; optional tails are presence-gated.
93
+ Object-only intersections use the checker's flattened apparent properties, validating each required property once. Required properties are checked for presence before their value, including properties whose type admits `undefined`; optional object properties remain unchecked.
94
+ String index signatures and `Record<string, V>` scan every own enumerable string-keyed value with `Object.keys`. Literal-key records instead check each required key, including names that require bracket access.
95
+ Primitive intersections used as brands validate only their string, number, boolean, bigint, or literal runtime representation. Recognized marker objects contain only computed unique-symbol properties, or conventional marker keys such as `__brand`, `_brand`, or `_tag` whose values are literals, unique symbols, or `never`. Brand marker properties are compile-time-only and are never read at runtime; class, callable, constructable, indexed, and ordinary structural intersections are not erased as brands. Enforcing application-specific brand semantics requires a future custom predicate facility.
96
+ Template-literal types always require a string. A template with one string-like placeholder also enforces its nonempty fixed prefix and suffix with `startsWith` and `endsWith`; multiple placeholders and non-string placeholders intentionally fall back to the string check.
97
+ Structural traversal keeps a per-path visited set and a maximum depth of eight, so recursive types cannot hang compilation.
98
+ Optional and defaulted parameters are validated only when their runtime value is not `undefined`; `null` is still checked against the declared type.
99
+ Rest parameters use the same full array and element validation as ordinary arrays.
100
+ Simple object, array, and nested destructured bindings are validated using the types of the bound identifiers.
101
+
102
+ Numeric, string, and const enums are checked against their checker-known member values. Enums with a computed member are skipped as a whole because the computed runtime value cannot be reproduced soundly without depending on an emitted enum object.
103
+ Class-typed parameters use `instanceof` when the checker can name an accessible local or local-namespace runtime constructor; interfaces remain structural. Subclasses pass the check, and generic type arguments are erased at runtime. As with any `instanceof` test, equivalent instances created in another JavaScript realm do not pass. ES-imported class bindings are conservatively skipped, including ordinary value imports, because TypeScript can elide an import used only as a type and the transformer does not synthesize or retain imports.
84
104
 
85
105
  ## Monorepo
86
106
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@di-framework/tsc",
3
- "version": "4.3.3",
3
+ "version": "5.0.0",
4
4
  "description": "ttsc transform that injects runtime parameter checks from TypeScript types (assertAll)",
5
5
  "author": "@di-framework Contributors",
6
6
  "license": "(MIT OR Apache-2.0)",
package/plugin/main.go CHANGED
@@ -9,7 +9,9 @@ import (
9
9
  "fmt"
10
10
  "os"
11
11
  "path/filepath"
12
+ "strconv"
12
13
  "strings"
14
+ "unicode"
13
15
 
14
16
  shimast "github.com/microsoft/typescript-go/shim/ast"
15
17
  shimchecker "github.com/microsoft/typescript-go/shim/checker"
@@ -183,8 +185,31 @@ func injectFile(factory *shimast.NodeFactory, file *shimast.SourceFile, checker
183
185
  if node == nil {
184
186
  return
185
187
  }
186
- if node.Kind == shimast.KindFunctionDeclaration {
187
- injectFunction(factory, checker, node)
188
+ switch node.Kind {
189
+ case shimast.KindFunctionDeclaration:
190
+ fn := node.AsFunctionDeclaration()
191
+ injectCallable(factory, checker, node, fn.Parameters, fn.Body)
192
+ case shimast.KindMethodDeclaration:
193
+ method := node.AsMethodDeclaration()
194
+ injectCallable(factory, checker, node, method.Parameters, method.Body)
195
+ case shimast.KindConstructor:
196
+ ctor := node.AsConstructorDeclaration()
197
+ injectCallable(factory, checker, node, ctor.Parameters, ctor.Body)
198
+ case shimast.KindFunctionExpression:
199
+ fn := node.AsFunctionExpression()
200
+ injectCallable(factory, checker, node, fn.Parameters, fn.Body)
201
+ case shimast.KindArrowFunction:
202
+ arrow := node.AsArrowFunction()
203
+ if arrow.Body != nil && arrow.Body.Kind == shimast.KindBlock {
204
+ injectCallable(factory, checker, node, arrow.Parameters, arrow.Body)
205
+ } else if arrow.Body != nil {
206
+ checks := checksForParams(factory, checker, node, arrow.Parameters)
207
+ if len(checks) > 0 {
208
+ stmts := append(checks, factory.NewReturnStatement(arrow.Body))
209
+ arrow.Body = factory.NewBlock(factory.NewNodeList(stmts), true)
210
+ shimast.SetParentInChildren(node)
211
+ }
212
+ }
188
213
  }
189
214
  node.ForEachChild(func(child *shimast.Node) bool {
190
215
  walk(child)
@@ -198,47 +223,116 @@ func injectFile(factory *shimast.NodeFactory, file *shimast.SourceFile, checker
198
223
  }
199
224
  }
200
225
 
201
- func injectFunction(factory *shimast.NodeFactory, checker *shimchecker.Checker, node *shimast.Node) {
202
- fn := node.AsFunctionDeclaration()
203
- if fn == nil || fn.Body == nil || fn.Parameters == nil {
226
+ func injectCallable(
227
+ factory *shimast.NodeFactory,
228
+ checker *shimchecker.Checker,
229
+ enclosing *shimast.Node,
230
+ params *shimast.NodeList,
231
+ body *shimast.Node,
232
+ ) {
233
+ if body == nil || params == nil {
204
234
  return
205
235
  }
206
- block := fn.Body.AsBlock()
236
+ block := body.AsBlock()
207
237
  if block == nil || block.Statements == nil {
208
238
  return
209
239
  }
210
240
 
241
+ checks := checksForParams(factory, checker, enclosing, params)
242
+ if len(checks) == 0 {
243
+ return
244
+ }
245
+
246
+ // Structural prepend — printer walks Statements.Nodes directly.
247
+ out := make([]*shimast.Node, 0, len(checks)+len(block.Statements.Nodes))
248
+ out = append(out, checks...)
249
+ out = append(out, block.Statements.Nodes...)
250
+ block.Statements.Nodes = out
251
+ // Emit walks parents; factory nodes start detached.
252
+ shimast.SetParentInChildren(body)
253
+ }
254
+
255
+ func checksForParams(
256
+ factory *shimast.NodeFactory,
257
+ checker *shimchecker.Checker,
258
+ enclosing *shimast.Node,
259
+ params *shimast.NodeList,
260
+ ) []*shimast.Node {
261
+ if params == nil {
262
+ return nil
263
+ }
211
264
  var checks []*shimast.Node
212
- for _, param := range fn.Parameters.Nodes {
265
+ for _, param := range params.Nodes {
213
266
  if param == nil || param.Kind != shimast.KindParameter {
214
267
  continue
215
268
  }
216
269
  p := param.AsParameterDeclaration()
217
- if p == nil || p.Name() == nil || p.Name().Kind != shimast.KindIdentifier {
270
+ if p == nil || p.Name() == nil {
271
+ continue
272
+ }
273
+ if p.Name().Kind == shimast.KindObjectBindingPattern || p.Name().Kind == shimast.KindArrayBindingPattern {
274
+ checks = append(checks, checksForBindingName(factory, checker, enclosing, p.Name())...)
218
275
  continue
219
276
  }
220
- if p.DotDotDotToken != nil || p.QuestionToken != nil || p.Initializer != nil {
277
+ if p.Name().Kind != shimast.KindIdentifier {
221
278
  continue
222
279
  }
223
280
  name := p.Name().Text()
224
- checks = append(checks, checksForParam(factory, checker, param, name)...)
225
- }
226
- if len(checks) == 0 {
227
- return
281
+ paramChecks := checksForParam(factory, checker, enclosing, param, name)
282
+ if len(paramChecks) == 0 {
283
+ continue
284
+ }
285
+ if p.QuestionToken != nil || p.Initializer != nil {
286
+ present := binary(factory, pathExpr(factory, name), shimast.KindExclamationEqualsEqualsToken, factory.NewIdentifier("undefined"))
287
+ checks = append(checks, factory.NewIfStatement(present, factory.NewBlock(factory.NewNodeList(paramChecks), true), nil))
288
+ continue
289
+ }
290
+ checks = append(checks, paramChecks...)
228
291
  }
292
+ return checks
293
+ }
229
294
 
230
- // Structural prepend — printer walks Statements.Nodes directly.
231
- out := make([]*shimast.Node, 0, len(checks)+len(block.Statements.Nodes))
232
- out = append(out, checks...)
233
- out = append(out, block.Statements.Nodes...)
234
- block.Statements.Nodes = out
235
- // Emit walks parents; factory nodes start detached.
236
- shimast.SetParentInChildren(fn.Body)
295
+ func checksForBindingName(
296
+ factory *shimast.NodeFactory,
297
+ checker *shimchecker.Checker,
298
+ enclosing *shimast.Node,
299
+ name *shimast.Node,
300
+ ) []*shimast.Node {
301
+ if name == nil || checker == nil {
302
+ return nil
303
+ }
304
+ if name.Kind == shimast.KindIdentifier {
305
+ t := checker.GetTypeAtLocation(name)
306
+ if t == nil {
307
+ return nil
308
+ }
309
+ return stmtsFromCheckerType(factory, checker, enclosing, name.Text(), t)
310
+ }
311
+ if name.Kind != shimast.KindObjectBindingPattern && name.Kind != shimast.KindArrayBindingPattern {
312
+ return nil
313
+ }
314
+ pattern := name.AsBindingPattern()
315
+ if pattern == nil || pattern.Elements == nil {
316
+ return nil
317
+ }
318
+ var out []*shimast.Node
319
+ for _, raw := range pattern.Elements.Nodes {
320
+ if raw == nil || raw.Kind != shimast.KindBindingElement {
321
+ continue
322
+ }
323
+ element := raw.AsBindingElement()
324
+ if element == nil || element.Name() == nil || element.DotDotDotToken != nil || element.Initializer != nil {
325
+ continue
326
+ }
327
+ out = append(out, checksForBindingName(factory, checker, enclosing, element.Name())...)
328
+ }
329
+ return out
237
330
  }
238
331
 
239
332
  func checksForParam(
240
333
  factory *shimast.NodeFactory,
241
334
  checker *shimchecker.Checker,
335
+ enclosing *shimast.Node,
242
336
  param *shimast.Node,
243
337
  path string,
244
338
  ) []*shimast.Node {
@@ -255,7 +349,7 @@ func checksForParam(
255
349
  if t == nil {
256
350
  return nil
257
351
  }
258
- return stmtsFromCheckerType(factory, checker, path, t)
352
+ return stmtsFromCheckerType(factory, checker, enclosing, path, t)
259
353
  }
260
354
 
261
355
  func stmtsFromTypeNode(
@@ -283,27 +377,143 @@ func stmtsFromTypeNode(
283
377
  func stmtsFromCheckerType(
284
378
  factory *shimast.NodeFactory,
285
379
  checker *shimchecker.Checker,
380
+ enclosing *shimast.Node,
286
381
  path string,
287
382
  t *shimchecker.Type,
288
383
  ) []*shimast.Node {
289
- return stmtsFromCheckerTypeSeen(factory, checker, path, t, make(map[*shimchecker.Type]bool), 0)
384
+ return stmtsFromCheckerTypeSeen(factory, checker, enclosing, path, t, newPredicateState(path), 0)
385
+ }
386
+
387
+ type predicateState struct {
388
+ active map[*shimchecker.Type]bool
389
+ identifiers map[string]bool
390
+ }
391
+
392
+ func newPredicateState(path string) *predicateState {
393
+ rootEnd := strings.IndexAny(path, ".[")
394
+ if rootEnd < 0 {
395
+ rootEnd = len(path)
396
+ }
397
+ return &predicateState{
398
+ active: make(map[*shimchecker.Type]bool),
399
+ identifiers: map[string]bool{path[:rootEnd]: true},
400
+ }
401
+ }
402
+
403
+ func (state *predicateState) clone() *predicateState {
404
+ clone := &predicateState{
405
+ active: make(map[*shimchecker.Type]bool, len(state.active)),
406
+ identifiers: make(map[string]bool, len(state.identifiers)),
407
+ }
408
+ for t, active := range state.active {
409
+ clone.active[t] = active
410
+ }
411
+ for name, used := range state.identifiers {
412
+ clone.identifiers[name] = used
413
+ }
414
+ return clone
415
+ }
416
+
417
+ func (state *predicateState) freshIdentifier(base string) string {
418
+ if !state.identifiers[base] {
419
+ state.identifiers[base] = true
420
+ return base
421
+ }
422
+ for suffix := 1; ; suffix++ {
423
+ candidate := fmt.Sprintf("%s_%d", base, suffix)
424
+ if !state.identifiers[candidate] {
425
+ state.identifiers[candidate] = true
426
+ return candidate
427
+ }
428
+ }
290
429
  }
291
430
 
292
431
  func stmtsFromCheckerTypeSeen(
293
432
  factory *shimast.NodeFactory,
294
433
  checker *shimchecker.Checker,
434
+ enclosing *shimast.Node,
295
435
  path string,
296
436
  t *shimchecker.Type,
297
- seen map[*shimchecker.Type]bool,
437
+ state *predicateState,
298
438
  depth int,
299
439
  ) []*shimast.Node {
300
440
  // Platform and application interfaces can be recursive. Runtime guards are
301
441
  // deliberately bounded so a cyclic type graph cannot make compilation hang.
302
- if depth > 8 || seen[t] {
442
+ if depth > 8 || state.active[t] {
303
443
  return nil
304
444
  }
305
445
  flags := t.Flags()
306
446
  switch {
447
+ case flags&shimchecker.TypeFlagsEnumLike != 0:
448
+ invalid, ok := enumInvalidPredicate(factory, path, t)
449
+ if !ok {
450
+ return nil
451
+ }
452
+ return []*shimast.Node{throwIf(factory, invalid, "Expected "+path+" to be a valid enum value")}
453
+ case classTypeSymbol(t) != nil:
454
+ invalid, className, ok := classInvalidPredicate(factory, checker, enclosing, path, t)
455
+ if !ok {
456
+ return nil
457
+ }
458
+ return []*shimast.Node{throwIf(factory, invalid, "Expected "+path+" to be an instance of "+className)}
459
+ case flags&shimchecker.TypeFlagsUnion != 0:
460
+ members := t.Types()
461
+ if len(members) == 0 || len(members) > 12 {
462
+ return nil
463
+ }
464
+ var invalid *shimast.Expression
465
+ for _, member := range members {
466
+ memberInvalid, ok := invalidPredicate(factory, checker, enclosing, path, member, state, depth+1)
467
+ if !ok {
468
+ return nil
469
+ }
470
+ if invalid == nil {
471
+ invalid = memberInvalid
472
+ } else {
473
+ invalid = binary(factory, invalid, shimast.KindAmpersandAmpersandToken, memberInvalid)
474
+ }
475
+ }
476
+ return []*shimast.Node{throwIf(factory, invalid, "Expected "+path+" to match its union type")}
477
+ case flags&shimchecker.TypeFlagsIntersection != 0:
478
+ if base, ok := brandedBaseType(checker, t); ok {
479
+ return stmtsFromCheckerTypeSeen(factory, checker, enclosing, path, base, state, depth+1)
480
+ }
481
+ if !isStructuralIntersection(checker, t) {
482
+ return nil
483
+ }
484
+ return structuralStatements(factory, checker, enclosing, path, t, state, depth)
485
+ case flags&shimchecker.TypeFlagsNull != 0:
486
+ return []*shimast.Node{equalityCheck(factory, path, factory.NewToken(shimast.KindNullKeyword), "null")}
487
+ case flags&shimchecker.TypeFlagsUndefined != 0:
488
+ return []*shimast.Node{equalityCheck(factory, path, factory.NewIdentifier("undefined"), "undefined")}
489
+ case flags&shimchecker.TypeFlagsStringLiteral != 0:
490
+ value, ok := t.AsLiteralType().Value().(string)
491
+ if !ok {
492
+ return nil
493
+ }
494
+ return []*shimast.Node{equalityCheck(factory, path, factory.NewStringLiteral(value, shimast.TokenFlagsNone), fmt.Sprintf("%q", value))}
495
+ case flags&shimchecker.TypeFlagsNumberLiteral != 0:
496
+ value := fmt.Sprint(t.AsLiteralType().Value())
497
+ return []*shimast.Node{equalityCheck(factory, path, factory.NewNumericLiteral(value, shimast.TokenFlagsNone), value)}
498
+ case flags&shimchecker.TypeFlagsBooleanLiteral != 0:
499
+ value, ok := t.AsLiteralType().Value().(bool)
500
+ if !ok {
501
+ return nil
502
+ }
503
+ kind, label := shimast.KindFalseKeyword, "false"
504
+ if value {
505
+ kind, label = shimast.KindTrueKeyword, "true"
506
+ }
507
+ return []*shimast.Node{equalityCheck(factory, path, factory.NewToken(kind), label)}
508
+ case flags&shimchecker.TypeFlagsBigIntLiteral != 0:
509
+ value := fmt.Sprint(t.AsLiteralType().Value()) + "n"
510
+ return []*shimast.Node{equalityCheck(factory, path, factory.NewBigIntLiteral(value, shimast.TokenFlagsNone), value)}
511
+ case flags&shimchecker.TypeFlagsTemplateLiteral != 0:
512
+ invalid, ok := templateLiteralInvalidPredicate(factory, path, t)
513
+ if !ok {
514
+ return nil
515
+ }
516
+ return []*shimast.Node{throwIf(factory, invalid, "Expected "+path+" to match its template literal type")}
307
517
  case flags&shimchecker.TypeFlagsString != 0:
308
518
  return []*shimast.Node{typeofCheck(factory, path, "string")}
309
519
  case flags&shimchecker.TypeFlagsNumber != 0:
@@ -312,56 +522,813 @@ func stmtsFromCheckerTypeSeen(
312
522
  return []*shimast.Node{typeofCheck(factory, path, "boolean")}
313
523
  case flags&shimchecker.TypeFlagsBigInt != 0:
314
524
  return []*shimast.Node{typeofCheck(factory, path, "bigint")}
315
- case flags&shimchecker.TypeFlagsObject != 0:
316
- seen[t] = true
317
- defer delete(seen, t)
318
- props := shimchecker.Checker_getApparentProperties(checker, t)
319
- if len(props) == 0 {
525
+ case flags&shimchecker.TypeFlagsObject != 0 && checker.IsArrayLikeType(t) && !shimchecker.IsTupleType(t):
526
+ invalid, ok := arrayInvalidPredicate(factory, checker, enclosing, path, t, state, depth)
527
+ if !ok {
320
528
  return nil
321
529
  }
322
- out := []*shimast.Node{objectCheck(factory, path)}
323
- for _, sym := range props {
324
- if sym == nil {
325
- continue
530
+ return []*shimast.Node{throwIf(factory, invalid, "Expected "+path+" to be an array with valid elements")}
531
+ case flags&shimchecker.TypeFlagsObject != 0 && shimchecker.IsTupleType(t):
532
+ invalid, ok := tupleInvalidPredicate(factory, checker, enclosing, path, t, state, depth)
533
+ if !ok {
534
+ return nil
535
+ }
536
+ return []*shimast.Node{throwIf(factory, invalid, "Expected "+path+" to be a valid tuple")}
537
+ case flags&shimchecker.TypeFlagsObject != 0:
538
+ return structuralStatements(factory, checker, enclosing, path, t, state, depth)
539
+ default:
540
+ return nil
541
+ }
542
+ }
543
+
544
+ // invalidPredicate returns a composable expression that is true when path does
545
+ // not satisfy t. The bool is false when a sound runtime predicate is not
546
+ // available; callers then skip the entire union rather than partially checking it.
547
+ func invalidPredicate(
548
+ factory *shimast.NodeFactory,
549
+ checker *shimchecker.Checker,
550
+ enclosing *shimast.Node,
551
+ path string,
552
+ t *shimchecker.Type,
553
+ state *predicateState,
554
+ depth int,
555
+ ) (*shimast.Expression, bool) {
556
+ if t == nil || depth > 8 || state.active[t] {
557
+ return nil, false
558
+ }
559
+ flags := t.Flags()
560
+ switch {
561
+ case flags&shimchecker.TypeFlagsEnumLike != 0:
562
+ return enumInvalidPredicate(factory, path, t)
563
+ case classTypeSymbol(t) != nil:
564
+ invalid, _, ok := classInvalidPredicate(factory, checker, enclosing, path, t)
565
+ return invalid, ok
566
+ case flags&shimchecker.TypeFlagsUnion != 0:
567
+ members := t.Types()
568
+ if len(members) == 0 || len(members) > 12 {
569
+ return nil, false
570
+ }
571
+ var out *shimast.Expression
572
+ for _, member := range members {
573
+ pred, ok := invalidPredicate(factory, checker, enclosing, path, member, state, depth+1)
574
+ if !ok {
575
+ return nil, false
326
576
  }
327
- propName := sym.Name
328
- if propName == "" || strings.HasPrefix(propName, "__") {
329
- continue
577
+ if out == nil {
578
+ out = pred
579
+ } else {
580
+ out = binary(factory, out, shimast.KindAmpersandAmpersandToken, pred)
330
581
  }
331
- if sym.Flags&shimast.SymbolFlagsOptional != 0 {
332
- continue
582
+ }
583
+ return out, true
584
+ case flags&shimchecker.TypeFlagsIntersection != 0:
585
+ if base, ok := brandedBaseType(checker, t); ok {
586
+ return invalidPredicate(factory, checker, enclosing, path, base, state, depth+1)
587
+ }
588
+ if !isStructuralIntersection(checker, t) {
589
+ return nil, false
590
+ }
591
+ return structuralInvalidPredicate(factory, checker, enclosing, path, t, state, depth)
592
+ case flags&shimchecker.TypeFlagsNull != 0:
593
+ return binary(factory, pathExpr(factory, path), shimast.KindExclamationEqualsEqualsToken, factory.NewToken(shimast.KindNullKeyword)), true
594
+ case flags&shimchecker.TypeFlagsUndefined != 0:
595
+ return binary(factory, pathExpr(factory, path), shimast.KindExclamationEqualsEqualsToken, factory.NewIdentifier("undefined")), true
596
+ case flags&shimchecker.TypeFlagsStringLiteral != 0:
597
+ value, ok := t.AsLiteralType().Value().(string)
598
+ if !ok {
599
+ return nil, false
600
+ }
601
+ return binary(factory, pathExpr(factory, path), shimast.KindExclamationEqualsEqualsToken, factory.NewStringLiteral(value, shimast.TokenFlagsNone)), true
602
+ case flags&shimchecker.TypeFlagsNumberLiteral != 0:
603
+ return binary(factory, pathExpr(factory, path), shimast.KindExclamationEqualsEqualsToken, factory.NewNumericLiteral(fmt.Sprint(t.AsLiteralType().Value()), shimast.TokenFlagsNone)), true
604
+ case flags&shimchecker.TypeFlagsBooleanLiteral != 0:
605
+ value, ok := t.AsLiteralType().Value().(bool)
606
+ if !ok {
607
+ return nil, false
608
+ }
609
+ kind := shimast.KindFalseKeyword
610
+ if value {
611
+ kind = shimast.KindTrueKeyword
612
+ }
613
+ return binary(factory, pathExpr(factory, path), shimast.KindExclamationEqualsEqualsToken, factory.NewToken(kind)), true
614
+ case flags&shimchecker.TypeFlagsBigIntLiteral != 0:
615
+ value := fmt.Sprint(t.AsLiteralType().Value()) + "n"
616
+ return binary(factory, pathExpr(factory, path), shimast.KindExclamationEqualsEqualsToken, factory.NewBigIntLiteral(value, shimast.TokenFlagsNone)), true
617
+ case flags&shimchecker.TypeFlagsTemplateLiteral != 0:
618
+ return templateLiteralInvalidPredicate(factory, path, t)
619
+ case flags&shimchecker.TypeFlagsString != 0:
620
+ return binary(factory, factory.NewTypeOfExpression(pathExpr(factory, path)), shimast.KindExclamationEqualsEqualsToken, factory.NewStringLiteral("string", shimast.TokenFlagsNone)), true
621
+ case flags&shimchecker.TypeFlagsNumber != 0:
622
+ return binary(factory, factory.NewTypeOfExpression(pathExpr(factory, path)), shimast.KindExclamationEqualsEqualsToken, factory.NewStringLiteral("number", shimast.TokenFlagsNone)), true
623
+ case flags&shimchecker.TypeFlagsBoolean != 0:
624
+ return binary(factory, factory.NewTypeOfExpression(pathExpr(factory, path)), shimast.KindExclamationEqualsEqualsToken, factory.NewStringLiteral("boolean", shimast.TokenFlagsNone)), true
625
+ case flags&shimchecker.TypeFlagsBigInt != 0:
626
+ return binary(factory, factory.NewTypeOfExpression(pathExpr(factory, path)), shimast.KindExclamationEqualsEqualsToken, factory.NewStringLiteral("bigint", shimast.TokenFlagsNone)), true
627
+ case flags&shimchecker.TypeFlagsObject != 0 && checker.IsArrayLikeType(t) && !shimchecker.IsTupleType(t):
628
+ return arrayInvalidPredicate(factory, checker, enclosing, path, t, state, depth)
629
+ case flags&shimchecker.TypeFlagsObject != 0 && shimchecker.IsTupleType(t):
630
+ return tupleInvalidPredicate(factory, checker, enclosing, path, t, state, depth)
631
+ case flags&shimchecker.TypeFlagsObject != 0:
632
+ return structuralInvalidPredicate(factory, checker, enclosing, path, t, state, depth)
633
+ default:
634
+ return nil, false
635
+ }
636
+ }
637
+
638
+ // brandedBaseType recognizes erased nominal types such as
639
+ // string & { readonly __brand: unique symbol }. Exactly one runtime-checkable
640
+ // primitive base is required; every other constituent must be a non-empty
641
+ // plain object marker. Marker properties are intentionally never inspected at
642
+ // runtime because TypeScript brands do not exist in emitted JavaScript.
643
+ func brandedBaseType(checker *shimchecker.Checker, t *shimchecker.Type) (*shimchecker.Type, bool) {
644
+ var base *shimchecker.Type
645
+ markerCount := 0
646
+ for _, member := range t.Types() {
647
+ if member == nil {
648
+ return nil, false
649
+ }
650
+ if isBrandBaseType(member) {
651
+ if base != nil {
652
+ return nil, false
333
653
  }
334
- propType := shimchecker.Checker_getTypeOfPropertyOfType(checker, t, propName)
335
- if propType == nil {
336
- continue
654
+ base = member
655
+ continue
656
+ }
657
+ if !isBrandMarkerType(checker, member) {
658
+ return nil, false
659
+ }
660
+ markerCount++
661
+ }
662
+ return base, base != nil && markerCount > 0
663
+ }
664
+
665
+ func isBrandBaseType(t *shimchecker.Type) bool {
666
+ flags := t.Flags()
667
+ return flags&(shimchecker.TypeFlagsString|shimchecker.TypeFlagsNumber|shimchecker.TypeFlagsBoolean|shimchecker.TypeFlagsBigInt|shimchecker.TypeFlagsStringLiteral|shimchecker.TypeFlagsNumberLiteral|shimchecker.TypeFlagsBooleanLiteral|shimchecker.TypeFlagsBigIntLiteral|shimchecker.TypeFlagsTemplateLiteral) != 0
668
+ }
669
+
670
+ func isBrandMarkerType(checker *shimchecker.Checker, t *shimchecker.Type) bool {
671
+ if t.Flags()&shimchecker.TypeFlagsObject == 0 ||
672
+ isClassObjectType(t) ||
673
+ checker.IsArrayLikeType(t) ||
674
+ shimchecker.IsTupleType(t) ||
675
+ len(shimchecker.Checker_getSignaturesOfType(checker, t, shimchecker.SignatureKindCall)) > 0 ||
676
+ len(shimchecker.Checker_getSignaturesOfType(checker, t, shimchecker.SignatureKindConstruct)) > 0 ||
677
+ len(shimchecker.Checker_getIndexInfosOfType(checker, t)) > 0 {
678
+ return false
679
+ }
680
+
681
+ properties := shimchecker.Checker_getApparentProperties(checker, t)
682
+ if len(properties) == 0 {
683
+ return false
684
+ }
685
+ for _, property := range properties {
686
+ if property == nil {
687
+ return false
688
+ }
689
+ if isUniqueSymbolMarkerProperty(property.Name) {
690
+ continue
691
+ }
692
+ if !conventionalBrandMarkerNames[property.Name] {
693
+ return false
694
+ }
695
+ propertyType := shimchecker.Checker_getTypeOfPropertyOfType(checker, t, property.Name)
696
+ if !isBrandMarkerValueType(propertyType) {
697
+ return false
698
+ }
699
+ }
700
+ return true
701
+ }
702
+
703
+ var conventionalBrandMarkerNames = map[string]bool{
704
+ "__brand": true,
705
+ "_brand": true,
706
+ "_tag": true,
707
+ "__tag": true,
708
+ "__type": true,
709
+ "_type": true,
710
+ "brand": true,
711
+ }
712
+
713
+ func isClassObjectType(t *shimchecker.Type) bool {
714
+ if t.ObjectFlags()&shimchecker.ObjectFlagsClass != 0 ||
715
+ t.Symbol() != nil && t.Symbol().Flags&shimast.SymbolFlagsClass != 0 {
716
+ return true
717
+ }
718
+ if t.ObjectFlags()&shimchecker.ObjectFlagsReference == 0 {
719
+ return false
720
+ }
721
+ target := t.Target()
722
+ return target != nil && (target.ObjectFlags()&shimchecker.ObjectFlagsClass != 0 ||
723
+ target.Symbol() != nil && target.Symbol().Flags&shimast.SymbolFlagsClass != 0)
724
+ }
725
+
726
+ // Unique symbol property names are encoded by typescript-go as
727
+ // <internal-prefix>@<symbol-name>@<numeric-id>. Well-known symbols omit the
728
+ // numeric identity suffix and are therefore not treated as nominal markers.
729
+ func isUniqueSymbolMarkerProperty(name string) bool {
730
+ if len(name) < 4 || name[0] != 0xfe || name[1] != '@' {
731
+ return false
732
+ }
733
+ lastAt := strings.LastIndexByte(name, '@')
734
+ if lastAt <= 1 || lastAt == len(name)-1 {
735
+ return false
736
+ }
737
+ _, err := strconv.ParseUint(name[lastAt+1:], 10, 64)
738
+ return err == nil
739
+ }
740
+
741
+ func isBrandMarkerValueType(t *shimchecker.Type) bool {
742
+ if t == nil {
743
+ return false
744
+ }
745
+ flags := t.Flags()
746
+ if flags&(shimchecker.TypeFlagsStringLiteral|shimchecker.TypeFlagsNumberLiteral|shimchecker.TypeFlagsBooleanLiteral|shimchecker.TypeFlagsBigIntLiteral|shimchecker.TypeFlagsUniqueESSymbol|shimchecker.TypeFlagsNever) != 0 {
747
+ return true
748
+ }
749
+ if flags&shimchecker.TypeFlagsUnion == 0 || len(t.Types()) == 0 {
750
+ return false
751
+ }
752
+ for _, member := range t.Types() {
753
+ if !isBrandMarkerValueType(member) {
754
+ return false
755
+ }
756
+ }
757
+ return true
758
+ }
759
+
760
+ // templateLiteralInvalidPredicate always checks the string representation. A
761
+ // template with exactly one string-like placeholder can also enforce its
762
+ // fixed prefix and suffix cheaply. Complex templates deliberately fall back
763
+ // to typeof rather than approximating their language with an unsound regex.
764
+ func templateLiteralInvalidPredicate(
765
+ factory *shimast.NodeFactory,
766
+ path string,
767
+ t *shimchecker.Type,
768
+ ) (*shimast.Expression, bool) {
769
+ template := t.AsTemplateLiteralType()
770
+ if template == nil {
771
+ return nil, false
772
+ }
773
+ notString := binary(factory, factory.NewTypeOfExpression(pathExpr(factory, path)), shimast.KindExclamationEqualsEqualsToken, factory.NewStringLiteral("string", shimast.TokenFlagsNone))
774
+ texts, placeholders := template.Texts(), template.Types()
775
+ if len(texts) != len(placeholders)+1 {
776
+ return nil, false
777
+ }
778
+ if len(placeholders) != 1 || !isStringLikeType(placeholders[0]) {
779
+ return notString, true
780
+ }
781
+ out := notString
782
+ if texts[0] != "" {
783
+ out = binary(factory, out, shimast.KindBarBarToken, stringMethodInvalidPredicate(factory, path, "startsWith", texts[0]))
784
+ }
785
+ if texts[1] != "" {
786
+ out = binary(factory, out, shimast.KindBarBarToken, stringMethodInvalidPredicate(factory, path, "endsWith", texts[1]))
787
+ }
788
+ return out, true
789
+ }
790
+
791
+ func isStringLikeType(t *shimchecker.Type) bool {
792
+ if t == nil {
793
+ return false
794
+ }
795
+ return t.Flags()&(shimchecker.TypeFlagsString|shimchecker.TypeFlagsStringLiteral|shimchecker.TypeFlagsTemplateLiteral|shimchecker.TypeFlagsStringMapping) != 0
796
+ }
797
+
798
+ func stringMethodInvalidPredicate(factory *shimast.NodeFactory, path, method, value string) *shimast.Expression {
799
+ call := factory.NewCallExpression(
800
+ factory.NewPropertyAccessExpression(pathExpr(factory, path), nil, factory.NewIdentifier(method), 0),
801
+ nil,
802
+ nil,
803
+ factory.NewNodeList([]*shimast.Node{factory.NewStringLiteral(value, shimast.TokenFlagsNone)}),
804
+ 0,
805
+ )
806
+ return factory.NewPrefixUnaryExpression(shimast.KindExclamationToken, call)
807
+ }
808
+
809
+ // isStructuralIntersection keeps primitive/object intersections (including
810
+ // brands) out of this generic structural path. Arrays and tuples intersected
811
+ // with objects also need semantics beyond a flattened property walk.
812
+ func isStructuralIntersection(checker *shimchecker.Checker, t *shimchecker.Type) bool {
813
+ members := t.Types()
814
+ if len(members) == 0 ||
815
+ len(shimchecker.Checker_getSignaturesOfType(checker, t, shimchecker.SignatureKindCall)) > 0 ||
816
+ len(shimchecker.Checker_getSignaturesOfType(checker, t, shimchecker.SignatureKindConstruct)) > 0 {
817
+ return false
818
+ }
819
+ for _, member := range members {
820
+ if member == nil ||
821
+ member.Flags()&shimchecker.TypeFlagsObject == 0 ||
822
+ checker.IsArrayLikeType(member) ||
823
+ shimchecker.IsTupleType(member) ||
824
+ len(shimchecker.Checker_getSignaturesOfType(checker, member, shimchecker.SignatureKindCall)) > 0 ||
825
+ len(shimchecker.Checker_getSignaturesOfType(checker, member, shimchecker.SignatureKindConstruct)) > 0 {
826
+ return false
827
+ }
828
+ }
829
+ return true
830
+ }
831
+
832
+ // structuralStatements emits readable, per-path checks after first proving
833
+ // that the complete structural predicate is representable. That preflight
834
+ // prevents an unsupported property or index value from producing a partial
835
+ // guard that could reject valid inputs.
836
+ func structuralStatements(
837
+ factory *shimast.NodeFactory,
838
+ checker *shimchecker.Checker,
839
+ enclosing *shimast.Node,
840
+ path string,
841
+ t *shimchecker.Type,
842
+ state *predicateState,
843
+ depth int,
844
+ ) []*shimast.Node {
845
+ if _, ok := structuralInvalidPredicate(factory, checker, enclosing, path, t, state.clone(), depth); !ok {
846
+ return nil
847
+ }
848
+ state.active[t] = true
849
+ defer delete(state.active, t)
850
+
851
+ out := []*shimast.Node{objectCheck(factory, path)}
852
+ propertyNames := make(map[string]bool)
853
+ for _, sym := range shimchecker.Checker_getApparentProperties(checker, t) {
854
+ if !isRuntimeProperty(sym, propertyNames) || sym.Flags&shimast.SymbolFlagsOptional != 0 {
855
+ continue
856
+ }
857
+ propName := sym.Name
858
+ propType := shimchecker.Checker_getTypeOfPropertyOfType(checker, t, propName)
859
+ out = append(out, requiredPropertyCheck(factory, path, propName))
860
+ out = append(out, stmtsFromCheckerTypeSeen(
861
+ factory,
862
+ checker,
863
+ enclosing,
864
+ propertyPath(path, propName),
865
+ propType,
866
+ state,
867
+ depth+1,
868
+ )...)
869
+ }
870
+
871
+ indexType, hasStringIndex, _ := stringIndexValueType(checker, t)
872
+ if hasStringIndex {
873
+ invalid, _ := stringIndexInvalidPredicate(factory, checker, enclosing, path, indexType, state, depth)
874
+ out = append(out, throwIf(factory, invalid, "Expected "+path+" to have valid string-indexed values"))
875
+ }
876
+ return out
877
+ }
878
+
879
+ // structuralInvalidPredicate composes object, required-property, and string
880
+ // index checks so structural types remain usable inside unions and arrays.
881
+ func structuralInvalidPredicate(
882
+ factory *shimast.NodeFactory,
883
+ checker *shimchecker.Checker,
884
+ enclosing *shimast.Node,
885
+ path string,
886
+ t *shimchecker.Type,
887
+ state *predicateState,
888
+ depth int,
889
+ ) (*shimast.Expression, bool) {
890
+ if t == nil || depth > 8 || state.active[t] {
891
+ return nil, false
892
+ }
893
+ state.active[t] = true
894
+ defer delete(state.active, t)
895
+
896
+ notObject := binary(factory, factory.NewTypeOfExpression(pathExpr(factory, path)), shimast.KindExclamationEqualsEqualsToken, factory.NewStringLiteral("object", shimast.TokenFlagsNone))
897
+ out := binary(factory, notObject, shimast.KindBarBarToken, binary(factory, pathExpr(factory, path), shimast.KindEqualsEqualsEqualsToken, factory.NewToken(shimast.KindNullKeyword)))
898
+ hasShape := false
899
+ propertyNames := make(map[string]bool)
900
+ for _, sym := range shimchecker.Checker_getApparentProperties(checker, t) {
901
+ if !isRuntimeProperty(sym, propertyNames) {
902
+ continue
903
+ }
904
+ hasShape = true
905
+ if sym.Flags&shimast.SymbolFlagsOptional != 0 {
906
+ continue
907
+ }
908
+ propType := shimchecker.Checker_getTypeOfPropertyOfType(checker, t, sym.Name)
909
+ pred, ok := invalidPredicate(factory, checker, enclosing, propertyPath(path, sym.Name), propType, state, depth+1)
910
+ if !ok {
911
+ return nil, false
912
+ }
913
+ missing := missingPropertyPredicate(factory, path, sym.Name)
914
+ out = binary(factory, out, shimast.KindBarBarToken, binary(factory, missing, shimast.KindBarBarToken, pred))
915
+ }
916
+
917
+ indexType, hasStringIndex, ok := stringIndexValueType(checker, t)
918
+ if !ok {
919
+ return nil, false
920
+ }
921
+ if hasStringIndex {
922
+ hasShape = true
923
+ invalid, valid := stringIndexInvalidPredicate(factory, checker, enclosing, path, indexType, state, depth)
924
+ if !valid {
925
+ return nil, false
926
+ }
927
+ out = binary(factory, out, shimast.KindBarBarToken, invalid)
928
+ }
929
+ if !hasShape {
930
+ return nil, false
931
+ }
932
+ return out, true
933
+ }
934
+
935
+ // enumInvalidPredicate inlines the checker-known enum member values. This is
936
+ // deliberately independent of the emitted enum object so it also works for
937
+ // const enums. A computed member has a nil checker value; in that case the
938
+ // complete enum guard is skipped instead of rejecting a valid runtime value.
939
+ func enumInvalidPredicate(
940
+ factory *shimast.NodeFactory,
941
+ path string,
942
+ t *shimchecker.Type,
943
+ ) (*shimast.Expression, bool) {
944
+ if t == nil || t.Flags()&shimchecker.TypeFlagsEnumLike == 0 {
945
+ return nil, false
946
+ }
947
+ members := []*shimchecker.Type{t}
948
+ if t.Flags()&shimchecker.TypeFlagsUnion != 0 {
949
+ members = t.Types()
950
+ }
951
+ if len(members) == 0 {
952
+ return nil, false
953
+ }
954
+
955
+ seen := make(map[string]bool, len(members))
956
+ var invalid *shimast.Expression
957
+ for _, member := range members {
958
+ if member == nil || member.Flags()&shimchecker.TypeFlagsEnumLike == 0 {
959
+ return nil, false
960
+ }
961
+ value := member.AsLiteralType().Value()
962
+ if value == nil {
963
+ return nil, false
964
+ }
965
+ var expected *shimast.Expression
966
+ var key string
967
+ switch {
968
+ case member.Flags()&shimchecker.TypeFlagsStringLiteral != 0:
969
+ text, ok := value.(string)
970
+ if !ok {
971
+ return nil, false
337
972
  }
338
- out = append(out, stmtsFromCheckerTypeSeen(
339
- factory,
340
- checker,
341
- path+"."+propName,
342
- propType,
343
- seen,
344
- depth+1,
345
- )...)
346
- }
347
- return out
348
- default:
973
+ expected = factory.NewStringLiteral(text, shimast.TokenFlagsNone)
974
+ key = "string:" + text
975
+ case member.Flags()&shimchecker.TypeFlagsNumberLiteral != 0:
976
+ text := fmt.Sprint(value)
977
+ expected = factory.NewNumericLiteral(text, shimast.TokenFlagsNone)
978
+ key = "number:" + text
979
+ default:
980
+ return nil, false
981
+ }
982
+ if seen[key] {
983
+ continue
984
+ }
985
+ seen[key] = true
986
+ memberInvalid := binary(factory, pathExpr(factory, path), shimast.KindExclamationEqualsEqualsToken, expected)
987
+ if invalid == nil {
988
+ invalid = memberInvalid
989
+ } else {
990
+ invalid = binary(factory, invalid, shimast.KindAmpersandAmpersandToken, memberInvalid)
991
+ }
992
+ }
993
+ return invalid, invalid != nil
994
+ }
995
+
996
+ func classTypeSymbol(t *shimchecker.Type) *shimast.Symbol {
997
+ if t == nil || t.Flags()&shimchecker.TypeFlagsObject == 0 {
349
998
  return nil
350
999
  }
1000
+ candidates := []*shimast.Symbol{t.Symbol(), shimchecker.Type_getTypeNameSymbol(t)}
1001
+ if t.ObjectFlags()&shimchecker.ObjectFlagsReference != 0 {
1002
+ target := t.Target()
1003
+ if target != nil {
1004
+ candidates = append(candidates, target.Symbol(), shimchecker.Type_getTypeNameSymbol(target))
1005
+ }
1006
+ }
1007
+ for _, symbol := range candidates {
1008
+ if symbol != nil && symbol.Flags&shimast.SymbolFlagsClass != 0 {
1009
+ return symbol
1010
+ }
1011
+ }
1012
+ return nil
1013
+ }
1014
+
1015
+ func classInvalidPredicate(
1016
+ factory *shimast.NodeFactory,
1017
+ checker *shimchecker.Checker,
1018
+ enclosing *shimast.Node,
1019
+ path string,
1020
+ t *shimchecker.Type,
1021
+ ) (*shimast.Expression, string, bool) {
1022
+ symbol := classTypeSymbol(t)
1023
+ if symbol == nil || !shimchecker.Checker_isSymbolAccessibleAsValue(checker, symbol, enclosing) {
1024
+ return nil, "", false
1025
+ }
1026
+ className := shimchecker.Checker_symbolToValueString(checker, symbol, enclosing)
1027
+ constructor, ok := qualifiedValueExpr(factory, className)
1028
+ if !ok || !hasRuntimeValueBinding(factory, checker, enclosing, className) {
1029
+ return nil, "", false
1030
+ }
1031
+ isInstance := binary(factory, pathExpr(factory, path), shimast.KindInstanceOfKeyword, constructor)
1032
+ return factory.NewPrefixUnaryExpression(shimast.KindExclamationToken, isInstance), className, true
1033
+ }
1034
+
1035
+ func hasRuntimeValueBinding(
1036
+ factory *shimast.NodeFactory,
1037
+ checker *shimchecker.Checker,
1038
+ enclosing *shimast.Node,
1039
+ name string,
1040
+ ) bool {
1041
+ root, _, _ := strings.Cut(name, ".")
1042
+ binding := shimchecker.Checker_resolveEntityName(
1043
+ checker,
1044
+ factory.NewIdentifier(root),
1045
+ shimast.SymbolFlagsValue,
1046
+ true,
1047
+ true,
1048
+ enclosing,
1049
+ )
1050
+ if binding == nil {
1051
+ return false
1052
+ }
1053
+ for _, declaration := range binding.Declarations {
1054
+ if declaration == nil {
1055
+ continue
1056
+ }
1057
+ for current := declaration; current != nil; current = current.Parent {
1058
+ // Emit decides whether an ES import survives from the original
1059
+ // semantic references. A detached predicate added here cannot
1060
+ // reliably retain an alias that was used only as a type, so reject
1061
+ // every imported root rather than risk a dangling constructor name.
1062
+ if current.Kind == shimast.KindImportClause {
1063
+ return false
1064
+ }
1065
+ }
1066
+ }
1067
+ return true
1068
+ }
1069
+
1070
+ // qualifiedValueExpr accepts the identifier/property-access form produced by
1071
+ // the checker for ordinary class values. If naming the constructor would need
1072
+ // a synthetic import or another expression form, the class guard is skipped.
1073
+ func qualifiedValueExpr(factory *shimast.NodeFactory, name string) (*shimast.Expression, bool) {
1074
+ parts := strings.Split(name, ".")
1075
+ if len(parts) == 0 || !isIdentifierSegment(parts[0]) {
1076
+ return nil, false
1077
+ }
1078
+ var out *shimast.Expression = factory.NewIdentifier(parts[0])
1079
+ for _, part := range parts[1:] {
1080
+ if !isIdentifierSegment(part) {
1081
+ return nil, false
1082
+ }
1083
+ out = factory.NewPropertyAccessExpression(out, nil, factory.NewIdentifier(part), 0)
1084
+ }
1085
+ return out, true
1086
+ }
1087
+
1088
+ func isRuntimeProperty(sym *shimast.Symbol, seen map[string]bool) bool {
1089
+ if sym == nil || strings.HasPrefix(sym.Name, "\xFE") || seen[sym.Name] {
1090
+ return false
1091
+ }
1092
+ seen[sym.Name] = true
1093
+ return true
1094
+ }
1095
+
1096
+ // stringIndexValueType accepts exactly one string index. Number and symbol
1097
+ // indexes are skipped as a whole because Object.keys cannot enforce their
1098
+ // distinct value semantics soundly.
1099
+ func stringIndexValueType(checker *shimchecker.Checker, t *shimchecker.Type) (*shimchecker.Type, bool, bool) {
1100
+ var valueType *shimchecker.Type
1101
+ for _, info := range shimchecker.Checker_getIndexInfosOfType(checker, t) {
1102
+ if info == nil || info.KeyType() == nil || info.KeyType().Flags()&shimchecker.TypeFlagsString == 0 || valueType != nil {
1103
+ return nil, false, false
1104
+ }
1105
+ valueType = info.ValueType()
1106
+ if valueType == nil {
1107
+ return nil, false, false
1108
+ }
1109
+ }
1110
+ return valueType, valueType != nil, true
1111
+ }
1112
+
1113
+ func stringIndexInvalidPredicate(
1114
+ factory *shimast.NodeFactory,
1115
+ checker *shimchecker.Checker,
1116
+ enclosing *shimast.Node,
1117
+ path string,
1118
+ valueType *shimchecker.Type,
1119
+ state *predicateState,
1120
+ depth int,
1121
+ ) (*shimast.Expression, bool) {
1122
+ keyName := state.freshIdentifier("__di_key")
1123
+ valueInvalid, ok := invalidPredicate(factory, checker, enclosing, path+"["+keyName+"]", valueType, state, depth+1)
1124
+ if !ok {
1125
+ return nil, false
1126
+ }
1127
+ keys := factory.NewCallExpression(
1128
+ factory.NewPropertyAccessExpression(factory.NewIdentifier("Object"), nil, factory.NewIdentifier("keys"), 0),
1129
+ nil, nil, factory.NewNodeList([]*shimast.Node{pathExpr(factory, path)}), 0,
1130
+ )
1131
+ param := factory.NewParameterDeclaration(nil, nil, factory.NewIdentifier(keyName), nil, nil, nil)
1132
+ arrow := factory.NewArrowFunction(nil, nil, factory.NewNodeList([]*shimast.Node{param}), nil, nil, factory.NewToken(shimast.KindEqualsGreaterThanToken), valueInvalid)
1133
+ return factory.NewCallExpression(
1134
+ factory.NewPropertyAccessExpression(keys, nil, factory.NewIdentifier("some"), 0),
1135
+ nil, nil, factory.NewNodeList([]*shimast.Node{arrow}), 0,
1136
+ ), true
1137
+ }
1138
+
1139
+ func isIdentifierSegment(value string) bool {
1140
+ if value == "" {
1141
+ return false
1142
+ }
1143
+ for i, r := range value {
1144
+ if i == 0 {
1145
+ if r != '_' && r != '$' && !unicode.IsLetter(r) {
1146
+ return false
1147
+ }
1148
+ continue
1149
+ }
1150
+ if r != '_' && r != '$' && !unicode.IsLetter(r) && !unicode.IsDigit(r) && !unicode.IsMark(r) {
1151
+ return false
1152
+ }
1153
+ }
1154
+ return true
1155
+ }
1156
+
1157
+ func tupleInvalidPredicate(
1158
+ factory *shimast.NodeFactory,
1159
+ checker *shimchecker.Checker,
1160
+ enclosing *shimast.Node,
1161
+ path string,
1162
+ t *shimchecker.Type,
1163
+ state *predicateState,
1164
+ depth int,
1165
+ ) (*shimast.Expression, bool) {
1166
+ elements := checker.GetTypeArguments(t)
1167
+ flags := t.TargetTupleType().ElementFlags()
1168
+ if len(elements) != len(flags) {
1169
+ return nil, false
1170
+ }
1171
+ minLength := 0
1172
+ for _, flag := range flags {
1173
+ if flag&shimchecker.ElementFlagsRest != 0 {
1174
+ return nil, false
1175
+ }
1176
+ if flag&shimchecker.ElementFlagsRequired != 0 {
1177
+ minLength++
1178
+ }
1179
+ }
1180
+ isArray := factory.NewCallExpression(
1181
+ factory.NewPropertyAccessExpression(factory.NewIdentifier("Array"), nil, factory.NewIdentifier("isArray"), 0),
1182
+ nil, nil, factory.NewNodeList([]*shimast.Node{pathExpr(factory, path)}), 0,
1183
+ )
1184
+ out := factory.NewPrefixUnaryExpression(shimast.KindExclamationToken, isArray)
1185
+ lengthPath := path + ".length"
1186
+ if minLength == len(elements) {
1187
+ badLength := binary(factory, pathExpr(factory, lengthPath), shimast.KindExclamationEqualsEqualsToken, factory.NewNumericLiteral(fmt.Sprint(len(elements)), shimast.TokenFlagsNone))
1188
+ out = binary(factory, out, shimast.KindBarBarToken, badLength)
1189
+ } else {
1190
+ tooShort := binary(factory, pathExpr(factory, lengthPath), shimast.KindLessThanToken, factory.NewNumericLiteral(fmt.Sprint(minLength), shimast.TokenFlagsNone))
1191
+ tooLong := binary(factory, pathExpr(factory, lengthPath), shimast.KindGreaterThanToken, factory.NewNumericLiteral(fmt.Sprint(len(elements)), shimast.TokenFlagsNone))
1192
+ out = binary(factory, out, shimast.KindBarBarToken, binary(factory, tooShort, shimast.KindBarBarToken, tooLong))
1193
+ }
1194
+ for i, element := range elements {
1195
+ pred, ok := invalidPredicate(factory, checker, enclosing, fmt.Sprintf("%s[%d]", path, i), element, state, depth+1)
1196
+ if !ok {
1197
+ return nil, false
1198
+ }
1199
+ if flags[i]&shimchecker.ElementFlagsOptional != 0 {
1200
+ present := binary(factory, pathExpr(factory, lengthPath), shimast.KindGreaterThanToken, factory.NewNumericLiteral(fmt.Sprint(i), shimast.TokenFlagsNone))
1201
+ pred = binary(factory, present, shimast.KindAmpersandAmpersandToken, pred)
1202
+ }
1203
+ out = binary(factory, out, shimast.KindBarBarToken, pred)
1204
+ }
1205
+ return out, true
1206
+ }
1207
+
1208
+ func arrayInvalidPredicate(
1209
+ factory *shimast.NodeFactory,
1210
+ checker *shimchecker.Checker,
1211
+ enclosing *shimast.Node,
1212
+ path string,
1213
+ t *shimchecker.Type,
1214
+ state *predicateState,
1215
+ depth int,
1216
+ ) (*shimast.Expression, bool) {
1217
+ typeArgs := checker.GetTypeArguments(t)
1218
+ if len(typeArgs) != 1 {
1219
+ return nil, false
1220
+ }
1221
+ itemName := state.freshIdentifier("__di_item")
1222
+ itemInvalid, ok := invalidPredicate(factory, checker, enclosing, itemName, typeArgs[0], state, depth+1)
1223
+ if !ok {
1224
+ return nil, false
1225
+ }
1226
+ isArray := factory.NewCallExpression(
1227
+ factory.NewPropertyAccessExpression(factory.NewIdentifier("Array"), nil, factory.NewIdentifier("isArray"), 0),
1228
+ nil, nil, factory.NewNodeList([]*shimast.Node{pathExpr(factory, path)}), 0,
1229
+ )
1230
+ notArray := factory.NewPrefixUnaryExpression(shimast.KindExclamationToken, isArray)
1231
+ param := factory.NewParameterDeclaration(nil, nil, factory.NewIdentifier(itemName), nil, nil, nil)
1232
+ arrow := factory.NewArrowFunction(nil, nil, factory.NewNodeList([]*shimast.Node{param}), nil, nil, factory.NewToken(shimast.KindEqualsGreaterThanToken), itemInvalid)
1233
+ hasInvalidItem := factory.NewCallExpression(
1234
+ factory.NewPropertyAccessExpression(pathExpr(factory, path), nil, factory.NewIdentifier("some"), 0),
1235
+ nil, nil, factory.NewNodeList([]*shimast.Node{arrow}), 0,
1236
+ )
1237
+ return binary(factory, notArray, shimast.KindBarBarToken, hasInvalidItem), true
1238
+ }
1239
+
1240
+ func equalityCheck(factory *shimast.NodeFactory, path string, expected *shimast.Expression, label string) *shimast.Node {
1241
+ cond := binary(factory, pathExpr(factory, path), shimast.KindExclamationEqualsEqualsToken, expected)
1242
+ return throwIf(factory, cond, "Expected "+path+" to equal "+label)
1243
+ }
1244
+
1245
+ func requiredPropertyCheck(factory *shimast.NodeFactory, path, property string) *shimast.Node {
1246
+ return throwIf(factory, missingPropertyPredicate(factory, path, property), "Expected "+path+" to have required property "+strconv.Quote(property))
1247
+ }
1248
+
1249
+ func missingPropertyPredicate(factory *shimast.NodeFactory, path, property string) *shimast.Expression {
1250
+ present := binary(
1251
+ factory,
1252
+ factory.NewStringLiteral(property, shimast.TokenFlagsNone),
1253
+ shimast.KindInKeyword,
1254
+ pathExpr(factory, path),
1255
+ )
1256
+ return factory.NewPrefixUnaryExpression(shimast.KindExclamationToken, present)
1257
+ }
1258
+
1259
+ func propertyPath(path, property string) string {
1260
+ if isSimpleIdentifier(property) {
1261
+ return path + "." + property
1262
+ }
1263
+ return path + "[" + strconv.Quote(property) + "]"
1264
+ }
1265
+
1266
+ func isSimpleIdentifier(value string) bool {
1267
+ if value == "" {
1268
+ return false
1269
+ }
1270
+ for i, r := range value {
1271
+ if !(r == '_' || r == '$' || r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || i > 0 && r >= '0' && r <= '9') {
1272
+ return false
1273
+ }
1274
+ }
1275
+ return true
351
1276
  }
352
1277
 
353
1278
  // pathExpr builds a fresh expression tree for a dotted path (e.g. "user.id").
354
1279
  // Each call allocates new nodes — AST nodes are not shareable across parents.
355
1280
  func pathExpr(factory *shimast.NodeFactory, path string) *shimast.Expression {
356
- parts := strings.Split(path, ".")
357
- var expr *shimast.Expression = factory.NewIdentifier(parts[0])
358
- for _, part := range parts[1:] {
359
- expr = factory.NewPropertyAccessExpression(
360
- expr,
361
- nil,
362
- factory.NewIdentifier(part),
363
- 0,
364
- )
1281
+ end := strings.IndexAny(path, ".[")
1282
+ if end < 0 {
1283
+ return factory.NewIdentifier(path)
1284
+ }
1285
+ var expr *shimast.Expression = factory.NewIdentifier(path[:end])
1286
+ for end < len(path) {
1287
+ if path[end] == '.' {
1288
+ start := end + 1
1289
+ end = start
1290
+ for end < len(path) && path[end] != '.' && path[end] != '[' {
1291
+ end++
1292
+ }
1293
+ expr = factory.NewPropertyAccessExpression(expr, nil, factory.NewIdentifier(path[start:end]), 0)
1294
+ continue
1295
+ }
1296
+ start := end + 1
1297
+ if start < len(path) && path[start] == '"' {
1298
+ quotedEnd := start + 1
1299
+ escaped := false
1300
+ for quotedEnd < len(path) {
1301
+ if path[quotedEnd] == '"' && !escaped {
1302
+ break
1303
+ }
1304
+ if path[quotedEnd] == '\\' {
1305
+ escaped = !escaped
1306
+ } else {
1307
+ escaped = false
1308
+ }
1309
+ quotedEnd++
1310
+ }
1311
+ if quotedEnd >= len(path) || quotedEnd+1 >= len(path) || path[quotedEnd+1] != ']' {
1312
+ return expr
1313
+ }
1314
+ property, err := strconv.Unquote(path[start : quotedEnd+1])
1315
+ if err != nil {
1316
+ return expr
1317
+ }
1318
+ expr = factory.NewElementAccessExpression(expr, nil, factory.NewStringLiteral(property, shimast.TokenFlagsNone), 0)
1319
+ end = quotedEnd + 2
1320
+ continue
1321
+ }
1322
+ close := strings.IndexByte(path[start:], ']')
1323
+ if close < 0 {
1324
+ return expr
1325
+ }
1326
+ close += start
1327
+ index := path[start:close]
1328
+ // An identifier-shaped synthesized index prints as the numeric token while
1329
+ // avoiding the emitter asking source-text questions of a detached literal.
1330
+ expr = factory.NewElementAccessExpression(expr, nil, factory.NewIdentifier(index), 0)
1331
+ end = close + 1
365
1332
  }
366
1333
  return expr
367
1334
  }