@ttsc/evidence 0.25.0 → 0.26.1

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.
@@ -14,7 +14,7 @@ func loadTypeScriptInventories(
14
14
  config graphConfig,
15
15
  ) map[string]*artifactInventory {
16
16
  inventories := map[string]*artifactInventory{}
17
- extendTypeScriptInventories(root, sources, config, inventories)
17
+ extendTypeScriptInventories(root, sources, config, inventories, nil)
18
18
  return inventories
19
19
  }
20
20
 
@@ -30,6 +30,7 @@ func extendTypeScriptInventories(
30
30
  sources []*shimast.SourceFile,
31
31
  config graphConfig,
32
32
  inventories map[string]*artifactInventory,
33
+ governed map[string]bool,
33
34
  ) {
34
35
  bases := configuredBases(config, artifactTypeScript)
35
36
  for _, file := range sources {
@@ -42,6 +43,9 @@ func extendTypeScriptInventories(
42
43
  continue
43
44
  }
44
45
  address := base.addressOf(relative)
46
+ if governed != nil && matchesConfiguredTypeScriptFile(config, base, relative) {
47
+ governed[address.Key] = true
48
+ }
45
49
  if inventories[address.Key] != nil {
46
50
  continue
47
51
  }
@@ -129,12 +133,13 @@ func scanTypeScriptInventoryAt(
129
133
  file *shimast.SourceFile,
130
134
  ) *artifactInventory {
131
135
  inventory := &artifactInventory{
132
- Address: address.Key,
133
- Path: address.Display,
134
- Type: artifactTypeScript,
135
- Imports: collectImportBindings(file),
136
- Exports: collectModuleExports(file),
137
- UnitNodes: map[string][]*shimast.Node{},
136
+ Address: address.Key,
137
+ Path: address.Display,
138
+ Type: artifactTypeScript,
139
+ Imports: collectImportBindings(file),
140
+ Exports: collectModuleExports(file),
141
+ UnitNodes: map[string][]*shimast.Node{},
142
+ UnitContent: map[string][]*shimast.Node{},
138
143
  }
139
144
  supportedHosts := map[*shimast.Node]symbolSet{}
140
145
  unitsByID := map[string]*evidenceUnit{}
@@ -151,6 +156,7 @@ func scanTypeScriptInventoryAt(
151
156
  false,
152
157
  "",
153
158
  )
159
+ withdrawHiddenHosts(inventory, supportedHosts)
154
160
  collectTypeScriptDeclarations(
155
161
  file,
156
162
  address.Key,
@@ -162,6 +168,7 @@ func scanTypeScriptInventoryAt(
162
168
  // used to derive them. Declarations now retain those IDs directly, so release
163
169
  // the transient index before this inventory enters the immutable graph.
164
170
  inventory.UnitNodes = nil
171
+ inventory.UnitContent = nil
165
172
  sort.Slice(inventory.Units, func(left int, right int) bool {
166
173
  if inventory.Units[left].Target != inventory.Units[right].Target {
167
174
  return inventory.Units[left].Target < inventory.Units[right].Target
@@ -171,6 +178,87 @@ func scanTypeScriptInventoryAt(
171
178
  return inventory
172
179
  }
173
180
 
181
+ // withdrawHiddenHosts takes every declaration of a withdrawn identity out of
182
+ // the host set, once materialization has decided which identities are withdrawn.
183
+ //
184
+ // Withdrawal is a property of the **identity**, while `supportedHosts` is filled
185
+ // in one node at a time by whichever collector walked that container. Those two
186
+ // facts disagree whenever an identity spans more than one declaration and the
187
+ // author tagged only one of them, which is every overload run and every merged
188
+ // interface: the unit came out marked, and the untagged sibling stayed a host,
189
+ // so a declaration the author had taken out of the API went on discharging
190
+ // coverage and acting as an exclusion carrier. Measured on both shapes; both
191
+ // were silent, because the unit really was marked.
192
+ //
193
+ // Reconciling here rather than in each collector is what turns a list of
194
+ // witnesses into one rule. A per-container withdrawal index closes the container
195
+ // it indexes and nothing else, and there is one such container per declaration
196
+ // form. This runs once, over the identities the whole file produced, so it
197
+ // reaches a form nobody has written yet — but only as far as that form's
198
+ // unit-to-node association goes, which is the condition below and the one place
199
+ // this is not yet a closed class.
200
+ //
201
+ // A position is given up only when **every** identity that reaches it is
202
+ // withdrawn. One node can host several: `export var price: number, live:
203
+ // number` is two property identities sharing the statement TypeScript attaches
204
+ // their block to, and withdrawing one of them must not take the other's only
205
+ // host position with it. Judging per node alone did exactly that, and refused a
206
+ // citation on a public declaration nobody had tagged. The exemplar is `var`
207
+ // rather than `const` because a partial withdrawal needs a second declaration
208
+ // of one identity, which for `const` is `TS2451`.
209
+ //
210
+ // What this cannot reach is a host node no unit records, so a form that
211
+ // registers a position owes that position to its unit. The variable declarator
212
+ // was the one that did not, and a withdrawn variable identity kept it until
213
+ // `collectTypeScriptVariables` began recording it. `documentedHosts` filters on
214
+ // the same association and was measured to answer identically either way, since
215
+ // it takes the first of a unit's *host* nodes and a declarator is a host only
216
+ // when its statement is one for the same symbol; the invariant is what was
217
+ // broken there rather than an observable answer.
218
+ //
219
+ // A second fault travelled with that one and has its own cause, which is why
220
+ // they are stated apart: a withdrawal tag read from a container is not the tag
221
+ // of the declarations
222
+ // inside it, so an inner declarator owes its own read, and recording the node
223
+ // would have left that exactly where it was.
224
+ //
225
+ // `documentedHosts` needs no equivalent: it skips a withdrawn unit before it
226
+ // ever consults the host set.
227
+ func withdrawHiddenHosts(
228
+ inventory *artifactInventory,
229
+ supportedHosts map[*shimast.Node]symbolSet,
230
+ ) {
231
+ type hostPosition struct {
232
+ node *shimast.Node
233
+ symbol string
234
+ }
235
+ live := map[hostPosition]bool{}
236
+ withdrawn := map[hostPosition]bool{}
237
+ for _, unit := range inventory.Units {
238
+ for _, node := range inventory.UnitNodes[unit.ID] {
239
+ position := hostPosition{node: node, symbol: unit.Symbol}
240
+ if unit.Hidden == "" {
241
+ live[position] = true
242
+ continue
243
+ }
244
+ withdrawn[position] = true
245
+ }
246
+ }
247
+ for position := range withdrawn {
248
+ if live[position] {
249
+ continue
250
+ }
251
+ hosts := supportedHosts[position.node]
252
+ if hosts == nil {
253
+ continue
254
+ }
255
+ delete(hosts, position.symbol)
256
+ if len(hosts) == 0 {
257
+ delete(supportedHosts, position.node)
258
+ }
259
+ }
260
+ }
261
+
174
262
  // collectTypeScriptStatements materializes the public units one statement list
175
263
  // declares.
176
264
  //
@@ -197,9 +285,10 @@ func collectTypeScriptStatements(
197
285
  exports := collectLocalExportNames(statements)
198
286
  hiddenNames := collectHiddenDeclarationNames(file, statements)
199
287
  // Built on the first namespace this list holds rather than up front, so a
200
- // file declaring none pays nothing. Every rebuild scans every configured
201
- // source, and most of them have no namespace at all.
288
+ // file declaring none pays nothing.
202
289
  var functionNames map[string]bool
290
+ // The same, for the class an interface in this list may merge with.
291
+ var classNames map[string]bool
203
292
  for _, statement := range statements.Nodes {
204
293
  if statement == nil {
205
294
  continue
@@ -210,7 +299,7 @@ func collectTypeScriptStatements(
210
299
  if name == "" {
211
300
  continue
212
301
  }
213
- targets := publicTypeScriptNames(
302
+ targets := publicTypeScriptExports(
214
303
  statement,
215
304
  name,
216
305
  exports,
@@ -224,26 +313,57 @@ func collectTypeScriptStatements(
224
313
  if memberHidden == "" {
225
314
  addTypeScriptHost(supportedHosts, statement, "type")
226
315
  }
227
- for _, name := range targets {
228
- identity := qualifyTypeScriptName(prefix, name)
316
+ // An interface merged with a class declares that class's instance
317
+ // members, so they take the instance address the class body form takes.
318
+ // Addressing them from the bare name instead published `Sale.charge` for
319
+ // a member reached as `Sale.prototype.charge`: a path no consumer can
320
+ // walk, a second unit for a method the class already declared, and an
321
+ // obligation that stayed owed however the real member was cited.
322
+ //
323
+ // The merge also brings the type-only rule with it. The members of an
324
+ // interface no class merges with are type-space and project through a
325
+ // type-only alias, a namespace or function partner included, but a
326
+ // class-merged one's are reached through the class value that alias does
327
+ // not expose, which is the address the class branch is already
328
+ // suppressed for. Publishing them here would breach that guard at
329
+ // exactly the address it exists to keep empty. What `target.TypeOnly`
330
+ // reaches, and what it does not, is stated once on the class branch this
331
+ // guard mirrors.
332
+ if classNames == nil {
333
+ classNames = collectClassDeclarationNames(statements)
334
+ }
335
+ mergedWithClass := classNames[name]
336
+ for _, target := range targets {
337
+ identity := qualifyTypeScriptName(prefix, target.Public)
229
338
  unit := addTypeScriptUnit(
230
339
  inventory,
231
340
  unitsByID,
232
341
  statement,
342
+ statement,
233
343
  "type",
234
344
  identity,
235
345
  parentID,
236
346
  memberHidden,
237
347
  )
348
+ memberOwner := identity
349
+ if mergedWithClass {
350
+ if typeOnlyProjection || target.TypeOnly {
351
+ continue
352
+ }
353
+ memberOwner = qualifyTypeScriptName(identity, "prototype")
354
+ }
238
355
  collectPropertyMembers(
239
356
  file,
240
357
  statement.AsInterfaceDeclaration().Members,
241
- identity,
358
+ memberOwner,
242
359
  unit.ID,
243
360
  inventory,
244
361
  supportedHosts,
245
362
  unitsByID,
246
363
  memberHidden,
364
+ // Only the class merge makes these value-space. An interface's own
365
+ // members are type-space and a type-only export publishes them.
366
+ mergedWithClass,
247
367
  )
248
368
  }
249
369
  case shimast.KindTypeAliasDeclaration:
@@ -272,6 +392,7 @@ func collectTypeScriptStatements(
272
392
  inventory,
273
393
  unitsByID,
274
394
  statement,
395
+ statement,
275
396
  "type",
276
397
  identity,
277
398
  parentID,
@@ -287,6 +408,7 @@ func collectTypeScriptStatements(
287
408
  supportedHosts,
288
409
  unitsByID,
289
410
  memberHidden,
411
+ false,
290
412
  )
291
413
  }
292
414
  }
@@ -317,11 +439,12 @@ func collectTypeScriptStatements(
317
439
  inventory,
318
440
  unitsByID,
319
441
  statement,
442
+ statement,
320
443
  "function",
321
444
  qualifyTypeScriptName(prefix, name),
322
445
  parentID,
323
446
  memberHidden,
324
- )
447
+ ).markSpace(true)
325
448
  }
326
449
  case shimast.KindVariableStatement:
327
450
  if typeOnlyProjection {
@@ -329,6 +452,7 @@ func collectTypeScriptStatements(
329
452
  }
330
453
  memberHidden := typeScriptHidingTag(file, statement, hidden)
331
454
  for symbol := range collectTypeScriptVariables(
455
+ file,
332
456
  statement,
333
457
  prefix,
334
458
  parentID,
@@ -347,23 +471,57 @@ func collectTypeScriptStatements(
347
471
  addTypeScriptHost(supportedHosts, statement, symbol)
348
472
  }
349
473
  case shimast.KindClassDeclaration:
350
- if typeOnlyProjection {
474
+ name := declarationName(statement.Name())
475
+ if name == "" {
351
476
  continue
352
477
  }
353
- name := declarationName(statement.Name())
354
- memberHidden := hidingTagFor(hidden, hiddenNames, name)
355
- for _, publicName := range publicTypeScriptNames(
478
+ // A class name is type-space, so a type-only alias exposes it exactly
479
+ // as it exposes an interface. What the alias withholds is the members:
480
+ // `C.prototype.field` and `C.staticField` are paths through the class
481
+ // *value*, and a type-only alias exposes no value to walk them from.
482
+ //
483
+ // This guard answers an export written in this file, where the
484
+ // declaration kind is in hand: `target.TypeOnly` sees `export type { C }`
485
+ // and `export { type C }` alike, and `typeOnlyProjection` sees a
486
+ // type-only alias of an enclosing namespace. A re-export naming another
487
+ // module has no such context and is answered at traversal time instead,
488
+ // from `evidenceUnit.ValueSpace`, which is why these members are marked
489
+ // there. The interface branch mirrors this guard for a class-merged
490
+ // interface and points here for the reason.
491
+ targets := publicTypeScriptExports(
356
492
  statement,
357
493
  name,
358
494
  exports,
359
- false,
495
+ true,
360
496
  implicitlyExported,
361
- ) {
362
- collectClassCallables(
363
- file,
497
+ )
498
+ if len(targets) == 0 {
499
+ continue
500
+ }
501
+ memberHidden := hidingTagFor(hidden, hiddenNames, name)
502
+ if memberHidden == "" {
503
+ addTypeScriptHost(supportedHosts, statement, "type")
504
+ }
505
+ for _, target := range targets {
506
+ identity := qualifyTypeScriptName(prefix, target.Public)
507
+ unit := addTypeScriptUnit(
508
+ inventory,
509
+ unitsByID,
510
+ statement,
364
511
  statement,
365
- qualifyTypeScriptName(prefix, publicName),
512
+ "type",
513
+ identity,
366
514
  parentID,
515
+ memberHidden,
516
+ )
517
+ if typeOnlyProjection || target.TypeOnly {
518
+ continue
519
+ }
520
+ collectClassMembers(
521
+ file,
522
+ statement,
523
+ identity,
524
+ unit.ID,
367
525
  inventory,
368
526
  supportedHosts,
369
527
  unitsByID,
@@ -405,6 +563,7 @@ func collectTypeScriptStatements(
405
563
  inventory,
406
564
  unitsByID,
407
565
  statement,
566
+ statement,
408
567
  "type",
409
568
  identity,
410
569
  parentID,
@@ -434,11 +593,12 @@ func collectTypeScriptStatements(
434
593
  //
435
594
  // A namespace merges only with a function, a class, or an enum in the same
436
595
  // scope; a `const` or `let` of the same name is `TS2451`, measured against the
437
- // pinned compiler. Of the three legal partners only a function also
438
- // materializes a unit under the merged name — a class registers no unit of its
439
- // own and the collector has no enum case — so the function form is the one
440
- // shape where a namespace and the declaration it merges with are the same
441
- // public entity spelled twice.
596
+ // pinned compiler. Only the function partner makes the namespace a static side
597
+ // that materializes nothing. A class partner keeps its namespace, because a
598
+ // companion namespace beside a class is authored contract — `namespace Sale`
599
+ // holding `Sale.IProps` beside `class Sale` — rather than the generated
600
+ // accessor machinery `get.path` and `get.Output` are. The collector has no enum
601
+ // case, so an enum partner reaches nothing either way.
442
602
  //
443
603
  // The name alone decides, without consulting export modifiers, because
444
604
  // TypeScript refuses a merged declaration whose halves disagree on export
@@ -464,7 +624,76 @@ func collectFunctionDeclarationNames(
464
624
  return names
465
625
  }
466
626
 
627
+ // collectClassDeclarationNames indexes the local names a statement list
628
+ // declares as classes, which is what an interface in the same list merges with.
629
+ //
630
+ // The merge is what decides a member's address rather than its kind. An
631
+ // interface merged with a class describes that class's instance side, so
632
+ // `interface Sale { charge(): void }` beside `class Sale` declares the same
633
+ // member the class body would and takes the same `Sale.prototype.charge`
634
+ // address. A namespace is the other partner and adds statics instead, which is
635
+ // why only the interface case moves.
636
+ //
637
+ // The whole list is indexed rather than only the statements already collected,
638
+ // because merging does not depend on which declaration is written first, and
639
+ // export modifiers are not consulted for the reason
640
+ // `collectFunctionDeclarationNames` gives: TypeScript refuses a merge whose
641
+ // halves disagree on export.
642
+ func collectClassDeclarationNames(
643
+ statements *shimast.NodeList,
644
+ ) map[string]bool {
645
+ names := map[string]bool{}
646
+ if statements == nil {
647
+ return names
648
+ }
649
+ for _, statement := range statements.Nodes {
650
+ if statement == nil ||
651
+ statement.Kind != shimast.KindClassDeclaration {
652
+ continue
653
+ }
654
+ if name := declarationName(statement.Name()); name != "" {
655
+ names[name] = true
656
+ }
657
+ }
658
+ return names
659
+ }
660
+
661
+ // collectTypeScriptVariables materializes a variable statement's exported
662
+ // declarators.
663
+ //
664
+ // A variable is a function only when a `const` is initialized with a function
665
+ // value, and that is the inverse of the member rule in memberSymbol:
666
+ // here the annotation decides nothing, because a variable's declared type
667
+ // describes a value that already exists rather than stating a contract. The
668
+ // three conditions below are each load-bearing and each measured separately, at
669
+ // module and at namespace scope: a binding pattern's initializer belongs to the
670
+ // pattern rather than to any leaf, `let` and `var` are excluded whatever they
671
+ // hold, and the initializer is read syntactically because these rules run with
672
+ // no type checker.
673
+ // A declarator is both a host position and a unit node, and it has to be both.
674
+ // It was only the first: `supportedHosts` held it while no unit recorded it, so
675
+ // nothing that walks from a unit to its declarations could see it. Two answers
676
+ // were wrong and both were silent. `withdrawHiddenHosts` could not take the
677
+ // position away from a withdrawn identity, so a declaration the author had
678
+ // removed from the API went on discharging coverage and carrying exclusions.
679
+ // And a citation written on it resolved to no semantic host, so
680
+ // `singleEvidencePerSymbol` counted the statement's identities as citing zero
681
+ // units while the same run reported the obligation satisfied.
682
+ //
683
+ // A third consumer reads the same association and was measured to answer
684
+ // identically either way. `hostNodesOf` keeps a unit's host nodes and takes the
685
+ // first, and a declarator is a host only when its statement is one for the same
686
+ // symbol, so `evidence/documented` always had the wrapper to look at. The
687
+ // invariant was broken there rather than the answer, which is reason enough:
688
+ // the next declaration form to register a position will not be so lucky.
689
+ //
690
+ // Its own withdrawal tag is read here for a separate fault rather than the same
691
+ // one: the statement wrapper's tag was taken for every declarator it holds, so
692
+ // `@internal` written on an inner declarator withdrew nothing. Recording the
693
+ // node closes the first two and leaves this one standing, which is why #1126
694
+ // states them apart.
467
695
  func collectTypeScriptVariables(
696
+ file *shimast.SourceFile,
468
697
  statement *shimast.Node,
469
698
  prefix []string,
470
699
  parentID string,
@@ -495,6 +724,7 @@ func collectTypeScriptVariables(
495
724
  isFunctionValue(value.Initializer) {
496
725
  symbol = "function"
497
726
  }
727
+ declaratorHidden := typeScriptHidingTag(file, declaration, hidden)
498
728
  for _, binding := range bindingIdentifierNodes(declaration.Name()) {
499
729
  name := declarationName(binding)
500
730
  targets := publicTypeScriptNames(
@@ -507,7 +737,7 @@ func collectTypeScriptVariables(
507
737
  if len(targets) == 0 {
508
738
  continue
509
739
  }
510
- if hidden == "" {
740
+ if declaratorHidden == "" {
511
741
  addTypeScriptHost(supportedHosts, declaration, symbol)
512
742
  }
513
743
  for _, name := range targets {
@@ -515,14 +745,18 @@ func collectTypeScriptVariables(
515
745
  inventory,
516
746
  unitsByID,
517
747
  binding,
748
+ declaration,
518
749
  symbol,
519
750
  qualifyTypeScriptName(prefix, name),
520
751
  parentID,
521
- hidden,
752
+ declaratorHidden,
522
753
  )
523
- // The binding names the unit, but TypeScript attaches a
524
- // variable's leading JSDoc to the statement wrapper, so that
525
- // is where a citation for this unit actually lives.
754
+ // TypeScript attaches a variable's leading JSDoc to the statement
755
+ // wrapper, so that is where a citation for this unit lives and the
756
+ // wrapper is a position this identity owns. It is not this
757
+ // identity's content: the same wrapper declares every sibling of
758
+ // this declarator, and their text belongs to them.
759
+ unit.markSpace(true)
526
760
  inventory.recordUnitNode(unit.ID, statement)
527
761
  }
528
762
  found[symbol] = true
@@ -531,11 +765,33 @@ func collectTypeScriptVariables(
531
765
  return found
532
766
  }
533
767
 
534
- func collectClassCallables(
768
+ // collectClassMembers materializes the public contract a class declares.
769
+ //
770
+ // A method is a function unit and a field is a property unit, which is the
771
+ // mapping a reader already assumes: the class is the subject, its methods are
772
+ // what the subject does, and its fields are the measured facts it carries.
773
+ // `memberSymbol` owns the one exception, where a field written as a
774
+ // callable joins the methods, and "written as" is literal: see
775
+ // `isDirectFunctionType`.
776
+ // Every member hangs below the class unit, so a citation on the class
777
+ // acknowledges the members it selected.
778
+ //
779
+ // A constructor is not a unit of its own, now that the class is: construction
780
+ // is how the subject comes to be, and the subject already carries that
781
+ // obligation. It is still read, because the fields it declares through the
782
+ // parameter-property shorthand are the class's fields; `collectParameterProperties`
783
+ // takes those.
784
+ //
785
+ // Two shapes stay out entirely. An accessor, including an auto-accessor, is a
786
+ // get/set pair rather than a member variable, which is the exclusion the
787
+ // published contract has always stated. A member with no citable name — a
788
+ // private identifier, an index signature, a computed name, a static block — has
789
+ // no target an author could write.
790
+ func collectClassMembers(
535
791
  file *shimast.SourceFile,
536
792
  statement *shimast.Node,
537
793
  classIdentity []string,
538
- parentID string,
794
+ classID string,
539
795
  inventory *artifactInventory,
540
796
  supportedHosts map[*shimast.Node]symbolSet,
541
797
  unitsByID map[string]*evidenceUnit,
@@ -545,46 +801,246 @@ func collectClassCallables(
545
801
  if class.Members == nil {
546
802
  return
547
803
  }
804
+ // A member's own tag is read from the node in hand, and that is enough for
805
+ // the unit: `addTypeScriptUnit` keeps the first tag any declaration of an
806
+ // identity carries, so an overload run tagged on one half comes out marked
807
+ // whichever half is written first. The host set is reconciled against those
808
+ // identities afterwards by `withdrawHiddenHosts`, which is what stops the
809
+ // untagged half of a withdrawn member from staying a claim host.
810
+ //
811
+ // The constructor is resolved separately and up front, because it is the one
812
+ // container here that declares units without being one: its tag has to reach
813
+ // the parameter properties rather than a member of its own, and no unit of
814
+ // its own will carry it there.
815
+ constructorHidden := ""
816
+ constructorResolved := false
548
817
  for _, member := range class.Members.Nodes {
549
- if member == nil || !isPublicClassMember(member) {
818
+ if member == nil {
819
+ continue
820
+ }
821
+ // A constructor is judged before the public check, because its own
822
+ // visibility is not its parameter properties'. A `private constructor`
823
+ // closes construction from outside; `public readonly price` on one of its
824
+ // parameters is still an instance field every holder of the object reads.
825
+ if member.Kind == shimast.KindConstructor {
826
+ // Resolved once per class rather than per declaration, so an overload
827
+ // run does not rescan the member list for every signature it holds.
828
+ if !constructorResolved {
829
+ constructorHidden = constructorHidingTag(file, class.Members, hidden)
830
+ constructorResolved = true
831
+ }
832
+ collectParameterProperties(
833
+ file,
834
+ member,
835
+ classIdentity,
836
+ classID,
837
+ inventory,
838
+ supportedHosts,
839
+ unitsByID,
840
+ constructorHidden,
841
+ )
842
+ continue
843
+ }
844
+ if !isPublicClassMember(member) {
550
845
  continue
551
846
  }
552
- callable := false
847
+ symbol := ""
553
848
  switch member.Kind {
554
849
  case shimast.KindMethodDeclaration:
555
- callable = true
850
+ symbol = "function"
556
851
  case shimast.KindPropertyDeclaration:
557
852
  if member.ModifierFlags()&shimast.ModifierFlagsAccessor != 0 {
558
853
  continue
559
854
  }
560
855
  property := member.AsPropertyDeclaration()
561
- callable = isFunctionValue(property.Initializer) ||
562
- isDirectFunctionType(property.Type)
563
- }
564
- if !callable {
856
+ symbol = memberSymbol(property.Initializer, property.Type)
857
+ default:
565
858
  continue
566
859
  }
567
- memberName := declarationName(member.Name())
568
- if memberName == "" {
860
+ addClassMemberUnit(
861
+ member,
862
+ declarationName(member.Name()),
863
+ symbol,
864
+ shimast.GetCombinedModifierFlags(member)&shimast.ModifierFlagsStatic != 0,
865
+ classIdentity,
866
+ classID,
867
+ inventory,
868
+ supportedHosts,
869
+ unitsByID,
870
+ typeScriptHidingTag(file, member, hidden),
871
+ )
872
+ }
873
+ }
874
+
875
+ // collectParameterProperties materializes the fields a constructor declares
876
+ // through the parameter-property shorthand.
877
+ //
878
+ // `constructor(public readonly price: number)` declares the same public
879
+ // instance field as `readonly price: number` written in the class body, and
880
+ // TypeScript attaches a leading documentation block to the parameter, so it is
881
+ // a real position a citation can live in. Reading only `class.Members` would
882
+ // leave the shorthand invisible while the body form was selected, which is one
883
+ // defect wearing two syntaxes.
884
+ //
885
+ // Every parameter property is an instance field. A constructor cannot be
886
+ // static, and TypeScript refuses a parameter property on an overload
887
+ // signature, so the implementation constructor is the only one that reaches
888
+ // here with any.
889
+ //
890
+ // The tag `hidden` carries is the constructor's, already resolved across every
891
+ // declaration of it by `constructorHidingTag`. The constructor is the only
892
+ // container in this collector that declares units without being one, so
893
+ // forwarding the class's tag alone would leave `@internal` on a constructor
894
+ // inert while the same tag on a class or on the field itself withdraws.
895
+ func collectParameterProperties(
896
+ file *shimast.SourceFile,
897
+ constructor *shimast.Node,
898
+ classIdentity []string,
899
+ classID string,
900
+ inventory *artifactInventory,
901
+ supportedHosts map[*shimast.Node]symbolSet,
902
+ unitsByID map[string]*evidenceUnit,
903
+ hidden string,
904
+ ) {
905
+ if constructor.ParameterList() == nil {
906
+ return
907
+ }
908
+ for _, parameter := range constructor.Parameters() {
909
+ if parameter == nil || !isParameterProperty(parameter) {
569
910
  continue
570
911
  }
571
- identity := qualifyTypeScriptName(classIdentity, "prototype", memberName)
572
- if shimast.GetCombinedModifierFlags(member)&shimast.ModifierFlagsStatic != 0 {
573
- identity = qualifyTypeScriptName(classIdentity, memberName)
912
+ if !isPublicClassMember(parameter) {
913
+ continue
574
914
  }
575
- memberHidden := typeScriptHidingTag(file, member, hidden)
576
- addTypeScriptUnit(
915
+ value := parameter.AsParameterDeclaration()
916
+ addClassMemberUnit(
917
+ parameter,
918
+ declarationName(parameter.Name()),
919
+ memberSymbol(value.Initializer, value.Type),
920
+ false,
921
+ classIdentity,
922
+ classID,
577
923
  inventory,
924
+ supportedHosts,
578
925
  unitsByID,
579
- member,
580
- "function",
581
- identity,
582
- parentID,
583
- memberHidden,
926
+ // A parameter property is declared exactly once, so its own tag is the
927
+ // only one below the constructor's and no identity index is needed.
928
+ typeScriptHidingTag(file, parameter, hidden),
584
929
  )
585
- if memberHidden == "" {
586
- addTypeScriptHost(supportedHosts, member, "function")
930
+ }
931
+ }
932
+
933
+ // constructorHidingTag resolves the withdrawal tag of a class's constructor
934
+ // across every declaration of it.
935
+ //
936
+ // An overload run is one constructor written several times, and the tag may sit
937
+ // on any of them: a signature is where a reader looks for the documentation,
938
+ // while only the implementation declares parameter properties. Reading the tag
939
+ // per node would make the withdrawal depend on which half the author documented,
940
+ // which is the source-order accident every other merged declaration in this
941
+ // collector already refuses.
942
+ func constructorHidingTag(
943
+ file *shimast.SourceFile,
944
+ members *shimast.NodeList,
945
+ inherited string,
946
+ ) string {
947
+ if inherited != "" {
948
+ return inherited
949
+ }
950
+ if file == nil || members == nil {
951
+ return ""
952
+ }
953
+ for _, member := range members.Nodes {
954
+ if member == nil || member.Kind != shimast.KindConstructor {
955
+ continue
587
956
  }
957
+ if tag := typeScriptHidingTag(file, member, ""); tag != "" {
958
+ return tag
959
+ }
960
+ }
961
+ return ""
962
+ }
963
+
964
+ // isParameterProperty reports whether a constructor parameter also declares a
965
+ // field, which is exactly what a property modifier says.
966
+ //
967
+ // The mask is TypeScript's own and holds five: the three accessibility
968
+ // modifiers, `readonly`, and `override`. Naming the familiar four is what a
969
+ // summary reaches for, and it is short by `override`, whose own meaning is
970
+ // about the base class rather than about the field, so it does not read as a
971
+ // field declaration at all. On a class extending one that declares `rate`,
972
+ // `constructor(override rate: number)` compiles, emits the assignment, and
973
+ // declares a public instance field. Take the mask rather than restating it;
974
+ // upstream's own comment beside the constant enumerates four.
975
+ func isParameterProperty(parameter *shimast.Node) bool {
976
+ return shimast.GetCombinedModifierFlags(parameter)&
977
+ shimast.ModifierFlagsParameterPropertyModifier != 0
978
+ }
979
+
980
+ // memberSymbol classifies one member variable by how it is written.
981
+ //
982
+ // Not by what it resolves to: an arrow or function initializer, or a type
983
+ // annotation spelled as a function type, makes a member a callable, and one
984
+ // annotated with an alias of that type does not. `isDirectFunctionType` says
985
+ // why.
986
+ //
987
+ // Spelled once because the answer may not depend on which syntax declared the
988
+ // member. A field in a class body, the same field written as a constructor
989
+ // parameter property, and the same member spelled on an interface or an
990
+ // object-shaped type alias are one contract in four spellings, and letting any
991
+ // of them classify separately is the syntax dependence this collector exists to
992
+ // remove. An interface once answered `property` to `charge: () => void` while a
993
+ // class answered `function`, so a `symbol: "function"` claim over a file of
994
+ // interfaces selected nothing, deactivated, and passed with no coverage.
995
+ func memberSymbol(initializer *shimast.Node, declared *shimast.Node) string {
996
+ if isFunctionValue(initializer) || isDirectFunctionType(declared) {
997
+ return "function"
998
+ }
999
+ return "property"
1000
+ }
1001
+
1002
+ // addClassMemberUnit registers one public class member under the address that
1003
+ // reaches it from the class.
1004
+ //
1005
+ // An instance member is addressed through `prototype` and a static member
1006
+ // directly, because those are the two paths a consumer can actually write. The
1007
+ // class unit is the parent, so the member is a structural descendant of the
1008
+ // subject that declares it rather than a sibling of the class's neighbours.
1009
+ // `memberHidden` is this declaration's own withdrawal tag, inherited from its
1010
+ // class. A sibling declaration of the same identity may carry one this node does
1011
+ // not; `addTypeScriptUnit` folds that into the unit, and `withdrawHiddenHosts`
1012
+ // then takes this node out of the host set.
1013
+ func addClassMemberUnit(
1014
+ node *shimast.Node,
1015
+ name string,
1016
+ symbol string,
1017
+ static bool,
1018
+ classIdentity []string,
1019
+ classID string,
1020
+ inventory *artifactInventory,
1021
+ supportedHosts map[*shimast.Node]symbolSet,
1022
+ unitsByID map[string]*evidenceUnit,
1023
+ memberHidden string,
1024
+ ) {
1025
+ if name == "" {
1026
+ return
1027
+ }
1028
+ identity := qualifyTypeScriptName(classIdentity, "prototype", name)
1029
+ if static {
1030
+ identity = qualifyTypeScriptName(classIdentity, name)
1031
+ }
1032
+ addTypeScriptUnit(
1033
+ inventory,
1034
+ unitsByID,
1035
+ node,
1036
+ node,
1037
+ symbol,
1038
+ identity,
1039
+ classID,
1040
+ memberHidden,
1041
+ ).markSpace(true)
1042
+ if memberHidden == "" {
1043
+ addTypeScriptHost(supportedHosts, node, symbol)
588
1044
  }
589
1045
  }
590
1046
 
@@ -612,6 +1068,19 @@ func isFunctionValue(node *shimast.Node) bool {
612
1068
  return false
613
1069
  }
614
1070
 
1071
+ // isDirectFunctionType reports whether a type annotation is written as a
1072
+ // function type, rather than whether it resolves to one.
1073
+ //
1074
+ // Syntactic on purpose, and this is the sentence every summary of the class
1075
+ // field rule gets wrong. `evidence/graph` declares `NeedsTypeChecker() false`,
1076
+ // so nothing here can follow `type Handler = () => void` to its target: a field
1077
+ // annotated `Handler` is a property, while the same field annotated
1078
+ // `() => void` is a function. Parentheses are unwrapped because they change
1079
+ // nothing a reader means; a constructor type, a union with a callable in it,
1080
+ // and an alias are all left alone because resolving them is the checker's job.
1081
+ //
1082
+ // Anyone restating this as "a field that holds a function" has restated
1083
+ // something else. Twice now, on shipped surfaces.
615
1084
  func isDirectFunctionType(node *shimast.Node) bool {
616
1085
  for node != nil && node.Kind == shimast.KindParenthesizedType {
617
1086
  parenthesized := node.AsParenthesizedTypeNode()
@@ -666,6 +1135,13 @@ func collectTypeScriptModule(
666
1135
  if name != "" {
667
1136
  identity := qualifyTypeScriptName(qualified, name)
668
1137
  innerHidden := typeScriptHidingTag(file, module.Body, hidden)
1138
+ // No citation reaches this position today. TypeScript attaches a leading
1139
+ // block to the outer declaration of a dotted namespace, so the tag on
1140
+ // `export namespace Outer.Inner {}` resolves through the outer host and
1141
+ // the inner one is written and never read. It is registered anyway,
1142
+ // because the host set is derived from the unit set and a position a unit
1143
+ // records must be in it; the day a form does attach a block here, the
1144
+ // guard is the thing that was already right.
669
1145
  if innerHidden == "" {
670
1146
  addTypeScriptHost(supportedHosts, module.Body, "type")
671
1147
  }
@@ -673,6 +1149,7 @@ func collectTypeScriptModule(
673
1149
  inventory,
674
1150
  unitsByID,
675
1151
  module.Body,
1152
+ module.Body,
676
1153
  "type",
677
1154
  identity,
678
1155
  parentID,
@@ -695,6 +1172,29 @@ func collectTypeScriptModule(
695
1172
  }
696
1173
  }
697
1174
 
1175
+ // collectPropertyMembers materializes the members an interface or an
1176
+ // object-shaped type alias declares.
1177
+ //
1178
+ // This is the other half of the four-spelling rule `collectClassMembers` owns,
1179
+ // and the two must stay in step: a member signature is a function unit and a
1180
+ // data member is a property unit, decided by `memberSymbol` from the annotation
1181
+ // as spelled. The classification is single-sourced there, while the member-kind
1182
+ // switch is written twice, once per container, so a kind added to one and not
1183
+ // the other is the drift this pair is most exposed to.
1184
+ //
1185
+ // The allowlist and the name check are the exclusion, and both gates matter. A
1186
+ // get or set signature is an accessor, which is a get/set pair rather than a
1187
+ // member variable and stays out on every container. A call signature, a
1188
+ // construct signature, and an index signature have no citable name, so an
1189
+ // author could not write a target for one. A computed name passes the switch
1190
+ // and is refused after it, for the same reason.
1191
+ //
1192
+ // That leaves a consequence worth stating rather than discovering: a
1193
+ // call-signature-only `interface Handler { (input: string): void }` contributes
1194
+ // no *member* unit, so a claim narrowed to `symbol: "function"` or `"property"`
1195
+ // over a file of them selects no host and deactivates silently. The interface
1196
+ // itself is still a `type` unit, so the default selector keeps such a claim
1197
+ // active; the qualifier is the whole statement here, not a detail of it.
698
1198
  func collectPropertyMembers(
699
1199
  file *shimast.SourceFile,
700
1200
  members *shimast.NodeList,
@@ -704,12 +1204,22 @@ func collectPropertyMembers(
704
1204
  supportedHosts map[*shimast.Node]symbolSet,
705
1205
  unitsByID map[string]*evidenceUnit,
706
1206
  hidden string,
1207
+ valueSpace bool,
707
1208
  ) {
708
1209
  if members == nil {
709
1210
  return
710
1211
  }
711
1212
  for _, member := range members.Nodes {
712
- if member == nil || member.Kind != shimast.KindPropertySignature {
1213
+ if member == nil {
1214
+ continue
1215
+ }
1216
+ symbol := ""
1217
+ switch member.Kind {
1218
+ case shimast.KindPropertySignature:
1219
+ symbol = memberSymbol(nil, member.AsPropertySignatureDeclaration().Type)
1220
+ case shimast.KindMethodSignature:
1221
+ symbol = "function"
1222
+ default:
713
1223
  continue
714
1224
  }
715
1225
  name := declarationName(member.Name())
@@ -722,13 +1232,14 @@ func collectPropertyMembers(
722
1232
  inventory,
723
1233
  unitsByID,
724
1234
  member,
725
- "property",
1235
+ member,
1236
+ symbol,
726
1237
  identity,
727
1238
  parentID,
728
1239
  memberHidden,
729
- )
1240
+ ).markSpace(valueSpace)
730
1241
  if memberHidden == "" {
731
- addTypeScriptHost(supportedHosts, member, "property")
1242
+ addTypeScriptHost(supportedHosts, member, symbol)
732
1243
  }
733
1244
  }
734
1245
  }
@@ -816,10 +1327,20 @@ func typeScriptHidingTag(
816
1327
  return ""
817
1328
  }
818
1329
 
1330
+ // addTypeScriptUnit materializes one declaration of an identity.
1331
+ //
1332
+ // `named` is the node that names the identity here and answers for where it is
1333
+ // reported. `content` is the node whose text is the identity's content at this
1334
+ // declaration. They are the same node for every form whose declaration carries
1335
+ // its own name, and they part for a variable, whose unit is named by a binding
1336
+ // identifier inside a declarator. One declarator can name several identities:
1337
+ // a destructuring pattern's leaves share it, and so do two aliases of one local
1338
+ // in an export list.
819
1339
  func addTypeScriptUnit(
820
1340
  inventory *artifactInventory,
821
1341
  unitsByID map[string]*evidenceUnit,
822
- node *shimast.Node,
1342
+ named *shimast.Node,
1343
+ content *shimast.Node,
823
1344
  symbol string,
824
1345
  identity []string,
825
1346
  parentID string,
@@ -835,7 +1356,7 @@ func addTypeScriptUnit(
835
1356
  // declaration that spells it. `interface I` beside `namespace I` is one
836
1357
  // unit and two nodes, and a rule asking where that unit's JSDoc may live
837
1358
  // has to see both.
838
- inventory.recordUnitNode(id, node)
1359
+ inventory.recordUnitContent(id, content)
839
1360
  if unit := unitsByID[id]; unit != nil {
840
1361
  // A merged identity is one unit, so one declaration marking itself
841
1362
  // internal marks the identity. Both halves of `interface I` beside
@@ -854,7 +1375,7 @@ func addTypeScriptUnit(
854
1375
  Type: artifactTypeScript,
855
1376
  Symbol: symbol,
856
1377
  Path: inventory.Path,
857
- Line: lineAtNode(inventory.Path, node),
1378
+ Line: lineAtNode(inventory.Path, named),
858
1379
  Readable: "TypeScript " + symbol + " '" + target + "'",
859
1380
  Hidden: hidden,
860
1381
  }
@@ -880,6 +1401,19 @@ func addTypeScriptHost(
880
1401
  // lineAtNode stores a byte offset until declarations are scanned against the
881
1402
  // complete source text. A position inside the name is on the declaration
882
1403
  // itself, while both the parent and name full starts may include leading trivia.
1404
+ //
1405
+ // An identifier is its own name and reports none, so it answers from its own
1406
+ // end rather than from the full start it would otherwise fall through to. A
1407
+ // full start begins where the previous token ended, so it lies in the node's
1408
+ // leading trivia rather than among its own tokens whenever any trivia sits
1409
+ // between the two. A variable unit is created from its binding identifier and
1410
+ // took that answer, which is a line above each leaf of a multi-line
1411
+ // destructuring pattern and two above a declarator whose documentation block
1412
+ // sits between it and the token before it.
1413
+ //
1414
+ // The last fallback is left as it was because no unit kind reaches it: every
1415
+ // form is created either from a declaration that carries a name or from an
1416
+ // identifier.
883
1417
  func lineAtNode(_ string, node *shimast.Node) int {
884
1418
  if node == nil {
885
1419
  return 0
@@ -887,6 +1421,9 @@ func lineAtNode(_ string, node *shimast.Node) int {
887
1421
  if name := node.Name(); name != nil && name.End() > 0 {
888
1422
  return name.End() - 1
889
1423
  }
1424
+ if node.Kind == shimast.KindIdentifier && node.End() > 0 {
1425
+ return node.End() - 1
1426
+ }
890
1427
  return node.Pos()
891
1428
  }
892
1429
 
@@ -958,10 +1495,15 @@ func collectTypeScriptDeclarations(
958
1495
  docs[key] = current
959
1496
  }
960
1497
  })
1498
+ attached := make(attachedCommentEnds, len(docs))
961
1499
  keys := make([]string, 0, len(docs))
962
- for key := range docs {
1500
+ for key, entry := range docs {
963
1501
  keys = append(keys, key)
1502
+ if start, taken := attached[entry.node.End()]; !taken || entry.node.Pos() < start {
1503
+ attached[entry.node.End()] = entry.node.Pos()
1504
+ }
964
1505
  }
1506
+ reportUnreadableTypeScriptTags(file, location, attached, inventory)
965
1507
  sort.Slice(keys, func(left int, right int) bool {
966
1508
  leftNode := docs[keys[left]].node
967
1509
  rightNode := docs[keys[right]].node
@@ -1006,11 +1548,31 @@ func collectTypeScriptDeclarations(
1006
1548
  Sequence: sequence,
1007
1549
  })
1008
1550
  }
1551
+ // Reviews are read from the same block by a separate pass into a separate
1552
+ // slice. Sharing the declaration loop would put a review one field away
1553
+ // from every acknowledgement map in evaluation.
1554
+ for _, review := range parseReviews(content[entry.node.Pos():entry.node.End()]) {
1555
+ inventory.Reviews = append(inventory.Reviews, &evidenceReview{
1556
+ HostID: hostID,
1557
+ SemanticHostIDs: semanticHostIDs,
1558
+ Reviews: review.Reviews,
1559
+ Type: artifactTypeScript,
1560
+ Target: review.Target,
1561
+ Fingerprint: review.Fingerprint,
1562
+ Description: review.Description,
1563
+ Path: location,
1564
+ Line: baseLine + review.LineOffset,
1565
+ })
1566
+ }
1009
1567
  }
1010
1568
  for _, unit := range inventory.Units {
1011
1569
  // TypeScript AST positions are byte offsets; translate them only after
1012
1570
  // the complete source text is available.
1013
1571
  unit.Line = lineAt(content, unit.Line)
1572
+ // The digest is deferred to the same pass for the same reason: the nodes
1573
+ // are recorded while walking, and their text needs the source file that
1574
+ // only the caller holds.
1575
+ unit.Digest = typeScriptUnitDigest(file, inventory.UnitContent[unit.ID])
1014
1576
  }
1015
1577
  }
1016
1578
 
@@ -1197,3 +1759,74 @@ func encodeTypeScriptIdentity(identity []string) string {
1197
1759
  }
1198
1760
  return builder.String()
1199
1761
  }
1762
+
1763
+ // matchesConfiguredTypeScriptFile reports whether a population rooted at this
1764
+ // base selects the file.
1765
+ //
1766
+ // The base is compared before the globs, for the reason the Markdown side
1767
+ // states: a walk covers one base at a time, and another base's patterns say
1768
+ // nothing about a path inside this one.
1769
+ func matchesConfiguredTypeScriptFile(
1770
+ config graphConfig,
1771
+ base populationBase,
1772
+ path string,
1773
+ ) bool {
1774
+ for _, claim := range config.Claims {
1775
+ if claim.Type == artifactTypeScript &&
1776
+ claim.Base.Absolute == base.Absolute &&
1777
+ claim.Files.matches(path) {
1778
+ return true
1779
+ }
1780
+ for _, reference := range claim.References {
1781
+ // A package reference reads an installed package from disk, and its
1782
+ // globs are written as a consumer thinks of that package, so they
1783
+ // resolve against the package root rather than the project. Matching
1784
+ // them here against a project-relative path made `lib/**` claim a
1785
+ // project's own `lib/`, and `**/*.ts` claim every file it has,
1786
+ // including the `node_modules` this confinement exists to release.
1787
+ if reference.Package != "" {
1788
+ continue
1789
+ }
1790
+ if reference.Type == artifactTypeScript &&
1791
+ reference.Base.Absolute == base.Absolute &&
1792
+ reference.Files.matches(path) {
1793
+ return true
1794
+ }
1795
+ }
1796
+ }
1797
+ return false
1798
+ }
1799
+
1800
+ // recordGovernedTypeScriptFiles marks every address a declared population
1801
+ // selects.
1802
+ //
1803
+ // Separate from the scan because the two answer different questions. Scanning
1804
+ // is driven by the active configuration, since an inactive claim must make no
1805
+ // loader do work; governance is a property of what an author wrote, and a claim
1806
+ // that deactivated still declared its population. Reading them from one pass
1807
+ // silenced a file whose declarations were all commented out, which is exactly
1808
+ // the shape the diagnostic's second repair clause exists for.
1809
+ func recordGovernedTypeScriptFiles(
1810
+ sources []*shimast.SourceFile,
1811
+ declared graphConfig,
1812
+ governed map[string]bool,
1813
+ ) {
1814
+ if governed == nil {
1815
+ return
1816
+ }
1817
+ bases := configuredBases(declared, artifactTypeScript)
1818
+ for _, file := range sources {
1819
+ if file == nil || !isTypeScriptPath(file.FileName()) {
1820
+ continue
1821
+ }
1822
+ for _, base := range bases {
1823
+ relative, ok := relativeProjectPath(base.Absolute, file.FileName())
1824
+ if !ok || !isTypeScriptPath(relative) {
1825
+ continue
1826
+ }
1827
+ if matchesConfiguredTypeScriptFile(declared, base, relative) {
1828
+ governed[base.addressOf(relative).Key] = true
1829
+ }
1830
+ }
1831
+ }
1832
+ }