@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.
- package/lib/structures/ITtscEvidenceGraphClaimBase.d.ts +7 -0
- package/lib/structures/ITtscEvidenceGraphReferenceBase.d.ts +7 -0
- package/lib/structures/ITtscEvidenceGraphTypeScriptReference.d.ts +18 -6
- package/lib/typings/TtscEvidenceGraphTypeScriptSymbol.d.ts +11 -12
- package/native/config.go +43 -47
- 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 +119 -79
- package/native/imports.go +21 -8
- package/native/inputs.go +13 -4
- package/native/loader.go +170 -23
- package/native/markdown.go +17 -11
- package/native/model.go +16 -4
- package/native/prisma.go +33 -26
- package/native/prisma_cache.go +16 -4
- package/native/rooted_typescript.go +116 -0
- package/native/severity.go +156 -0
- package/native/swagger.go +23 -16
- package/native/typescript.go +33 -14
- package/native/typescript_cache.go +5 -4
- package/native/unreadable.go +5 -6
- package/package.json +6 -6
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
package evidence
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"sort"
|
|
5
|
+
|
|
6
|
+
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
// Diagnostic inspection never creates an obligation. It distinguishes a
|
|
10
|
+
// declaration the collector intentionally withheld from a misspelled address.
|
|
11
|
+
func diagnoseFileLinkTarget(loader *typeScriptLoader, module string, segments []string, where string) string {
|
|
12
|
+
inventory := loader.inventory(module)
|
|
13
|
+
if inventory == nil {
|
|
14
|
+
return "Unreadable TypeScript evidence target" + where + ": " + loader.failure(module) + ". Restore the configured source file."
|
|
15
|
+
}
|
|
16
|
+
resolver := newTypeScriptExportResolver(loader, []string{module})
|
|
17
|
+
if len(resolver.lookup(module, segments, false)) != 0 {
|
|
18
|
+
return "Unselected TypeScript evidence target" + where + ": the declaration exists but this reference's files or symbol selection does not admit it. Select its symbol kind or cite a selected declaration."
|
|
19
|
+
}
|
|
20
|
+
exports := resolver.accessor(module, segments, nil, false, false)
|
|
21
|
+
sort.SliceStable(exports, func(left, right int) bool { return len(exports[left].Address) > len(exports[right].Address) })
|
|
22
|
+
for _, exported := range exports {
|
|
23
|
+
if exported.Local == "" {
|
|
24
|
+
namespace := formatFileAccessor(exported.Address)
|
|
25
|
+
if len(exported.Address) == len(segments) {
|
|
26
|
+
return "Unsupported TypeScript evidence target" + where + ": '" + namespace + "' is a module namespace, not a declared evidence unit. Cite a supported declaration inside it."
|
|
27
|
+
}
|
|
28
|
+
return "Missing TypeScript evidence export" + where + ": the module namespace '" + namespace + "' exports no declaration named '" + segments[len(exported.Address)] + "'. Correct the name inside that namespace."
|
|
29
|
+
}
|
|
30
|
+
if len(exported.Address) > len(segments) {
|
|
31
|
+
continue
|
|
32
|
+
}
|
|
33
|
+
if encodeTypeScriptIdentity(exported.Address) != encodeTypeScriptIdentity(segments[:len(exported.Address)]) {
|
|
34
|
+
continue
|
|
35
|
+
}
|
|
36
|
+
source := loader.inventory(exported.Path)
|
|
37
|
+
if source == nil || source.Source == nil {
|
|
38
|
+
continue
|
|
39
|
+
}
|
|
40
|
+
local := exported.Local
|
|
41
|
+
// Inventories use public local aliases, while the AST uses bindings.
|
|
42
|
+
for _, entry := range source.Exports {
|
|
43
|
+
if entry.Specifier == "" && entry.Public == local && entry.Local != "" {
|
|
44
|
+
local = entry.Local
|
|
45
|
+
break
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
tail := append([]string{local}, segments[len(exported.Address):]...)
|
|
49
|
+
if exported.TypeOnly {
|
|
50
|
+
identity := append([]string{exported.Local}, segments[len(exported.Address):]...)
|
|
51
|
+
for _, unit := range source.Units {
|
|
52
|
+
if unit.ValueSpace && !unit.TypeSpace && encodeTypeScriptIdentity(unit.Identity) == encodeTypeScriptIdentity(identity) {
|
|
53
|
+
return "Type-only TypeScript evidence target" + where + ": this export exposes no value through which the member can be addressed. Use a public value export or cite its type."
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if cause := inspectUnavailableDeclaration(source.Source.Statements, tail); cause != "" {
|
|
58
|
+
return "Unsupported TypeScript evidence target" + where + ": " + cause + ". Cite a supported public declaration."
|
|
59
|
+
}
|
|
60
|
+
if len(segments) > len(exported.Address) {
|
|
61
|
+
return "Missing TypeScript evidence member" + where + ": the exported declaration has no addressable member '" + formatFileAccessor(segments[len(exported.Address):]) + "'. Correct the member name and use '.prototype' for an instance member."
|
|
62
|
+
}
|
|
63
|
+
return "Unsupported TypeScript evidence declaration" + where + ": the export is not a supported type, function, or property unit. Cite a supported declaration."
|
|
64
|
+
}
|
|
65
|
+
if inventory.Source != nil && inventory.Source.Statements != nil {
|
|
66
|
+
for _, statement := range inventory.Source.Statements.Nodes {
|
|
67
|
+
if statement == nil {
|
|
68
|
+
continue
|
|
69
|
+
}
|
|
70
|
+
for _, declared := range topLevelDeclaredNames(statement) {
|
|
71
|
+
if declared.Name == segments[0] {
|
|
72
|
+
return "Unexported TypeScript evidence declaration" + where + ": '" + segments[0] + "' is a local binding, not a public export of this module. Use its public alias or default export."
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if cause := inspectUnavailableDeclaration(inventory.Source.Statements, segments); cause != "" {
|
|
78
|
+
return "Unsupported TypeScript evidence target" + where + ": " + cause + ". Export a supported declaration or correct the target."
|
|
79
|
+
}
|
|
80
|
+
return "Missing TypeScript evidence export" + where + ": the module exports no declaration named '" + segments[0] + "'. Use its public export name or correct the file."
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
func inspectUnavailableDeclaration(statements *shimast.NodeList, segments []string) string {
|
|
84
|
+
if statements == nil || len(segments) == 0 {
|
|
85
|
+
return ""
|
|
86
|
+
}
|
|
87
|
+
for _, statement := range statements.Nodes {
|
|
88
|
+
if statement == nil {
|
|
89
|
+
continue
|
|
90
|
+
}
|
|
91
|
+
if typeScriptDeclarationName(statement) != segments[0] {
|
|
92
|
+
continue
|
|
93
|
+
}
|
|
94
|
+
if len(segments) == 1 {
|
|
95
|
+
if !isSyntacticallyExported(statement) {
|
|
96
|
+
return "the declaration is not exported"
|
|
97
|
+
}
|
|
98
|
+
return "the declaration form is not supported by the evidence unit model"
|
|
99
|
+
}
|
|
100
|
+
tail := segments[1:]
|
|
101
|
+
var members *shimast.NodeList
|
|
102
|
+
class := statement.Kind == shimast.KindClassDeclaration
|
|
103
|
+
switch statement.Kind {
|
|
104
|
+
case shimast.KindClassDeclaration:
|
|
105
|
+
members = statement.AsClassDeclaration().Members
|
|
106
|
+
case shimast.KindInterfaceDeclaration:
|
|
107
|
+
members = statement.AsInterfaceDeclaration().Members
|
|
108
|
+
case shimast.KindTypeAliasDeclaration:
|
|
109
|
+
node := statement.AsTypeAliasDeclaration().Type
|
|
110
|
+
if node != nil && node.Kind == shimast.KindTypeLiteral {
|
|
111
|
+
members = node.AsTypeLiteralNode().Members
|
|
112
|
+
}
|
|
113
|
+
case shimast.KindModuleDeclaration:
|
|
114
|
+
body := statement.AsModuleDeclaration().Body
|
|
115
|
+
if body != nil && body.Kind == shimast.KindModuleBlock {
|
|
116
|
+
return inspectUnavailableDeclaration(body.AsModuleBlock().Statements, tail)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
instance := class && len(tail) > 1 && tail[0] == "prototype"
|
|
120
|
+
if instance {
|
|
121
|
+
tail = tail[1:]
|
|
122
|
+
}
|
|
123
|
+
if members == nil || len(tail) != 1 {
|
|
124
|
+
continue
|
|
125
|
+
}
|
|
126
|
+
for _, member := range members.Nodes {
|
|
127
|
+
if member != nil && member.Kind == shimast.KindConstructor && instance {
|
|
128
|
+
for _, parameter := range member.Parameters() {
|
|
129
|
+
if parameter != nil && isParameterProperty(parameter) && declarationName(parameter.Name()) == tail[0] && !isPublicClassMember(parameter) {
|
|
130
|
+
return "the member is private or protected"
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if member == nil || member.Name() == nil {
|
|
135
|
+
continue
|
|
136
|
+
}
|
|
137
|
+
name := member.Name()
|
|
138
|
+
if name.Kind != shimast.KindIdentifier && name.Kind != shimast.KindPrivateIdentifier && name.Kind != shimast.KindStringLiteral && name.Kind != shimast.KindNumericLiteral {
|
|
139
|
+
continue
|
|
140
|
+
}
|
|
141
|
+
if name.Text() != tail[0] {
|
|
142
|
+
continue
|
|
143
|
+
}
|
|
144
|
+
if class {
|
|
145
|
+
static := shimast.GetCombinedModifierFlags(member)&shimast.ModifierFlagsStatic != 0
|
|
146
|
+
if static == instance {
|
|
147
|
+
continue
|
|
148
|
+
}
|
|
149
|
+
if !isPublicClassMember(member) || name.Kind == shimast.KindPrivateIdentifier {
|
|
150
|
+
return "the member is private or protected"
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if member.Kind == shimast.KindGetAccessor || member.Kind == shimast.KindSetAccessor || member.ModifierFlags()&shimast.ModifierFlagsAccessor != 0 {
|
|
154
|
+
return "accessors are not evidence units"
|
|
155
|
+
}
|
|
156
|
+
return "the member is not exposed as a supported public evidence unit"
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return ""
|
|
160
|
+
}
|
package/native/fingerprint.go
CHANGED
|
@@ -161,12 +161,12 @@ func (index *scopeIndex) compute(rootID string) string {
|
|
|
161
161
|
if len(scope) == 0 {
|
|
162
162
|
return ""
|
|
163
163
|
}
|
|
164
|
-
// Ordered by
|
|
164
|
+
// Ordered by identity rather than by walk order, so the value does not depend
|
|
165
165
|
// on which file the loader happened to read first, and by symbol as well
|
|
166
166
|
// because two same-titled Markdown headings answer to one anchor.
|
|
167
167
|
sort.Slice(scope, func(left int, right int) bool {
|
|
168
|
-
if scope[left]
|
|
169
|
-
return scope[left]
|
|
168
|
+
if fingerprintIdentity(scope[left]) != fingerprintIdentity(scope[right]) {
|
|
169
|
+
return fingerprintIdentity(scope[left]) < fingerprintIdentity(scope[right])
|
|
170
170
|
}
|
|
171
171
|
if scope[left].Symbol != scope[right].Symbol {
|
|
172
172
|
return scope[left].Symbol < scope[right].Symbol
|
|
@@ -178,7 +178,7 @@ func (index *scopeIndex) compute(rootID string) string {
|
|
|
178
178
|
// NUL separates the fields because a target may contain any printable
|
|
179
179
|
// character, and a joined pair that can be re-split ambiguously lets two
|
|
180
180
|
// different scopes compose one digest.
|
|
181
|
-
composite.Write([]byte(unit
|
|
181
|
+
composite.Write([]byte(fingerprintIdentity(unit)))
|
|
182
182
|
composite.Write([]byte{0})
|
|
183
183
|
composite.Write([]byte(unit.Symbol))
|
|
184
184
|
composite.Write([]byte{0})
|
|
@@ -188,6 +188,35 @@ func (index *scopeIndex) compute(rootID string) string {
|
|
|
188
188
|
return presentedFingerprint(hex.EncodeToString(composite.Sum(nil)))
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
+
func fingerprintIdentity(unit *evidenceUnit) string {
|
|
192
|
+
if unit.Type == artifactTypeScript {
|
|
193
|
+
return unit.ID
|
|
194
|
+
}
|
|
195
|
+
return unit.Target
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// A code review fingerprints the declaring inventory, before any public alias
|
|
199
|
+
// or type-only edge projects its surface. Selection controls permission and
|
|
200
|
+
// coverage; it cannot make one citation require multiple content fingerprints.
|
|
201
|
+
func referenceReviewScopes(reference referenceState, loader *typeScriptLoader) *scopeIndex {
|
|
202
|
+
units := append(append([]*evidenceUnit{}, reference.Population...), reference.Hidden...)
|
|
203
|
+
if reference.Spec.Type != artifactTypeScript {
|
|
204
|
+
return newScopeIndex(units)
|
|
205
|
+
}
|
|
206
|
+
seen := map[string]bool{}
|
|
207
|
+
canonical := []*evidenceUnit{}
|
|
208
|
+
for _, unit := range units {
|
|
209
|
+
if seen[unit.Path] {
|
|
210
|
+
continue
|
|
211
|
+
}
|
|
212
|
+
seen[unit.Path] = true
|
|
213
|
+
if inventory := loader.inventory(unit.Path); inventory != nil {
|
|
214
|
+
canonical = append(canonical, inventory.Units...)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return newScopeIndex(canonical)
|
|
218
|
+
}
|
|
219
|
+
|
|
191
220
|
// scopeContribution is what one unit adds to its scope's composite.
|
|
192
221
|
//
|
|
193
222
|
// A withdrawn unit contributes the tag that withdrew it. That is what makes a
|