@ttsc/evidence 0.29.0 → 0.30.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.
- package/lib/structures/ITtscEvidenceGraphTypeScriptReference.d.ts +18 -6
- package/lib/typings/TtscEvidenceGraphTypeScriptSymbol.d.ts +11 -12
- package/native/config.go +12 -35
- package/native/declaration.go +50 -1
- package/native/default_exports.go +62 -0
- package/native/export_resolution.go +275 -0
- package/native/exports.go +62 -144
- package/native/file_link.go +370 -0
- package/native/file_link_diagnostics.go +160 -0
- package/native/fingerprint.go +33 -4
- package/native/graph.go +38 -8
- package/native/imports.go +21 -8
- package/native/inputs.go +13 -4
- package/native/loader.go +170 -23
- package/native/model.go +10 -3
- package/native/rooted_typescript.go +116 -0
- package/native/typescript.go +30 -11
- package/native/typescript_cache.go +5 -4
- package/package.json +5 -5
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:
|
|
72
|
-
Local:
|
|
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:
|
|
85
|
-
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
|
|
202
|
-
Address
|
|
203
|
-
|
|
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
|
|
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
|
|
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:
|
|
407
|
-
Address:
|
|
408
|
-
|
|
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
|
-
|
|
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
|
-
|
|
523
|
-
if len(identity)
|
|
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
|
-
|
|
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
|
+
}
|