@ttsc/evidence 0.28.6 → 0.30.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.
@@ -0,0 +1,275 @@
1
+ package evidence
2
+
3
+ import (
4
+ "sort"
5
+ "strings"
6
+ )
7
+
8
+ // Export names are finite even when modules re-export each other. Discover
9
+ // that vocabulary before resolving bindings, and expand namespace paths only
10
+ // after a binding is known. A module recursion guard cannot answer whether a
11
+ // particular export exists: the module may be busy resolving another name.
12
+ type typeScriptExportResolver struct {
13
+ loader *typeScriptLoader
14
+ modules map[string]*typeScriptExportModule
15
+ bindings map[scopedTargetKey][]reachedSymbol
16
+ owned *ownedUnitIndex
17
+ }
18
+
19
+ type typeScriptExportModule struct {
20
+ exports []moduleExport
21
+ targets map[string]string
22
+ names map[string]bool
23
+ named map[string][]moduleExport
24
+ stars []moduleExport
25
+ }
26
+
27
+ func newTypeScriptExportResolver(loader *typeScriptLoader, entries []string) *typeScriptExportResolver {
28
+ resolver := &typeScriptExportResolver{loader: loader, modules: map[string]*typeScriptExportModule{}, bindings: map[scopedTargetKey][]reachedSymbol{}}
29
+ resolver.owned = newOwnedUnitIndex(loader)
30
+ for _, entry := range entries {
31
+ resolver.load(entry)
32
+ }
33
+ keys := make([]string, 0, len(resolver.modules))
34
+ for key := range resolver.modules {
35
+ keys = append(keys, key)
36
+ }
37
+ sort.Strings(keys)
38
+ // Only star edges add names. Namespace and named re-exports already have
39
+ // explicit names, so namespace cycles cannot grow this fixed point forever.
40
+ for changed := true; changed; {
41
+ changed = false
42
+ for _, key := range keys {
43
+ module := resolver.modules[key]
44
+ for _, export := range module.stars {
45
+ target := resolver.modules[module.targets[export.Specifier]]
46
+ if target == nil {
47
+ continue
48
+ }
49
+ for name := range target.names {
50
+ if name != "default" && !module.names[name] {
51
+ module.names[name] = true
52
+ changed = true
53
+ }
54
+ }
55
+ }
56
+ }
57
+ }
58
+ if loader.boundary != nil {
59
+ for _, key := range keys {
60
+ module := resolver.modules[key]
61
+ for _, export := range module.exports {
62
+ if export.Specifier == "" || export.Namespace || export.Imported == "" {
63
+ continue
64
+ }
65
+ target := module.targets[export.Specifier]
66
+ if resolver.modules[target] == nil {
67
+ continue
68
+ }
69
+ if len(resolver.resolve(target, export.Imported)) == 0 {
70
+ loader.failures[key+" -> "+export.Specifier+"#"+export.Imported] = "the module has no public export named '" + export.Imported + "'"
71
+ }
72
+ }
73
+ }
74
+ }
75
+ return resolver
76
+ }
77
+
78
+ func (resolver *typeScriptExportResolver) load(entry string) {
79
+ if _, loaded := resolver.modules[entry]; loaded {
80
+ return
81
+ }
82
+ inventory := resolver.loader.inventory(entry)
83
+ if inventory == nil {
84
+ return
85
+ }
86
+ module := &typeScriptExportModule{exports: inventory.Exports, targets: map[string]string{}, names: map[string]bool{}, named: map[string][]moduleExport{}}
87
+ declarations := map[string]bool{}
88
+ if inventory.Source != nil && inventory.Source.Statements != nil {
89
+ for _, statement := range inventory.Source.Statements.Nodes {
90
+ if statement == nil {
91
+ continue
92
+ }
93
+ for _, declaration := range topLevelDeclaredNames(statement) {
94
+ declarations[declaration.Name] = true
95
+ }
96
+ if name := typeScriptDeclarationName(statement); name != "" {
97
+ declarations[name] = true
98
+ }
99
+ }
100
+ }
101
+ resolver.modules[entry] = module
102
+ for _, export := range module.exports {
103
+ if resolver.loader.boundary != nil && export.Specifier == "" && !declarations[export.Local] {
104
+ resolver.loader.failures[entry+"#"+export.Public] = "the exported binding '" + export.Local + "' has no declaration in this module"
105
+ continue
106
+ }
107
+ if export.Public != "" {
108
+ module.names[export.Public] = true
109
+ module.named[export.Public] = append(module.named[export.Public], export)
110
+ } else if export.Specifier != "" {
111
+ module.stars = append(module.stars, export)
112
+ }
113
+ if export.Specifier == "" {
114
+ continue
115
+ }
116
+ if _, resolved := module.targets[export.Specifier]; resolved {
117
+ continue
118
+ }
119
+ target := resolver.loader.resolve(entry, export.Specifier)
120
+ module.targets[export.Specifier] = target
121
+ if target != "" {
122
+ resolver.load(target)
123
+ }
124
+ }
125
+ }
126
+
127
+ // accessor follows only the namespace segments the citation actually names.
128
+ // Each hop consumes at least one query segment, so namespace cycles are finite.
129
+ // Legacy inline spelling can join several query segments into a literal export
130
+ // name; Address records the consumed query segments for the remaining lookup.
131
+ func (resolver *typeScriptExportResolver) accessor(entry string, segments []string, prefix []string, typeOnly bool, legacy bool) []reachedSymbol {
132
+ if len(segments) == 0 {
133
+ return nil
134
+ }
135
+ result := []reachedSymbol{}
136
+ limit := 1
137
+ if legacy {
138
+ limit = len(segments)
139
+ }
140
+ for consumed := 1; consumed <= limit; consumed++ {
141
+ name := strings.Join(segments[:consumed], ".")
142
+ for _, binding := range resolver.resolve(entry, name) {
143
+ binding.Address = append(append([]string{}, prefix...), segments[:consumed]...)
144
+ binding.TypeOnly = binding.TypeOnly || typeOnly
145
+ // Keep namespace prefixes for diagnostics, including when the next name
146
+ // is missing. They never become units in lookup or population traversal.
147
+ result = append(result, binding)
148
+ if binding.Local == "" {
149
+ result = append(result, resolver.accessor(binding.Path, segments[consumed:], binding.Address, binding.TypeOnly, legacy)...)
150
+ }
151
+ }
152
+ }
153
+ return result
154
+ }
155
+
156
+ func (resolver *typeScriptExportResolver) lookup(entry string, segments []string, legacy bool) []*evidenceUnit {
157
+ result := []*evidenceUnit{}
158
+ seen := map[string]bool{}
159
+ for _, binding := range resolver.accessor(entry, segments, nil, false, legacy) {
160
+ if binding.Local == "" {
161
+ continue
162
+ }
163
+ target := append([]string{binding.Local}, segments[len(binding.Address):]...)
164
+ for _, unit := range resolver.owned.of(binding.Path, binding.Local) {
165
+ if binding.TypeOnly && unit.ValueSpace && !unit.TypeSpace {
166
+ continue
167
+ }
168
+ matches := encodeTypeScriptIdentity(unit.Identity) == encodeTypeScriptIdentity(target)
169
+ if legacy {
170
+ matches = strings.Join(unit.Identity, ".") == strings.Join(target, ".")
171
+ }
172
+ if matches && !seen[unit.ID] {
173
+ seen[unit.ID] = true
174
+ result = append(result, unit)
175
+ }
176
+ }
177
+ }
178
+ return result
179
+ }
180
+
181
+ func (resolver *typeScriptExportResolver) resolve(module string, name string) []reachedSymbol {
182
+ key := scopedTargetKey{path: module, target: name}
183
+ if cached, exists := resolver.bindings[key]; exists {
184
+ return cached
185
+ }
186
+ resolved := resolver.resolveFrom(key, map[scopedTargetKey]bool{})
187
+ resolver.bindings[key] = resolved
188
+ return resolved
189
+ }
190
+
191
+ func (resolver *typeScriptExportResolver) resolveFrom(key scopedTargetKey, visited map[scopedTargetKey]bool) []reachedSymbol {
192
+ if visited[key] {
193
+ return nil
194
+ }
195
+ if cached, exists := resolver.bindings[key]; exists {
196
+ return cached
197
+ }
198
+ module := resolver.modules[key.path]
199
+ if module == nil {
200
+ return nil
201
+ }
202
+ visited[key] = true
203
+ defer delete(visited, key)
204
+ result := []reachedSymbol{}
205
+ byBinding := map[scopedTargetKey]int{}
206
+ add := func(binding reachedSymbol, typeOnly bool) {
207
+ binding.TypeOnly = binding.TypeOnly || typeOnly
208
+ identity := scopedTargetKey{path: binding.Path, target: binding.Local}
209
+ if index, exists := byBinding[identity]; exists {
210
+ result[index].TypeOnly = result[index].TypeOnly && binding.TypeOnly
211
+ return
212
+ }
213
+ byBinding[identity] = len(result)
214
+ result = append(result, binding)
215
+ }
216
+ exports := module.named[key.target]
217
+ if len(exports) == 0 && key.target != "default" {
218
+ exports = module.stars
219
+ }
220
+ for _, export := range exports {
221
+ if export.Specifier == "" {
222
+ local := export.Public
223
+ if export.Identity != "" {
224
+ local = export.Identity
225
+ }
226
+ add(reachedSymbol{Path: key.path, Local: local}, export.TypeOnly)
227
+ continue
228
+ }
229
+ target := module.targets[export.Specifier]
230
+ if resolver.modules[target] == nil {
231
+ continue
232
+ }
233
+ if export.Namespace {
234
+ add(reachedSymbol{Path: target}, export.TypeOnly)
235
+ continue
236
+ }
237
+ imported := key.target
238
+ if export.Imported != "" {
239
+ imported = export.Imported
240
+ }
241
+ for _, binding := range resolver.resolveFrom(scopedTargetKey{path: target, target: imported}, visited) {
242
+ add(binding, export.TypeOnly)
243
+ }
244
+ }
245
+ return result
246
+ }
247
+
248
+ func (resolver *typeScriptExportResolver) traverse(entry string, prefix []string, visited map[string]bool, typeOnly bool) []reachedSymbol {
249
+ if visited[entry] {
250
+ return nil
251
+ }
252
+ module := resolver.modules[entry]
253
+ if module == nil {
254
+ return nil
255
+ }
256
+ visited[entry] = true
257
+ defer delete(visited, entry)
258
+ names := make([]string, 0, len(module.names))
259
+ for name := range module.names {
260
+ names = append(names, name)
261
+ }
262
+ sort.Strings(names)
263
+ reached := []reachedSymbol{}
264
+ for _, name := range names {
265
+ for _, binding := range resolver.resolve(entry, name) {
266
+ binding.Address = append(append([]string{}, prefix...), name)
267
+ binding.TypeOnly = binding.TypeOnly || typeOnly
268
+ reached = append(reached, binding)
269
+ if binding.Local == "" {
270
+ reached = append(reached, resolver.traverse(binding.Path, binding.Address, visited, binding.TypeOnly)...)
271
+ }
272
+ }
273
+ }
274
+ return reached
275
+ }
package/native/exports.go CHANGED
@@ -13,6 +13,9 @@ import (
13
13
  // another. Keeping the two apart is what lets identity stay with the declaring
14
14
  // file while reachability is decided by whoever re-exports it.
15
15
  type moduleExport struct {
16
+ // Identity overrides the local unit name for a default alias. The alias
17
+ // adds reachability without materializing a second declaration.
18
+ Identity string
16
19
  // Public is the name an importer sees. Empty for `export * from`, which
17
20
  // contributes its target's names rather than a name of its own.
18
21
  Public string
@@ -49,6 +52,7 @@ func collectModuleExports(file *shimast.SourceFile) []moduleExport {
49
52
  return exports
50
53
  }
51
54
  locals := collectLocalExportNames(file.Statements)
55
+ defaults := collectDefaultExportBindings(file.Statements)
52
56
  for _, statement := range file.Statements.Nodes {
53
57
  if statement == nil {
54
58
  continue
@@ -60,6 +64,13 @@ func collectModuleExports(file *shimast.SourceFile) []moduleExport {
60
64
  if !isSyntacticallyExported(statement) {
61
65
  continue
62
66
  }
67
+ if isDefaultExported(statement) {
68
+ name := typeScriptDeclarationName(statement)
69
+ if name != "" {
70
+ exports = append(exports, moduleExport{Public: "default", Local: name, Identity: name})
71
+ }
72
+ continue
73
+ }
63
74
  for _, declared := range topLevelDeclaredNames(statement) {
64
75
  exports = append(exports, moduleExport{
65
76
  Public: declared.Name,
@@ -68,8 +79,9 @@ func collectModuleExports(file *shimast.SourceFile) []moduleExport {
68
79
  for _, exported := range locals[declared.Name] {
69
80
  if exported.Public != declared.Name {
70
81
  exports = append(exports, moduleExport{
71
- Public: exported.Public,
72
- Local: declared.Name,
82
+ Public: exported.Public,
83
+ Local: declared.Name,
84
+ TypeOnly: exported.TypeOnly,
73
85
  })
74
86
  }
75
87
  }
@@ -77,15 +89,41 @@ func collectModuleExports(file *shimast.SourceFile) []moduleExport {
77
89
  }
78
90
  for local, names := range locals {
79
91
  for _, exported := range names {
80
- if exported.Public == "" {
92
+ if exported.Public == "" || exported.IdentityOnly {
81
93
  continue
82
94
  }
83
95
  exports = append(exports, moduleExport{
84
- Public: exported.Public,
85
- Local: local,
96
+ Public: exported.Public,
97
+ Local: local,
98
+ TypeOnly: exported.TypeOnly,
86
99
  })
87
100
  }
88
101
  }
102
+ for local, binding := range defaults {
103
+ identity := local
104
+ chosen := false
105
+ for _, name := range locals[local] {
106
+ if !binding.TypeOnly && name.TypeOnly && !name.ValueIdentity {
107
+ continue
108
+ }
109
+ if !chosen || name.Public < identity {
110
+ identity, chosen = name.Public, true
111
+ }
112
+ }
113
+ exports = append(exports, moduleExport{Public: "default", Local: local, Identity: identity, TypeOnly: binding.TypeOnly})
114
+ }
115
+ imports := collectImportBindings(file)
116
+ for index := range exports {
117
+ export := &exports[index]
118
+ if export.Specifier != "" {
119
+ continue
120
+ }
121
+ if binding, imported := imports[export.Local]; imported {
122
+ export.Specifier, export.Imported, export.Namespace = binding.Specifier, binding.Imported, binding.Namespace
123
+ export.Identity = ""
124
+ export.TypeOnly = export.TypeOnly || binding.TypeOnly
125
+ }
126
+ }
89
127
  return dedupeModuleExports(exports)
90
128
  }
91
129
 
@@ -181,6 +219,7 @@ func dedupeModuleExports(exports []moduleExport) []moduleExport {
181
219
  // yields: the obligations, the scopes above them, and the addresses that reach
182
220
  // them.
183
221
  type traversedPopulation struct {
222
+ Code *typeScriptExportResolver
184
223
  Units []*evidenceUnit
185
224
  Reached []*evidenceUnit
186
225
  // Hidden are the reached declarations that withdrew themselves from the
@@ -198,9 +237,10 @@ type traversedPopulation struct {
198
237
  // several selected modules publish the same declaration: each address is legal
199
238
  // exactly where it is written, and none of them is legal everywhere.
200
239
  type publishedAddress struct {
201
- Module string
202
- Address string
203
- Unit *evidenceUnit
240
+ Module string
241
+ Address string
242
+ Segments []string
243
+ Unit *evidenceUnit
204
244
  }
205
245
 
206
246
  type reachedSymbol struct {
@@ -208,7 +248,8 @@ type reachedSymbol struct {
208
248
  Address []string
209
249
  // Path is the file that declares the symbol, which owns its identity.
210
250
  Path string
211
- // Local is the name that file's inventory gives the declaration, which is
251
+ // Local is empty for a module namespace, which publishes names but declares
252
+ // no evidence unit. Otherwise it is the name that file's inventory gives the declaration, which is
212
253
  // the name it exposes rather than the binding it wrote. `export { a as b }`
213
254
  // declares one unit called `b`, so matching on `a` would find nothing.
214
255
  Local string
@@ -219,123 +260,6 @@ type reachedSymbol struct {
219
260
  TypeOnly bool
220
261
  }
221
262
 
222
- // traverseEntryExports walks a module's export graph and reports every public
223
- // symbol it reaches, with the address the entry gives it.
224
- //
225
- // Membership comes from reachability and identity comes from the declaring
226
- // file, so a symbol re-exported through two barrels is reached twice and still
227
- // resolves to one unit. Cycles are bounded by the visited set: a barrel that
228
- // re-exports itself is a real shape, not a reason to hang the build.
229
- func traverseEntryExports(
230
- loader *typeScriptLoader,
231
- entry string,
232
- prefix []string,
233
- visited map[string]bool,
234
- typeOnly bool,
235
- ) []reachedSymbol {
236
- if visited[entry] {
237
- return nil
238
- }
239
- visited[entry] = true
240
- defer delete(visited, entry)
241
-
242
- inventory := loader.inventory(entry)
243
- if inventory == nil {
244
- return nil
245
- }
246
- reached := []reachedSymbol{}
247
- // Named re-exports are grouped by the module they come from and that module
248
- // is walked once. Walking it per specifier would re-traverse the whole
249
- // subtree for every name a barrel forwards, which is quadratic on exactly
250
- // the wide generated barrels this selection exists to describe.
251
- surfaces := map[string]map[string]reachedSymbol{}
252
- surfaceOf := func(target string) map[string]reachedSymbol {
253
- if surface, built := surfaces[target]; built {
254
- return surface
255
- }
256
- surface := map[string]reachedSymbol{}
257
- for _, nested := range traverseEntryExports(loader, target, nil, visited, false) {
258
- if len(nested.Address) != 1 {
259
- continue
260
- }
261
- existing, taken := surface[nested.Address[0]]
262
- // First wins, because two paths to one declaration name the same unit and
263
- // the entry has to keep describing a path that exists.
264
- //
265
- // The type-only mark is unioned, and only between paths to the same
266
- // declaration. It differs per path while the population is the union of
267
- // what the paths reach, so a value path has to clear a type-only one
268
- // rather than lose to declaration order. Doing that unconditionally is
269
- // two different mistakes: replacing the entry swaps `Path` and `Local`,
270
- // and clearing the mark alone leaves `Path` from one entry with the mark
271
- // from another, which is a combination no path produced. Two entries
272
- // under one name are not always one declaration, since an explicit named
273
- // re-export shadows a star, and either mistake publishes the value
274
- // members of a declaration this module reaches type-only.
275
- switch {
276
- case !taken:
277
- surface[nested.Address[0]] = nested
278
- case existing.TypeOnly && !nested.TypeOnly &&
279
- existing.Path == nested.Path && existing.Local == nested.Local:
280
- existing.TypeOnly = false
281
- surface[nested.Address[0]] = existing
282
- }
283
- }
284
- surfaces[target] = surface
285
- return surface
286
- }
287
- for _, export := range inventory.Exports {
288
- if export.Specifier == "" {
289
- // A declaration this module exposes is inventoried under the name it
290
- // exposes it as. `export { local as renamed }` is one unit named
291
- // `renamed`, so the local binding never identifies it.
292
- reached = append(reached, reachedSymbol{
293
- Address: append(append([]string{}, prefix...), export.Public),
294
- Path: entry,
295
- Local: export.Public,
296
- TypeOnly: typeOnly,
297
- })
298
- continue
299
- }
300
- target := loader.resolve(entry, export.Specifier)
301
- if target == "" {
302
- continue
303
- }
304
- switch {
305
- case export.Namespace:
306
- reached = append(reached, traverseEntryExports(
307
- loader,
308
- target,
309
- append(append([]string{}, prefix...), export.Public),
310
- visited,
311
- typeOnly || export.TypeOnly,
312
- )...)
313
- case export.Imported == "":
314
- reached = append(reached, traverseEntryExports(
315
- loader,
316
- target,
317
- prefix,
318
- visited,
319
- typeOnly || export.TypeOnly,
320
- )...)
321
- default:
322
- nested, found := surfaceOf(target)[export.Imported]
323
- if !found {
324
- continue
325
- }
326
- reached = append(reached, reachedSymbol{
327
- Address: append(append([]string{}, prefix...), export.Public),
328
- Path: nested.Path,
329
- Local: nested.Local,
330
- // The edge into this module and every edge the target itself walked
331
- // both count, so a type-only hop anywhere on the path withholds.
332
- TypeOnly: typeOnly || export.TypeOnly || nested.TypeOnly,
333
- })
334
- }
335
- }
336
- return reached
337
- }
338
-
339
263
  // materializeEntryUnits turns reached symbols into the units an obligation
340
264
  // counts, giving each its entry-relative address.
341
265
  //
@@ -362,8 +286,9 @@ func materializeEntryUnits(
362
286
  order := []string{}
363
287
  published := []publishedAddress{}
364
288
  candidates := newOwnedUnitIndex(loader)
289
+ exports := newTypeScriptExportResolver(loader, entries)
365
290
  for _, entry := range entries {
366
- for _, symbol := range traverseEntryExports(loader, entry, nil, map[string]bool{}, false) {
291
+ for _, symbol := range exports.traverse(entry, nil, map[string]bool{}, false) {
367
292
  if symbol.Local == "" {
368
293
  continue
369
294
  }
@@ -403,14 +328,15 @@ func materializeEntryUnits(
403
328
  }
404
329
  }
405
330
  published = append(published, publishedAddress{
406
- Module: entry,
407
- Address: target,
408
- Unit: current,
331
+ Module: entry,
332
+ Address: target,
333
+ Segments: append([]string{}, address...),
334
+ Unit: current,
409
335
  })
410
336
  }
411
337
  }
412
338
  }
413
- population := traversedPopulation{Published: published}
339
+ population := traversedPopulation{Published: published, Code: exports}
414
340
  for _, id := range order {
415
341
  unit := byID[id]
416
342
  sort.Strings(unit.Aliases)
@@ -460,6 +386,7 @@ func narrowTraversedPopulation(
460
386
  withdrawn[unit.ID] = true
461
387
  }
462
388
  narrowed := traversedPopulation{
389
+ Code: published.Code,
463
390
  Reached: published.Reached,
464
391
  Published: published.Published,
465
392
  }
@@ -509,26 +436,17 @@ func (index *ownedUnitIndex) of(path string, local string) []*evidenceUnit {
509
436
  }
510
437
  index.byPath[path] = roots
511
438
  }
512
- root := local
513
- if cut := strings.Index(local, "."); cut != -1 {
514
- root = local[:cut]
515
- }
516
- return roots[root]
439
+ return roots[local]
517
440
  }
518
441
 
519
442
  // identitySuffix reports the segments below a reached declaration, and whether
520
443
  // the unit belongs to it at all.
521
444
  func identitySuffix(identity []string, local string) ([]string, bool) {
522
- owner := strings.Split(local, ".")
523
- if len(identity) < len(owner) {
445
+ // Local is one module export, including a quoted name containing a dot.
446
+ if len(identity) == 0 || identity[0] != local {
524
447
  return nil, false
525
448
  }
526
- for index, segment := range owner {
527
- if identity[index] != segment {
528
- return nil, false
529
- }
530
- }
531
- return identity[len(owner):], true
449
+ return identity[1:], true
532
450
  }
533
451
 
534
452
  func containsString(values []string, wanted string) bool {