@ttsc/evidence 0.25.0 → 0.26.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -6
- package/lib/index.d.ts +7 -2
- package/lib/index.js +7 -2
- package/lib/internal/canonicalDigest.d.ts +1 -0
- package/lib/internal/canonicalDigest.js +97 -0
- package/lib/internal/loadPrismaModels.js +9 -2
- package/lib/internal/loadSwaggerOperations.js +96 -5
- package/lib/structures/ITtscEvidenceGraphReferenceBase.d.ts +48 -0
- package/lib/structures/ITtscEvidenceGraphTypeScriptClaim.d.ts +3 -3
- package/lib/structures/ITtscEvidenceGraphTypeScriptReference.d.ts +4 -4
- package/lib/typings/ITtscLintContributorRules.d.ts +23 -0
- package/lib/typings/TtscEvidenceGraphTypeScriptSymbol.d.ts +64 -29
- package/native/comments.go +162 -0
- package/native/config.go +2 -0
- package/native/declaration.go +213 -0
- package/native/documented.go +12 -12
- package/native/exports.go +64 -7
- package/native/fingerprint.go +351 -0
- package/native/graph.go +252 -16
- package/native/markdown.go +263 -0
- package/native/model.go +179 -1
- package/native/prisma.go +59 -3
- package/native/prisma_cache.go +7 -0
- package/native/review.go +355 -0
- package/native/swagger.go +8 -0
- package/native/typescript.go +697 -64
- package/native/unreadable.go +155 -0
- package/package.json +3 -3
package/native/exports.go
CHANGED
|
@@ -25,6 +25,16 @@ type moduleExport struct {
|
|
|
25
25
|
// Namespace marks `export * as ns from`, which nests the target's whole
|
|
26
26
|
// surface one segment deeper instead of flattening it.
|
|
27
27
|
Namespace bool
|
|
28
|
+
// TypeOnly marks an export that carries only the type side of what it names,
|
|
29
|
+
// written either as `export type { X }` or as `export { type X }`.
|
|
30
|
+
//
|
|
31
|
+
// A local export list already reaches the collector's own projection guard,
|
|
32
|
+
// so this field exists for the half that cannot: a re-export naming another
|
|
33
|
+
// module. Without it the mark stopped at the module boundary, and a barrel
|
|
34
|
+
// saying `export type { Sale } from "./sale.js"` published every class member
|
|
35
|
+
// the declaring file holds, which is the opposite of both what the author
|
|
36
|
+
// wrote and what the same intent written locally does.
|
|
37
|
+
TypeOnly bool
|
|
28
38
|
}
|
|
29
39
|
|
|
30
40
|
// collectModuleExports reads a file's public surface as importers see it.
|
|
@@ -94,7 +104,12 @@ func exportDeclarationEntries(
|
|
|
94
104
|
return nil
|
|
95
105
|
}
|
|
96
106
|
if declaration.ExportClause == nil {
|
|
97
|
-
|
|
107
|
+
// `export * from` and `export type * from`. The star form has no clause to
|
|
108
|
+
// carry a per-name mark, so the declaration's own is the whole answer.
|
|
109
|
+
return []moduleExport{{
|
|
110
|
+
Specifier: specifier,
|
|
111
|
+
TypeOnly: declaration.IsTypeOnly,
|
|
112
|
+
}}
|
|
98
113
|
}
|
|
99
114
|
switch declaration.ExportClause.Kind {
|
|
100
115
|
case shimast.KindNamespaceExport:
|
|
@@ -106,6 +121,7 @@ func exportDeclarationEntries(
|
|
|
106
121
|
Public: name,
|
|
107
122
|
Specifier: specifier,
|
|
108
123
|
Namespace: true,
|
|
124
|
+
TypeOnly: declaration.IsTypeOnly,
|
|
109
125
|
}}
|
|
110
126
|
case shimast.KindNamedExports:
|
|
111
127
|
named := declaration.ExportClause.AsNamedExports()
|
|
@@ -133,6 +149,10 @@ func exportDeclarationEntries(
|
|
|
133
149
|
Public: public,
|
|
134
150
|
Specifier: specifier,
|
|
135
151
|
Imported: imported,
|
|
152
|
+
// Either spelling marks it. The declaration carries the mark for
|
|
153
|
+
// `export type { A, B } from`, the specifier for
|
|
154
|
+
// `export { type A, B } from`, and the second is per name.
|
|
155
|
+
TypeOnly: declaration.IsTypeOnly || exportSpecifier.IsTypeOnly,
|
|
136
156
|
})
|
|
137
157
|
}
|
|
138
158
|
return entries
|
|
@@ -192,6 +212,11 @@ type reachedSymbol struct {
|
|
|
192
212
|
// the name it exposes rather than the binding it wrote. `export { a as b }`
|
|
193
213
|
// declares one unit called `b`, so matching on `a` would find nothing.
|
|
194
214
|
Local string
|
|
215
|
+
// TypeOnly is true when some edge on the path from the entry to this symbol
|
|
216
|
+
// carried only its type side. It travels rather than being read at the end,
|
|
217
|
+
// because a barrel re-exported type-only withholds value-space from
|
|
218
|
+
// everything below it however many hops away that is.
|
|
219
|
+
TypeOnly bool
|
|
195
220
|
}
|
|
196
221
|
|
|
197
222
|
// traverseEntryExports walks a module's export graph and reports every public
|
|
@@ -206,6 +231,7 @@ func traverseEntryExports(
|
|
|
206
231
|
entry string,
|
|
207
232
|
prefix []string,
|
|
208
233
|
visited map[string]bool,
|
|
234
|
+
typeOnly bool,
|
|
209
235
|
) []reachedSymbol {
|
|
210
236
|
if visited[entry] {
|
|
211
237
|
return nil
|
|
@@ -228,12 +254,31 @@ func traverseEntryExports(
|
|
|
228
254
|
return surface
|
|
229
255
|
}
|
|
230
256
|
surface := map[string]reachedSymbol{}
|
|
231
|
-
for _, nested := range traverseEntryExports(loader, target, nil, visited) {
|
|
257
|
+
for _, nested := range traverseEntryExports(loader, target, nil, visited, false) {
|
|
232
258
|
if len(nested.Address) != 1 {
|
|
233
259
|
continue
|
|
234
260
|
}
|
|
235
|
-
|
|
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:
|
|
236
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
|
|
237
282
|
}
|
|
238
283
|
}
|
|
239
284
|
surfaces[target] = surface
|
|
@@ -245,9 +290,10 @@ func traverseEntryExports(
|
|
|
245
290
|
// exposes it as. `export { local as renamed }` is one unit named
|
|
246
291
|
// `renamed`, so the local binding never identifies it.
|
|
247
292
|
reached = append(reached, reachedSymbol{
|
|
248
|
-
Address:
|
|
249
|
-
Path:
|
|
250
|
-
Local:
|
|
293
|
+
Address: append(append([]string{}, prefix...), export.Public),
|
|
294
|
+
Path: entry,
|
|
295
|
+
Local: export.Public,
|
|
296
|
+
TypeOnly: typeOnly,
|
|
251
297
|
})
|
|
252
298
|
continue
|
|
253
299
|
}
|
|
@@ -262,6 +308,7 @@ func traverseEntryExports(
|
|
|
262
308
|
target,
|
|
263
309
|
append(append([]string{}, prefix...), export.Public),
|
|
264
310
|
visited,
|
|
311
|
+
typeOnly || export.TypeOnly,
|
|
265
312
|
)...)
|
|
266
313
|
case export.Imported == "":
|
|
267
314
|
reached = append(reached, traverseEntryExports(
|
|
@@ -269,6 +316,7 @@ func traverseEntryExports(
|
|
|
269
316
|
target,
|
|
270
317
|
prefix,
|
|
271
318
|
visited,
|
|
319
|
+
typeOnly || export.TypeOnly,
|
|
272
320
|
)...)
|
|
273
321
|
default:
|
|
274
322
|
nested, found := surfaceOf(target)[export.Imported]
|
|
@@ -279,6 +327,9 @@ func traverseEntryExports(
|
|
|
279
327
|
Address: append(append([]string{}, prefix...), export.Public),
|
|
280
328
|
Path: nested.Path,
|
|
281
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,
|
|
282
333
|
})
|
|
283
334
|
}
|
|
284
335
|
}
|
|
@@ -312,7 +363,7 @@ func materializeEntryUnits(
|
|
|
312
363
|
published := []publishedAddress{}
|
|
313
364
|
candidates := newOwnedUnitIndex(loader)
|
|
314
365
|
for _, entry := range entries {
|
|
315
|
-
for _, symbol := range traverseEntryExports(loader, entry, nil, map[string]bool{}) {
|
|
366
|
+
for _, symbol := range traverseEntryExports(loader, entry, nil, map[string]bool{}, false) {
|
|
316
367
|
if symbol.Local == "" {
|
|
317
368
|
continue
|
|
318
369
|
}
|
|
@@ -321,6 +372,12 @@ func materializeEntryUnits(
|
|
|
321
372
|
if !owned {
|
|
322
373
|
continue
|
|
323
374
|
}
|
|
375
|
+
// A type-only edge exposes no value, so nothing reached only through
|
|
376
|
+
// one comes with it. The name itself still arrives, because a type is
|
|
377
|
+
// exactly what such an export carries.
|
|
378
|
+
if symbol.TypeOnly && unit.ValueSpace && !unit.TypeSpace {
|
|
379
|
+
continue
|
|
380
|
+
}
|
|
324
381
|
address := append(append([]string{}, symbol.Address...), suffix...)
|
|
325
382
|
target := strings.Join(address, ".")
|
|
326
383
|
current := byID[unit.ID]
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
package evidence
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"crypto/sha256"
|
|
5
|
+
"encoding/hex"
|
|
6
|
+
"sort"
|
|
7
|
+
"strings"
|
|
8
|
+
|
|
9
|
+
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
// normalizeFingerprintText puts content into the one form every platform agrees
|
|
13
|
+
// on before it is hashed.
|
|
14
|
+
//
|
|
15
|
+
// Line endings collapse and trailing whitespace per line is dropped, because a
|
|
16
|
+
// consumer's `core.autocrlf` and `.gitattributes` are outside this repository's
|
|
17
|
+
// control. A digest over raw bytes would expire every review on a CRLF checkout
|
|
18
|
+
// and none on an LF checkout of the same commit, which is a stale report whose
|
|
19
|
+
// repair no author can perform. A reformat that changes no content must not
|
|
20
|
+
// expire a review either, and leading whitespace is kept because in Markdown and
|
|
21
|
+
// TypeScript alike it can carry structure.
|
|
22
|
+
func normalizeFingerprintText(text string) string {
|
|
23
|
+
text = strings.ReplaceAll(text, "\r\n", "\n")
|
|
24
|
+
text = strings.ReplaceAll(text, "\r", "\n")
|
|
25
|
+
lines := strings.Split(text, "\n")
|
|
26
|
+
for index, line := range lines {
|
|
27
|
+
lines[index] = strings.TrimRight(line, " \t")
|
|
28
|
+
}
|
|
29
|
+
// Trailing blank lines say nothing about content and appear or vanish with an
|
|
30
|
+
// editor's final-newline setting.
|
|
31
|
+
for len(lines) != 0 && lines[len(lines)-1] == "" {
|
|
32
|
+
lines = lines[:len(lines)-1]
|
|
33
|
+
}
|
|
34
|
+
// Leading blank lines go for a sharper reason. A TypeScript node's position is
|
|
35
|
+
// its *full* start, meaning the end of the previous token, so an undocumented
|
|
36
|
+
// declaration's span begins with whatever blank lines and `//` comments sit
|
|
37
|
+
// above it. Keeping them made inserting one blank line elsewhere in the file
|
|
38
|
+
// expire a review, and inconsistently: the same edit above a *documented*
|
|
39
|
+
// declaration is neutral, because the whole leading run through `*/` is
|
|
40
|
+
// excluded as a tag position. Dropping them makes both cases agree and honors
|
|
41
|
+
// the rule that a reformat which changes no content expires nothing.
|
|
42
|
+
return strings.Join(lines, "\n")
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// withoutLeadingTrivia drops the leading run a TypeScript span carries but does
|
|
46
|
+
// not own.
|
|
47
|
+
//
|
|
48
|
+
// A node's position is its full start, so an undocumented declaration's text
|
|
49
|
+
// begins at the previous token and arrives with the blank lines and `//` comments
|
|
50
|
+
// above it. Those are not content of the declaration: keeping them made inserting
|
|
51
|
+
// a blank line elsewhere expire a review, and inconsistently, because above a
|
|
52
|
+
// *documented* declaration the whole leading run through the block is already
|
|
53
|
+
// excluded as a tag position.
|
|
54
|
+
//
|
|
55
|
+
// Only the leading run, and only for TypeScript. An interior comment is part of
|
|
56
|
+
// what the author wrote inside the body, and in Markdown a line opening with `//`
|
|
57
|
+
// is ordinary prose that must keep counting.
|
|
58
|
+
func withoutLeadingTrivia(text string) string {
|
|
59
|
+
lines := strings.Split(text, "\n")
|
|
60
|
+
for len(lines) != 0 {
|
|
61
|
+
first := strings.TrimSpace(lines[0])
|
|
62
|
+
if first != "" && !strings.HasPrefix(first, "//") {
|
|
63
|
+
break
|
|
64
|
+
}
|
|
65
|
+
lines = lines[1:]
|
|
66
|
+
}
|
|
67
|
+
return strings.Join(lines, "\n")
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// contentDigest is the digest of one unit's own content.
|
|
71
|
+
func contentDigest(text string) string {
|
|
72
|
+
sum := sha256.Sum256([]byte(normalizeFingerprintText(text)))
|
|
73
|
+
return hex.EncodeToString(sum[:])
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// scopeIndex answers what a citation of one unit has to name, over one
|
|
77
|
+
// reference's whole materialized population.
|
|
78
|
+
//
|
|
79
|
+
// The scope of a citation is the unit plus every structural descendant, and it is
|
|
80
|
+
// deliberately **not** the reference's covered set. `UnitsByScope` and `Units` are
|
|
81
|
+
// both narrowed by that reference's `symbol` selector, while one `@evidence` tag
|
|
82
|
+
// may acknowledge several references and carries exactly one fingerprint token.
|
|
83
|
+
// Composing from a narrowed set made the value a function of the reference: a
|
|
84
|
+
// selector that skipped the descendants left them out of the digest, so the
|
|
85
|
+
// review never expired, and two references over one scope under different
|
|
86
|
+
// selectors demanded two different values from that single token with no value
|
|
87
|
+
// able to satisfy both. The population is passed instead, which is why this type
|
|
88
|
+
// exists rather than a function over `Units`.
|
|
89
|
+
//
|
|
90
|
+
// So the fingerprint is a property of the address a citation names rather than of
|
|
91
|
+
// what the citation discharges. A reference confining acknowledgement to the
|
|
92
|
+
// named unit does not narrow this: the citation still names the scope, and
|
|
93
|
+
// re-reviewing when the subtree moves is conservative rather than wrong.
|
|
94
|
+
//
|
|
95
|
+
// Descendants are found through the stored parent identity, never through a
|
|
96
|
+
// shared address prefix. A literal dot may sit inside a TypeScript name, so
|
|
97
|
+
// treating a common prefix as ownership would make an unrelated same-name
|
|
98
|
+
// declaration an ancestor.
|
|
99
|
+
//
|
|
100
|
+
// A withdrawn descendant contributes its identity, so adding `@internal` to a
|
|
101
|
+
// member moves the composite: the tag's own block is excluded as a tag position,
|
|
102
|
+
// which would otherwise make a withdrawal invisible.
|
|
103
|
+
//
|
|
104
|
+
// Its content is **not** separable from its ancestor's, and an earlier version of
|
|
105
|
+
// this comment claimed otherwise. A TypeScript unit's digest is its whole
|
|
106
|
+
// declaration text, so a withdrawn member's body is already inside the enclosing
|
|
107
|
+
// type's digest and no substitution here can take it out. Churn behind
|
|
108
|
+
// `@internal` therefore does expire a review of the enclosing type. That is
|
|
109
|
+
// conservative noise rather than a wrong answer, and the honest trade: removing it
|
|
110
|
+
// means excluding every child unit's span from its ancestor's text, which is a
|
|
111
|
+
// second exclusion mechanism layered on the tag-position one.
|
|
112
|
+
//
|
|
113
|
+
// The index is built once per reference and the answers are memoized, because
|
|
114
|
+
// building it per citation is quadratic and the cost is not theoretical: it spans
|
|
115
|
+
// the whole population, so a claim with a citation per selected unit would walk
|
|
116
|
+
// every unit once per citation. On a requirements set of a few thousand headings
|
|
117
|
+
// that is millions of map inserts per Program cycle, paid again on every watch
|
|
118
|
+
// rebuild, for a value that is identical each time.
|
|
119
|
+
type scopeIndex struct {
|
|
120
|
+
byID map[string]*evidenceUnit
|
|
121
|
+
children map[string][]*evidenceUnit
|
|
122
|
+
cache map[string]string
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
func newScopeIndex(units []*evidenceUnit) *scopeIndex {
|
|
126
|
+
index := &scopeIndex{
|
|
127
|
+
byID: make(map[string]*evidenceUnit, len(units)),
|
|
128
|
+
children: map[string][]*evidenceUnit{},
|
|
129
|
+
cache: map[string]string{},
|
|
130
|
+
}
|
|
131
|
+
for _, unit := range units {
|
|
132
|
+
if unit == nil || index.byID[unit.ID] != nil {
|
|
133
|
+
continue
|
|
134
|
+
}
|
|
135
|
+
index.byID[unit.ID] = unit
|
|
136
|
+
if unit.ParentID != "" {
|
|
137
|
+
index.children[unit.ParentID] = append(index.children[unit.ParentID], unit)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return index
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// fingerprint answers for one cited scope, remembering the answer.
|
|
144
|
+
//
|
|
145
|
+
// Several citations of one scope are ordinary — a reference may be acknowledged
|
|
146
|
+
// by many hosts — so the memo matters as much as the shared index.
|
|
147
|
+
func (index *scopeIndex) fingerprint(rootID string) string {
|
|
148
|
+
if index == nil || rootID == "" {
|
|
149
|
+
return ""
|
|
150
|
+
}
|
|
151
|
+
if remembered, seen := index.cache[rootID]; seen {
|
|
152
|
+
return remembered
|
|
153
|
+
}
|
|
154
|
+
computed := index.compute(rootID)
|
|
155
|
+
index.cache[rootID] = computed
|
|
156
|
+
return computed
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
func (index *scopeIndex) compute(rootID string) string {
|
|
160
|
+
scope := index.collect(rootID)
|
|
161
|
+
if len(scope) == 0 {
|
|
162
|
+
return ""
|
|
163
|
+
}
|
|
164
|
+
// Ordered by address rather than by walk order, so the value does not depend
|
|
165
|
+
// on which file the loader happened to read first, and by symbol as well
|
|
166
|
+
// because two same-titled Markdown headings answer to one anchor.
|
|
167
|
+
sort.Slice(scope, func(left int, right int) bool {
|
|
168
|
+
if scope[left].Target != scope[right].Target {
|
|
169
|
+
return scope[left].Target < scope[right].Target
|
|
170
|
+
}
|
|
171
|
+
if scope[left].Symbol != scope[right].Symbol {
|
|
172
|
+
return scope[left].Symbol < scope[right].Symbol
|
|
173
|
+
}
|
|
174
|
+
return scope[left].Digest < scope[right].Digest
|
|
175
|
+
})
|
|
176
|
+
composite := sha256.New()
|
|
177
|
+
for _, unit := range scope {
|
|
178
|
+
// NUL separates the fields because a target may contain any printable
|
|
179
|
+
// character, and a joined pair that can be re-split ambiguously lets two
|
|
180
|
+
// different scopes compose one digest.
|
|
181
|
+
composite.Write([]byte(unit.Target))
|
|
182
|
+
composite.Write([]byte{0})
|
|
183
|
+
composite.Write([]byte(unit.Symbol))
|
|
184
|
+
composite.Write([]byte{0})
|
|
185
|
+
composite.Write([]byte(scopeContribution(unit)))
|
|
186
|
+
composite.Write([]byte{0})
|
|
187
|
+
}
|
|
188
|
+
return presentedFingerprint(hex.EncodeToString(composite.Sum(nil)))
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// scopeContribution is what one unit adds to its scope's composite.
|
|
192
|
+
//
|
|
193
|
+
// A withdrawn unit contributes the tag that withdrew it. That is what makes a
|
|
194
|
+
// withdrawal visible at all, since the tag lives in a documentation block and
|
|
195
|
+
// every such block is excluded as a tag position, so the enclosing declaration's
|
|
196
|
+
// text is unchanged by adding one.
|
|
197
|
+
//
|
|
198
|
+
// It does not make the withdrawn member's content invisible. For TypeScript that
|
|
199
|
+
// content sits inside its ancestor's declaration text, which is that ancestor's
|
|
200
|
+
// digest; see `evidenceUnit.Digest`.
|
|
201
|
+
func scopeContribution(unit *evidenceUnit) string {
|
|
202
|
+
if unit.Hidden != "" {
|
|
203
|
+
return "\x00withdrawn:" + unit.Hidden
|
|
204
|
+
}
|
|
205
|
+
return unit.Digest
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// collect gathers one unit and every structural descendant.
|
|
209
|
+
func (index *scopeIndex) collect(rootID string) []*evidenceUnit {
|
|
210
|
+
children := index.children
|
|
211
|
+
root := index.byID[rootID]
|
|
212
|
+
if root == nil {
|
|
213
|
+
return nil
|
|
214
|
+
}
|
|
215
|
+
scope := []*evidenceUnit{}
|
|
216
|
+
// Iterative rather than recursive, and guarded by a visited set: parent
|
|
217
|
+
// identities are stored while materializing, and a cycle there would
|
|
218
|
+
// otherwise hang the compiler instead of reporting anything.
|
|
219
|
+
visited := map[string]bool{}
|
|
220
|
+
queue := []*evidenceUnit{root}
|
|
221
|
+
for len(queue) != 0 {
|
|
222
|
+
current := queue[0]
|
|
223
|
+
queue = queue[1:]
|
|
224
|
+
if current == nil || visited[current.ID] {
|
|
225
|
+
continue
|
|
226
|
+
}
|
|
227
|
+
visited[current.ID] = true
|
|
228
|
+
scope = append(scope, current)
|
|
229
|
+
queue = append(queue, children[current.ID]...)
|
|
230
|
+
}
|
|
231
|
+
return scope
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// presentedFingerprint shortens a digest to what a tag carries.
|
|
235
|
+
func presentedFingerprint(digest string) string {
|
|
236
|
+
if len(digest) <= reviewFingerprintLength {
|
|
237
|
+
return digest
|
|
238
|
+
}
|
|
239
|
+
return digest[:reviewFingerprintLength]
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// typeScriptUnitDigest hashes one identity's declarations with every
|
|
243
|
+
// documentation block removed.
|
|
244
|
+
//
|
|
245
|
+
// A JSDoc block is the only place a TypeScript citation or review can live, so
|
|
246
|
+
// removing exactly those spans is the general form of "exclude every position a
|
|
247
|
+
// tag can occupy". Removing only the block that happens to carry a tag would
|
|
248
|
+
// leave the case that breaks first: a property's own JSDoc is interior text of
|
|
249
|
+
// the interface containing it, so writing a review on one property would change
|
|
250
|
+
// the digest of every citation of the enclosing type.
|
|
251
|
+
//
|
|
252
|
+
// Blocks are collected from the whole subtree rather than from the declaration
|
|
253
|
+
// alone, for that same reason. An ordinary comment interior to the declaration
|
|
254
|
+
// stays in the digest, and that is correct: the graph reads tags only from the
|
|
255
|
+
// blocks a node reports, so no citation can hide in one.
|
|
256
|
+
//
|
|
257
|
+
// Leading text is the other case and reaches the digest in some forms only.
|
|
258
|
+
// Text outside the declaration's span was never in it, which for a variable is
|
|
259
|
+
// everything above its declarator, including the `export const` and any comment
|
|
260
|
+
// above a sibling. Text a full start swallowed is inside the span, and there
|
|
261
|
+
// `withoutLeadingTrivia` drops the blank lines and `//` comments while a
|
|
262
|
+
// documentation block is cut as a tag position. A block comment that is neither
|
|
263
|
+
// is hashed, so editing an ordinary `/* */` above an undocumented declaration
|
|
264
|
+
// expires a review of it.
|
|
265
|
+
//
|
|
266
|
+
// Every declaration of a merged identity contributes, in source order, because
|
|
267
|
+
// `interface I` beside `namespace I` is one unit whose content is both halves.
|
|
268
|
+
func typeScriptUnitDigest(
|
|
269
|
+
file *shimast.SourceFile,
|
|
270
|
+
nodes []*shimast.Node,
|
|
271
|
+
) string {
|
|
272
|
+
if file == nil || len(nodes) == 0 {
|
|
273
|
+
return ""
|
|
274
|
+
}
|
|
275
|
+
content := file.Text()
|
|
276
|
+
ordered := make([]*shimast.Node, 0, len(nodes))
|
|
277
|
+
for _, node := range nodes {
|
|
278
|
+
if node == nil || node.Pos() < 0 || node.End() > len(content) || node.Pos() >= node.End() {
|
|
279
|
+
continue
|
|
280
|
+
}
|
|
281
|
+
ordered = append(ordered, node)
|
|
282
|
+
}
|
|
283
|
+
if len(ordered) == 0 {
|
|
284
|
+
return ""
|
|
285
|
+
}
|
|
286
|
+
sort.SliceStable(ordered, func(left int, right int) bool {
|
|
287
|
+
return ordered[left].Pos() < ordered[right].Pos()
|
|
288
|
+
})
|
|
289
|
+
builder := strings.Builder{}
|
|
290
|
+
for _, node := range ordered {
|
|
291
|
+
builder.WriteString(
|
|
292
|
+
withoutLeadingTrivia(withoutDocumentationSpans(content, node, file)),
|
|
293
|
+
)
|
|
294
|
+
builder.WriteByte(0)
|
|
295
|
+
}
|
|
296
|
+
return contentDigest(builder.String())
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// withoutDocumentationSpans renders one declaration's text with its subtree's
|
|
300
|
+
// documentation blocks cut out.
|
|
301
|
+
func withoutDocumentationSpans(
|
|
302
|
+
content string,
|
|
303
|
+
node *shimast.Node,
|
|
304
|
+
file *shimast.SourceFile,
|
|
305
|
+
) string {
|
|
306
|
+
type span struct {
|
|
307
|
+
start int
|
|
308
|
+
end int
|
|
309
|
+
}
|
|
310
|
+
excluded := []span{}
|
|
311
|
+
walkTypeScriptNode(node, func(current *shimast.Node) {
|
|
312
|
+
for _, doc := range current.JSDoc(file) {
|
|
313
|
+
if doc == nil {
|
|
314
|
+
continue
|
|
315
|
+
}
|
|
316
|
+
start, end := doc.Pos(), doc.End()
|
|
317
|
+
if start < node.Pos() || end > node.End() || start >= end {
|
|
318
|
+
continue
|
|
319
|
+
}
|
|
320
|
+
excluded = append(excluded, span{start: start, end: end})
|
|
321
|
+
}
|
|
322
|
+
})
|
|
323
|
+
if len(excluded) == 0 {
|
|
324
|
+
return content[node.Pos():node.End()]
|
|
325
|
+
}
|
|
326
|
+
sort.Slice(excluded, func(left int, right int) bool {
|
|
327
|
+
if excluded[left].start != excluded[right].start {
|
|
328
|
+
return excluded[left].start < excluded[right].start
|
|
329
|
+
}
|
|
330
|
+
return excluded[left].end < excluded[right].end
|
|
331
|
+
})
|
|
332
|
+
builder := strings.Builder{}
|
|
333
|
+
cursor := node.Pos()
|
|
334
|
+
for _, current := range excluded {
|
|
335
|
+
// Overlapping and duplicate spans are expected: TypeScript cascades one
|
|
336
|
+
// leading block onto nested nodes, so several nodes report the same
|
|
337
|
+
// comment. Advancing the cursor monotonically collapses them instead of
|
|
338
|
+
// emitting text twice or slicing backwards.
|
|
339
|
+
if current.end <= cursor {
|
|
340
|
+
continue
|
|
341
|
+
}
|
|
342
|
+
if current.start > cursor {
|
|
343
|
+
builder.WriteString(content[cursor:current.start])
|
|
344
|
+
}
|
|
345
|
+
cursor = current.end
|
|
346
|
+
}
|
|
347
|
+
if cursor < node.End() {
|
|
348
|
+
builder.WriteString(content[cursor:node.End()])
|
|
349
|
+
}
|
|
350
|
+
return builder.String()
|
|
351
|
+
}
|