@ttsc/evidence 0.29.0 → 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.
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 {
@@ -0,0 +1,370 @@
1
+ package evidence
2
+
3
+ import (
4
+ "encoding/json"
5
+ "net/url"
6
+ "path"
7
+ "path/filepath"
8
+ "sort"
9
+ "strings"
10
+ "unicode"
11
+ )
12
+
13
+ const fileLinkPrefix = "\x00file:"
14
+
15
+ func isFileLinkTarget(target string) bool {
16
+ return strings.HasPrefix(target, fileLinkPrefix)
17
+ }
18
+
19
+ // splitFileLinkBody reads one address, allowing spaces only inside a quoted
20
+ // accessor segment. Paths use percent encoding, so # always separates the file.
21
+ func splitFileLinkBody(body string) (string, string, bool) {
22
+ forced := isFileLinkTarget(body)
23
+ if !forced {
24
+ for _, marker := range inlineLinkTags {
25
+ if strings.HasPrefix(body, marker) {
26
+ return "", "", false
27
+ }
28
+ }
29
+ }
30
+ body = strings.TrimPrefix(body, fileLinkPrefix)
31
+ file, _, qualified := strings.Cut(body, "#")
32
+ if decoded, err := url.PathUnescape(file); err == nil {
33
+ file = decoded
34
+ }
35
+ if !forced && (!qualified || !isTypeScriptPath(file)) {
36
+ return "", "", false
37
+ }
38
+ quoted, escaped := false, false
39
+ end := len(body)
40
+ for index, char := range body {
41
+ if escaped {
42
+ escaped = false
43
+ continue
44
+ }
45
+ if quoted && char == '\\' {
46
+ escaped = true
47
+ continue
48
+ }
49
+ if char == '"' {
50
+ quoted = !quoted
51
+ }
52
+ if !quoted && unicode.IsSpace(char) {
53
+ end = index
54
+ break
55
+ }
56
+ }
57
+ target := body[:end]
58
+ if target == "" {
59
+ return "", strings.TrimSpace(body[end:]), true
60
+ }
61
+ if link, problem := parseFileLink(target); problem == "" {
62
+ target = encodeFileLinkPath(link.File) + "#" + formatFileAccessor(link.Segments)
63
+ }
64
+ return fileLinkPrefix + target, strings.TrimSpace(body[end:]), true
65
+ }
66
+
67
+ type fileLink struct {
68
+ File string
69
+ Segments []string
70
+ }
71
+
72
+ func parseFileLink(target string) (fileLink, string) {
73
+ target = strings.TrimPrefix(target, fileLinkPrefix)
74
+ file, accessor, found := strings.Cut(target, "#")
75
+ if !found || file == "" || accessor == "" {
76
+ return fileLink{}, "write a TypeScript file path followed by '#' and an exported declaration or member"
77
+ }
78
+ decoded, err := url.PathUnescape(file)
79
+ if err != nil || strings.ContainsAny(decoded, "\x00\r\n") || !isTypeScriptPath(decoded) {
80
+ return fileLink{}, "the file must be a TypeScript path with valid percent escapes; use %20 for a space and %23 for a literal '#'"
81
+ }
82
+ normalized := strings.ReplaceAll(decoded, "\\", "/")
83
+ // path.Clean collapses a UNC prefix; preserve that volume spelling.
84
+ unc := strings.HasPrefix(normalized, "//")
85
+ normalized = path.Clean(normalized)
86
+ if unc {
87
+ normalized = "/" + normalized
88
+ }
89
+ segments, problem := parseFileAccessor(accessor)
90
+ if problem != "" {
91
+ return fileLink{}, problem
92
+ }
93
+ return fileLink{File: normalized, Segments: segments}, ""
94
+ }
95
+
96
+ func parseFileAccessor(accessor string) ([]string, string) {
97
+ segments := []string{}
98
+ for len(accessor) != 0 {
99
+ if accessor[0] == '[' {
100
+ closing := -1
101
+ quoted, escaped := false, false
102
+ for index := 1; index < len(accessor); index++ {
103
+ char := accessor[index]
104
+ if escaped {
105
+ escaped = false
106
+ continue
107
+ }
108
+ if quoted && char == '\\' {
109
+ escaped = true
110
+ continue
111
+ }
112
+ if char == '"' {
113
+ quoted = !quoted
114
+ }
115
+ if !quoted && char == ']' {
116
+ closing = index
117
+ break
118
+ }
119
+ }
120
+ if closing < 0 {
121
+ return nil, "close the bracket accessor and its quoted member name"
122
+ }
123
+ literal := accessor[1:closing]
124
+ var segment string
125
+ if len(literal) > 0 && literal[0] == '"' {
126
+ if json.Unmarshal([]byte(literal), &segment) != nil {
127
+ return nil, "a bracket accessor must contain a JSON string or an unsigned integer"
128
+ }
129
+ } else {
130
+ if literal == "" || strings.Trim(literal, "0123456789") != "" || len(literal) > 1 && literal[0] == '0' {
131
+ return nil, "a bracket accessor must contain a JSON string or an unsigned integer"
132
+ }
133
+ segment = literal
134
+ }
135
+ segments = append(segments, segment)
136
+ accessor = accessor[closing+1:]
137
+ } else {
138
+ end := strings.IndexAny(accessor, ".[")
139
+ if end < 0 {
140
+ end = len(accessor)
141
+ }
142
+ segment := accessor[:end]
143
+ if !fileAccessorIdentifier(segment) {
144
+ return nil, "use dotted identifiers or brackets containing a JSON string for literal names"
145
+ }
146
+ segments = append(segments, segment)
147
+ accessor = accessor[end:]
148
+ }
149
+ if accessor == "" {
150
+ break
151
+ }
152
+ if accessor[0] == '.' {
153
+ accessor = accessor[1:]
154
+ if accessor == "" || accessor[0] == '[' {
155
+ return nil, "a dot must be followed by an identifier"
156
+ }
157
+ } else if accessor[0] != '[' {
158
+ return nil, "separate accessor segments with a dot or a bracket"
159
+ }
160
+ }
161
+ if len(segments) == 0 {
162
+ return nil, "name an exported declaration after '#'"
163
+ }
164
+ return segments, ""
165
+ }
166
+
167
+ func fileAccessorIdentifier(value string) bool {
168
+ for index, char := range value {
169
+ if char == '_' || char == '$' || unicode.IsLetter(char) || index > 0 && (unicode.IsDigit(char) || unicode.IsMark(char)) {
170
+ continue
171
+ }
172
+ return false
173
+ }
174
+ return value != ""
175
+ }
176
+
177
+ func formatFileAccessor(segments []string) string {
178
+ var result strings.Builder
179
+ for index, segment := range segments {
180
+ if fileAccessorIdentifier(segment) {
181
+ if index > 0 {
182
+ result.WriteByte('.')
183
+ }
184
+ result.WriteString(segment)
185
+ } else {
186
+ encoded, _ := json.Marshal(segment)
187
+ result.WriteByte('[')
188
+ result.Write(encoded)
189
+ result.WriteByte(']')
190
+ }
191
+ }
192
+ return result.String()
193
+ }
194
+
195
+ func encodeFileLinkPath(file string) string {
196
+ return (&url.URL{Path: file}).EscapedPath()
197
+ }
198
+
199
+ // resolveFileLinkDeclaration selects by module and segmented public accessor.
200
+ // It never searches other files for a matching name. A review still annotates
201
+ // this citation; the existing unit ID owns coverage and content fingerprints.
202
+ func resolveFileLinkDeclaration(declaration *evidenceDeclaration, owners []claimState, loader *typeScriptLoader, index map[int]*fileLinkClaimIndex, context string) (string, string) {
203
+ where := " '" + displayTarget(declaration.Target) + "' at " + declaration.location() + " for " + context
204
+ if declaration.Type != artifactMarkdown && declaration.Type != artifactTypeScript {
205
+ return "", "Unsupported file-qualified evidence target" + where + ": use this syntax on a Markdown or TypeScript claim."
206
+ }
207
+ link, problem := parseFileLink(declaration.Target)
208
+ if problem != "" {
209
+ return "", "Malformed file-qualified evidence target" + where + ": " + problem + "."
210
+ }
211
+ absolute := filepath.FromSlash(link.File)
212
+ if !filepath.IsAbs(absolute) {
213
+ absolute = filepath.Join(filepath.Dir(resolveProjectPath(loader.root, declaration.Path)), absolute)
214
+ }
215
+ module := loader.projectPath(filepath.ToSlash(absolute))
216
+ identity, _ := loader.moduleIdentity(module)
217
+ candidates := map[string]*evidenceUnit{}
218
+ hidden := map[string]*evidenceUnit{}
219
+ selectedModule := false
220
+ key := scopedTargetKey{path: identity, target: encodeTypeScriptIdentity(link.Segments)}
221
+ for _, owner := range owners {
222
+ claim := index[owner.Spec.Index]
223
+ if claim == nil {
224
+ continue
225
+ }
226
+ selectedModule = selectedModule || claim.modules[identity]
227
+ for id, unit := range claim.targets[key] {
228
+ candidates[id] = unit
229
+ }
230
+ for id, unit := range claim.hidden[key] {
231
+ hidden[id] = unit
232
+ }
233
+ }
234
+ dynamic, withdrawn := queryFileLinkClaims(index, owners, identity, link.Segments, false)
235
+ for id, unit := range dynamic {
236
+ candidates[id] = unit
237
+ }
238
+ for id, unit := range withdrawn {
239
+ hidden[id] = unit
240
+ }
241
+ if len(candidates) == 1 {
242
+ for id := range candidates {
243
+ return id, ""
244
+ }
245
+ }
246
+ if len(candidates) > 1 {
247
+ descriptions := []string{}
248
+ for _, unit := range candidates {
249
+ descriptions = append(descriptions, unit.Readable+" at "+unit.location())
250
+ }
251
+ sort.Strings(descriptions)
252
+ return "", "Ambiguous file-qualified evidence target" + where + ": " + strings.Join(descriptions, "; ") + ". Select one symbol kind or an unambiguous public accessor."
253
+ }
254
+ if len(hidden) != 0 {
255
+ ids := []string{}
256
+ for id := range hidden {
257
+ ids = append(ids, id)
258
+ }
259
+ sort.Strings(ids)
260
+ return "", hiddenTargetProblem(declaration, hidden[ids[0]], context)
261
+ }
262
+ if !loader.exists(module) {
263
+ return "", "Missing TypeScript evidence file" + where + ": '" + module + "' does not exist. Correct the path relative to the citing file or restore the configured file."
264
+ }
265
+ if !selectedModule {
266
+ return "", "Out-of-population TypeScript evidence target" + where + ": '" + module + "' is not a module selected by this claim's TypeScript references. Configure its files and, for disk sources outside the Program, an explicit root."
267
+ }
268
+ if declarationResolutionUncertain(owners) {
269
+ return "", ""
270
+ }
271
+ return "", diagnoseFileLinkTarget(loader, module, link.Segments, where)
272
+ }
273
+
274
+ type fileLinkClaimIndex struct {
275
+ references map[string][]fileLinkReference
276
+ modules map[string]bool
277
+ targets map[scopedTargetKey]map[string]*evidenceUnit
278
+ hidden map[scopedTargetKey]map[string]*evidenceUnit
279
+ }
280
+
281
+ // Build once per evaluation. Resolution is proportional to the number of
282
+ // owning claims, not their complete populations for every citation.
283
+ func indexFileLinkClaims(states []claimState, loader *typeScriptLoader) map[int]*fileLinkClaimIndex {
284
+ result := map[int]*fileLinkClaimIndex{}
285
+ for _, state := range states {
286
+ index := &fileLinkClaimIndex{modules: map[string]bool{}, targets: map[scopedTargetKey]map[string]*evidenceUnit{}, hidden: map[scopedTargetKey]map[string]*evidenceUnit{}, references: map[string][]fileLinkReference{}}
287
+ result[state.Spec.Index] = index
288
+ for _, reference := range state.References {
289
+ if reference.Spec.Type != artifactTypeScript {
290
+ continue
291
+ }
292
+ scopes := map[string]bool{}
293
+ for _, scope := range reference.Scopes {
294
+ scopes[scope.ID] = true
295
+ }
296
+ if reference.Code != nil {
297
+ modules := map[string]string{}
298
+ for _, entry := range reference.Paths {
299
+ identity, _ := loader.moduleIdentity(entry)
300
+ modules[identity] = entry
301
+ }
302
+ for _, address := range reference.Published {
303
+ identity, _ := loader.moduleIdentity(address.Module)
304
+ modules[identity] = address.Module
305
+ }
306
+ hidden := map[string]bool{}
307
+ for _, unit := range reference.Hidden {
308
+ hidden[unit.ID] = true
309
+ }
310
+ for identity, entry := range modules {
311
+ index.references[identity] = append(index.references[identity], fileLinkReference{entry: entry, code: reference.Code, scopes: scopes, hidden: hidden})
312
+ }
313
+ }
314
+ for _, entry := range reference.Paths {
315
+ module, _ := loader.moduleIdentity(entry)
316
+ index.modules[module] = true
317
+ }
318
+ for _, address := range reference.Published {
319
+ module, _ := loader.moduleIdentity(address.Module)
320
+ index.modules[module] = true
321
+ key := scopedTargetKey{path: module, target: encodeTypeScriptIdentity(address.Segments)}
322
+ target := index.targets
323
+ if address.Unit.Hidden != "" {
324
+ target = index.hidden
325
+ } else if !scopes[address.Unit.ID] {
326
+ continue
327
+ }
328
+ if target[key] == nil {
329
+ target[key] = map[string]*evidenceUnit{}
330
+ }
331
+ target[key][address.Unit.ID] = address.Unit
332
+ }
333
+ }
334
+ }
335
+ return result
336
+ }
337
+
338
+ type fileLinkReference struct {
339
+ entry string
340
+ code *typeScriptExportResolver
341
+ scopes map[string]bool
342
+ hidden map[string]bool
343
+ }
344
+
345
+ // Namespace cycles have infinitely many possible spellings but a citation has
346
+ // finitely many segments. Resolve that path on demand, then apply the same
347
+ // reference membership as the finite population index.
348
+ func queryFileLinkClaims(index map[int]*fileLinkClaimIndex, owners []claimState, module string, segments []string, legacy bool) (map[string]*evidenceUnit, map[string]*evidenceUnit) {
349
+ found, hidden := map[string]*evidenceUnit{}, map[string]*evidenceUnit{}
350
+ for _, owner := range owners {
351
+ claim := index[owner.Spec.Index]
352
+ if claim == nil {
353
+ continue
354
+ }
355
+ for _, reference := range claim.references[module] {
356
+ for _, unit := range reference.code.lookup(reference.entry, segments, legacy) {
357
+ if unit.Hidden != "" {
358
+ if reference.hidden[unit.ID] {
359
+ hidden[unit.ID] = unit
360
+ }
361
+ continue
362
+ }
363
+ if reference.scopes[unit.ID] {
364
+ found[unit.ID] = unit
365
+ }
366
+ }
367
+ }
368
+ }
369
+ return found, hidden
370
+ }