@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
|
@@ -7,30 +7,52 @@
|
|
|
7
7
|
* and qualified identities keep nested contracts addressable without adding a
|
|
8
8
|
* file path to every target.
|
|
9
9
|
*
|
|
10
|
-
* - `"type"` selects exported interfaces, type aliases, and namespaces.
|
|
11
|
-
*
|
|
10
|
+
* - `"type"` selects exported interfaces, type aliases, classes, and namespaces.
|
|
11
|
+
* Enums do not become type units.
|
|
12
12
|
* - `"function"` selects exported function declarations, exported `const`
|
|
13
13
|
* variables initialized with an arrow function or function expression
|
|
14
14
|
* (including parentheses and type-only expression wrappers), public instance
|
|
15
|
-
* and static methods of exported classes,
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
15
|
+
* and static methods of exported classes, method signatures of exported
|
|
16
|
+
* interfaces and object-shaped type aliases, every member of any of those
|
|
17
|
+
* three written as a callable (an arrow/function initializer or a function
|
|
18
|
+
* type spelled out), and the same callable forms exported from namespaces. An
|
|
19
|
+
* overload run is one unit. Constructors and accessors are not selected.
|
|
20
|
+
* - `"property"` selects every member declared directly by an exported class,
|
|
21
|
+
* interface, or object-shaped type alias that is not written as a callable,
|
|
22
|
+
* plus exported `const`, `let`, and `var` declarations at module or namespace
|
|
23
|
+
* scope. A `const` initialized with an arrow or function expression remains a
|
|
24
|
+
* function; every other variable, including a function-typed declaration or
|
|
25
|
+
* function-valued `let` or `var`, is a property. A member is the one place
|
|
26
|
+
* that rule reads the other way, on all three containers alike: it is a
|
|
27
|
+
* property unless it is function-valued **or** annotated with a function type
|
|
28
|
+
* written out, either of which makes it a function. The test is syntactic,
|
|
29
|
+
* because this rule reads no type checker: `charge: () => void` is a function
|
|
30
|
+
* and `charge: Handler` is a property even where `Handler` is an alias of the
|
|
31
|
+
* same type, as are a constructor type and a union. Every exported leaf in an
|
|
32
|
+
* object or array binding pattern is a property. An accessor, including an
|
|
33
|
+
* auto-accessor, is neither, and a `private` or `protected` member is not
|
|
34
|
+
* selected whichever syntax declared it. A constructor parameter carrying any
|
|
35
|
+
* property modifier declares a field and classifies exactly as the same field
|
|
36
|
+
* written in the class body would, whatever the constructor's own visibility
|
|
37
|
+
* is, so a `private` or `protected` one materializes nothing, exactly as the
|
|
38
|
+
* body form does not. The property modifiers are TypeScript's own five:
|
|
39
|
+
* `public`, `protected`, `private`, `readonly`, and `override`. The last is
|
|
40
|
+
* the one to know about, because its meaning is about the base class rather
|
|
41
|
+
* than about the field, so it does not read as a field declaration. It still
|
|
42
|
+
* declares one: on a class extending one that declares `rate`,
|
|
43
|
+
* `constructor(override rate: number)` is a public instance field. A
|
|
44
|
+
* parameter property's citation belongs on the parameter: a constructor's own
|
|
45
|
+
* block hosts nothing, because two parameter properties would leave
|
|
46
|
+
* `@evidence` no way to say which field it means.
|
|
26
47
|
*
|
|
27
48
|
* TypeScript units form containment scopes. An interface or object-shaped type
|
|
28
|
-
* alias contains
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* remain addressable as
|
|
49
|
+
* alias contains the members it declares, callables included. A class contains
|
|
50
|
+
* its selected members. A namespace contains every selected public unit nested
|
|
51
|
+
* below it, including nested namespaces. An `@evidence` target, or an
|
|
52
|
+
* `@evidenceExclude` target allowed by its reference policy, acknowledges its
|
|
53
|
+
* selected node and every selected descendant. A reference selector defines the
|
|
54
|
+
* obligation kinds while their unselected type ancestors remain addressable as
|
|
55
|
+
* aggregate scopes.
|
|
34
56
|
*
|
|
35
57
|
* Top-level identities use the public export name, and namespace members
|
|
36
58
|
* prepend their namespace, such as `Orders.create`. A namespace itself uses its
|
|
@@ -38,16 +60,29 @@
|
|
|
38
60
|
* exposed as `export { Local as Public }` therefore uses `Public`. A named
|
|
39
61
|
* default declaration keeps its declaration name; anonymous and default-only
|
|
40
62
|
* aliases have no stable target and are not selected. Members of an ambient
|
|
41
|
-
* namespace are public without their own `export` modifier. A type-only
|
|
42
|
-
* namespace
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
63
|
+
* namespace are public without their own `export` modifier. A type-only alias
|
|
64
|
+
* exposes a namespace or a class, its public type-space descendants, and
|
|
65
|
+
* everything an object-shaped type alias, or an interface no class merges with,
|
|
66
|
+
* declares, callables included, because every member of those two is
|
|
67
|
+
* type-space. What it withholds is value-space: namespace data, namespace
|
|
68
|
+
* functions, and every class member, the last because a member is addressed
|
|
69
|
+
* through the class value the alias does not expose. An interface merged with a
|
|
70
|
+
* class is withheld with it, since its members are reached through that same
|
|
71
|
+
* value. Every type-only export withholds, in every spelling and across every
|
|
72
|
+
* module boundary: an export list in the declaring file, an inline `export {
|
|
73
|
+
* type Sale }`, and a re-export in any of `export type { Sale } from`, `export
|
|
74
|
+
* { type Sale } from`, `export type * from`, and `export type * as api from`. A
|
|
75
|
+
* member of an object-shaped type alias, or of an interface no class merges
|
|
76
|
+
* with, uses `TypeName.member`, whether it carries data or is written as a
|
|
77
|
+
* callable; one declared by an interface merged with a class is an instance
|
|
78
|
+
* member of that class and uses `ClassName.prototype.member` like the body
|
|
79
|
+
* form. Static class members use `ClassName.member`; instance members use
|
|
80
|
+
* `ClassName.prototype.member`, and a constructor parameter property is an
|
|
81
|
+
* instance member addressed that way. Computed names are not selected, even
|
|
82
|
+
* when their expression is a literal. Literal names must be whitespace-free
|
|
83
|
+
* because a declaration target is one whitespace-delimited token. A dot inside
|
|
84
|
+
* a literal name is rendered unchanged; if that spelling collides with
|
|
85
|
+
* qualification, the target is ambiguous.
|
|
51
86
|
*
|
|
52
87
|
* These targets deliberately omit file paths. If selected files expose the same
|
|
53
88
|
* qualified target, a declaration using that target is ambiguous; rename or
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
package evidence
|
|
2
|
+
|
|
3
|
+
// Parser-aware comment enumeration.
|
|
4
|
+
//
|
|
5
|
+
// This is a copy of the enumeration `@ttsc/lint` performs for its own rules,
|
|
6
|
+
// carried here rather than imported from it. `@ttsc/lint` is a peer dependency
|
|
7
|
+
// with a published range, and this package ships Go **source** into a
|
|
8
|
+
// consumer's build against whichever version they installed. A helper this file
|
|
9
|
+
// took from that package would compile here and fail there for every consumer
|
|
10
|
+
// on a release that predates it, which is what a benchmark fixture proved by
|
|
11
|
+
// refusing `undefined: astutil.ForEachComment`. What crosses that boundary is
|
|
12
|
+
// the published rule API and nothing else.
|
|
13
|
+
|
|
14
|
+
import (
|
|
15
|
+
"sort"
|
|
16
|
+
|
|
17
|
+
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
18
|
+
shimscanner "github.com/microsoft/typescript-go/shim/scanner"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
type commentToken struct {
|
|
22
|
+
kind shimast.Kind
|
|
23
|
+
pos int
|
|
24
|
+
end int
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type sourceSpan struct {
|
|
28
|
+
pos int
|
|
29
|
+
end int
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// forEachCommentToken visits every real comment in `file` in source order.
|
|
33
|
+
//
|
|
34
|
+
// TypeScript's parser, rather than a context-free scanner, owns the lexical
|
|
35
|
+
// goal for regular expressions, templates, and JSX. The parsed AST retains
|
|
36
|
+
// those context-sensitive tokens as exact spans. We treat those spans as
|
|
37
|
+
// opaque and scan only the gaps between them, where slash-shaped bytes are
|
|
38
|
+
// ordinary JavaScript trivia or punctuation. This preserves the scanner's
|
|
39
|
+
// complete comment and line-terminator behavior without guessing whether a
|
|
40
|
+
// slash is division, manually tracking template braces, or special-casing JSX
|
|
41
|
+
// strings. Exact ranges are deduplicated before the callback runs because a
|
|
42
|
+
// recovery AST can expose overlapping token nodes for malformed source.
|
|
43
|
+
func forEachComment(file *shimast.SourceFile, visit func(kind shimast.Kind, pos, end int)) {
|
|
44
|
+
if file == nil || visit == nil {
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
text := file.Text()
|
|
48
|
+
opaque := parserOpaqueTokenSpans(file)
|
|
49
|
+
comments := make([]commentToken, 0)
|
|
50
|
+
seen := make(map[sourceSpan]struct{})
|
|
51
|
+
gapScanner := shimscanner.NewScanner()
|
|
52
|
+
collect := func(kind shimast.Kind, pos, end int) {
|
|
53
|
+
if pos < 0 || end <= pos || end > len(text) {
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
span := sourceSpan{pos: pos, end: end}
|
|
57
|
+
if _, ok := seen[span]; ok {
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
seen[span] = struct{}{}
|
|
61
|
+
comments = append(comments, commentToken{kind: kind, pos: pos, end: end})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
cursor := 0
|
|
65
|
+
for _, span := range opaque {
|
|
66
|
+
if cursor < span.pos {
|
|
67
|
+
scanCommentGap(gapScanner, text, cursor, span.pos, collect)
|
|
68
|
+
}
|
|
69
|
+
if span.end > cursor {
|
|
70
|
+
cursor = span.end
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if cursor < len(text) {
|
|
74
|
+
scanCommentGap(gapScanner, text, cursor, len(text), collect)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
sort.Slice(comments, func(i, j int) bool {
|
|
78
|
+
if comments[i].pos != comments[j].pos {
|
|
79
|
+
return comments[i].pos < comments[j].pos
|
|
80
|
+
}
|
|
81
|
+
return comments[i].end < comments[j].end
|
|
82
|
+
})
|
|
83
|
+
for _, comment := range comments {
|
|
84
|
+
visit(comment.kind, comment.pos, comment.end)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// parserOpaqueTokenSpans returns merged source ranges for the parser-owned
|
|
89
|
+
// tokens whose contents a context-free scanner cannot classify: strings,
|
|
90
|
+
// regex literals, template heads/middles/tails, and JSX text. Numeric literals,
|
|
91
|
+
// identifiers, keywords, and punctuation remain in the scanned gaps because
|
|
92
|
+
// they cannot contain comment-shaped source bytes; keeping them there avoids a
|
|
93
|
+
// scanner reset for every ordinary token.
|
|
94
|
+
func parserOpaqueTokenSpans(file *shimast.SourceFile) []sourceSpan {
|
|
95
|
+
spans := make([]sourceSpan, 0)
|
|
96
|
+
var walk func(*shimast.Node)
|
|
97
|
+
walk = func(node *shimast.Node) {
|
|
98
|
+
if node == nil {
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
if parserOpaqueTokenKind(node.Kind) {
|
|
102
|
+
pos := shimscanner.GetTokenPosOfNode(node, file, false /*includeJSDoc*/)
|
|
103
|
+
if pos >= 0 && node.End() > pos && node.End() <= len(file.Text()) {
|
|
104
|
+
spans = append(spans, sourceSpan{pos: pos, end: node.End()})
|
|
105
|
+
}
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
node.ForEachChild(func(child *shimast.Node) bool {
|
|
109
|
+
walk(child)
|
|
110
|
+
return false
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
walk(file.AsNode())
|
|
114
|
+
sort.Slice(spans, func(i, j int) bool {
|
|
115
|
+
if spans[i].pos != spans[j].pos {
|
|
116
|
+
return spans[i].pos < spans[j].pos
|
|
117
|
+
}
|
|
118
|
+
return spans[i].end < spans[j].end
|
|
119
|
+
})
|
|
120
|
+
if len(spans) < 2 {
|
|
121
|
+
return spans
|
|
122
|
+
}
|
|
123
|
+
merged := spans[:1]
|
|
124
|
+
for _, span := range spans[1:] {
|
|
125
|
+
last := &merged[len(merged)-1]
|
|
126
|
+
if span.pos <= last.end {
|
|
127
|
+
if span.end > last.end {
|
|
128
|
+
last.end = span.end
|
|
129
|
+
}
|
|
130
|
+
continue
|
|
131
|
+
}
|
|
132
|
+
merged = append(merged, span)
|
|
133
|
+
}
|
|
134
|
+
return merged
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
func parserOpaqueTokenKind(kind shimast.Kind) bool {
|
|
138
|
+
return kind >= shimast.KindStringLiteral && kind <= shimast.KindLastLiteralToken ||
|
|
139
|
+
kind >= shimast.KindFirstTemplateToken && kind <= shimast.KindLastTemplateToken
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// scanCommentGap scans one parser-classified non-token gap. Context-sensitive
|
|
143
|
+
// token bodies never enter this function, so every comment trivia token it
|
|
144
|
+
// returns is a real source comment. Scanning the isolated gap also preserves
|
|
145
|
+
// CRLF and Unicode line-terminator behavior without range arithmetic in a
|
|
146
|
+
// second comment parser.
|
|
147
|
+
func scanCommentGap(scanner *shimscanner.Scanner, text string, from, to int, visit func(kind shimast.Kind, pos, end int)) {
|
|
148
|
+
if scanner == nil || from < 0 || to <= from || to > len(text) {
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
scanner.SetText(text[from:to])
|
|
152
|
+
scanner.SetSkipTrivia(false)
|
|
153
|
+
for {
|
|
154
|
+
kind := scanner.Scan()
|
|
155
|
+
switch kind {
|
|
156
|
+
case shimast.KindEndOfFile:
|
|
157
|
+
return
|
|
158
|
+
case shimast.KindSingleLineCommentTrivia, shimast.KindMultiLineCommentTrivia:
|
|
159
|
+
visit(kind, from+scanner.TokenStart(), from+scanner.TokenEnd())
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
package/native/config.go
CHANGED
|
@@ -185,6 +185,7 @@ func decodeReference(
|
|
|
185
185
|
"noEvidenceExclude",
|
|
186
186
|
"uniqueEvidence",
|
|
187
187
|
"singleEvidencePerSymbol",
|
|
188
|
+
"requireReview",
|
|
188
189
|
"package",
|
|
189
190
|
"root",
|
|
190
191
|
"file",
|
|
@@ -306,6 +307,7 @@ func decodeReferencePolicy(
|
|
|
306
307
|
decodeFlag("noEvidenceExclude", &policy.NoExclude)
|
|
307
308
|
decodeFlag("uniqueEvidence", &policy.UniqueEvidence)
|
|
308
309
|
decodeFlag("singleEvidencePerSymbol", &policy.SingleEvidencePerSymbol)
|
|
310
|
+
decodeFlag("requireReview", &policy.RequireReview)
|
|
309
311
|
return policy, problems
|
|
310
312
|
}
|
|
311
313
|
|
package/native/declaration.go
CHANGED
|
@@ -102,6 +102,21 @@ func parseCommentDeclarations(
|
|
|
102
102
|
}
|
|
103
103
|
continue
|
|
104
104
|
}
|
|
105
|
+
// A review closes the declaration above it in every host, and it does so
|
|
106
|
+
// regardless of tagBoundaries. That flag answers whether *another tool's*
|
|
107
|
+
// `@tag` is a boundary, which is a property of the host's comment grammar:
|
|
108
|
+
// an HTML comment has no field syntax, so `@architecture approved this` is
|
|
109
|
+
// ordinary prose belonging to the reason above it, and a test pins that.
|
|
110
|
+
//
|
|
111
|
+
// A review is not another tool's tag. It belongs to this grammar, so it is
|
|
112
|
+
// always a boundary. Without this, an `@evidenceReview` written under an
|
|
113
|
+
// `@evidence` inside one Markdown comment was swallowed into that citation's
|
|
114
|
+
// reason: the review vanished and the reason grew a sentence its author
|
|
115
|
+
// addressed to a different question.
|
|
116
|
+
if _, _, opened := reviewLine(line); opened {
|
|
117
|
+
flush()
|
|
118
|
+
continue
|
|
119
|
+
}
|
|
105
120
|
if jsdoc && strings.HasPrefix(line, "@") {
|
|
106
121
|
flush()
|
|
107
122
|
continue
|
|
@@ -220,6 +235,204 @@ func normalizeMarkdownTarget(target string) string {
|
|
|
220
235
|
return target
|
|
221
236
|
}
|
|
222
237
|
|
|
238
|
+
// reviewMarkers open a verification statement, one per acknowledgement tag.
|
|
239
|
+
//
|
|
240
|
+
// There are two because the two acknowledgements answer opposite questions and
|
|
241
|
+
// so do their reviews. Verifying an `@evidence` means checking that this
|
|
242
|
+
// declaration does what the cited unit describes. Verifying an
|
|
243
|
+
// `@evidenceExclude` means checking that the unit genuinely does not apply here,
|
|
244
|
+
// which no amount of reading the code can establish and no coverage number can.
|
|
245
|
+
// One tag for both would leave a reader unable to tell which question was
|
|
246
|
+
// answered without finding the sibling tag first, and would let a review of the
|
|
247
|
+
// easier question discharge the harder one.
|
|
248
|
+
//
|
|
249
|
+
// Order is longest marker first, the way `declarationLine` orders its own, so a
|
|
250
|
+
// prefix never shadows a longer tag. The whitespace boundary keeps the four
|
|
251
|
+
// apart on its own — `@evidenceExcludeReview` reaching the `@evidenceExclude`
|
|
252
|
+
// arm is refused exactly as `@evidenceReview` reaching `@evidence` is — but the
|
|
253
|
+
// ordering means that guard is a second line of defense rather than the only one.
|
|
254
|
+
var reviewMarkers = []struct {
|
|
255
|
+
marker string
|
|
256
|
+
tag tagKind
|
|
257
|
+
}{
|
|
258
|
+
{marker: "@evidenceExcludeReview", tag: tagExclude},
|
|
259
|
+
{marker: "@evidenceReview", tag: tagEvidence},
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// reviewFingerprintLength is how many hex characters a fingerprint presents.
|
|
263
|
+
//
|
|
264
|
+
// Seven is a staleness detector's length, not a security boundary's. A
|
|
265
|
+
// collision costs one review that failed to expire, while the tag stays short
|
|
266
|
+
// enough that an author reads the target beside it instead of scrolling past a
|
|
267
|
+
// full digest.
|
|
268
|
+
const reviewFingerprintLength = 7
|
|
269
|
+
|
|
270
|
+
// parsedReview is one verification statement read out of a comment.
|
|
271
|
+
//
|
|
272
|
+
// It is deliberately not a `parsedDeclaration`, and the separation is the whole
|
|
273
|
+
// safety property rather than a matter of taste. Every acknowledgement map in
|
|
274
|
+
// `evaluateEvidenceGraph` consumes the declarations of a claim, so a review
|
|
275
|
+
// arriving as a third `tagKind` would discharge coverage, count as a semantic
|
|
276
|
+
// host under `uniqueEvidence`, count as a cited unit under
|
|
277
|
+
// `singleEvidencePerSymbol`, and conflict with an exclusion of the same scope.
|
|
278
|
+
// Each of those is a build turning green because a review was mistaken for
|
|
279
|
+
// evidence. A distinct type cannot reach any of them by construction; a shared
|
|
280
|
+
// type could only be kept out by remembering to, at every one of six sites.
|
|
281
|
+
type parsedReview struct {
|
|
282
|
+
// Reviews names which acknowledgement this review answers for, so a review of
|
|
283
|
+
// an exclusion can never discharge a citation's obligation or the reverse.
|
|
284
|
+
Reviews tagKind
|
|
285
|
+
Target string
|
|
286
|
+
Fingerprint string
|
|
287
|
+
Description string
|
|
288
|
+
LineOffset int
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// marker spells the tag this review was written as, for a diagnostic that has to
|
|
292
|
+
// name the repair rather than describe it.
|
|
293
|
+
func (review parsedReview) marker() string {
|
|
294
|
+
return reviewMarkerFor(review.Reviews)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// parseReviews reads every verification statement one comment carries.
|
|
298
|
+
//
|
|
299
|
+
// `declarationLine` needs no change to keep these out of the declaration
|
|
300
|
+
// stream, and that is worth stating because it looks like an omission. It
|
|
301
|
+
// matches `@evidence` only when the next character is whitespace, so
|
|
302
|
+
// `@evidenceReview` falls through its loop exactly as `@evidenceExclude`
|
|
303
|
+
// reaching the `@evidence` arm does. The marker namespace was already reserved.
|
|
304
|
+
//
|
|
305
|
+
// The scan is independent for the same reason `todoEntries` is: one tag, one
|
|
306
|
+
// pass, no shared state with the parser whose output must never contain it. Any
|
|
307
|
+
// other `@`-opening line closes the review above it, so a review sharing a
|
|
308
|
+
// block with `@param` or a following `@evidence` swallows neither.
|
|
309
|
+
func parseReviews(comment string) []parsedReview {
|
|
310
|
+
trimmed := strings.TrimLeftFunc(comment, unicode.IsSpace)
|
|
311
|
+
leadingLines := strings.Count(comment[:len(comment)-len(trimmed)], "\n")
|
|
312
|
+
comment = trimmed
|
|
313
|
+
comment = strings.TrimPrefix(comment, "/**")
|
|
314
|
+
comment = strings.TrimPrefix(comment, "/*")
|
|
315
|
+
comment = strings.TrimSuffix(comment, "*/")
|
|
316
|
+
reviews := []parsedReview{}
|
|
317
|
+
var pending *parsedReview
|
|
318
|
+
var body []string
|
|
319
|
+
flush := func() {
|
|
320
|
+
if pending == nil {
|
|
321
|
+
return
|
|
322
|
+
}
|
|
323
|
+
target, remainder := splitDeclarationBody(strings.Join(body, "\n"))
|
|
324
|
+
fingerprint, description := splitReviewFingerprint(remainder)
|
|
325
|
+
pending.Target = target
|
|
326
|
+
pending.Fingerprint = fingerprint
|
|
327
|
+
pending.Description = description
|
|
328
|
+
reviews = append(reviews, *pending)
|
|
329
|
+
pending = nil
|
|
330
|
+
body = nil
|
|
331
|
+
}
|
|
332
|
+
for index, rawLine := range strings.Split(comment, "\n") {
|
|
333
|
+
line := strings.TrimSpace(rawLine)
|
|
334
|
+
line = strings.TrimSpace(strings.TrimPrefix(line, "*"))
|
|
335
|
+
line = strings.TrimSpace(strings.TrimPrefix(line, "///"))
|
|
336
|
+
if reviews, remainder, opened := reviewLine(line); opened {
|
|
337
|
+
flush()
|
|
338
|
+
pending = &parsedReview{
|
|
339
|
+
Reviews: reviews,
|
|
340
|
+
LineOffset: leadingLines + index,
|
|
341
|
+
}
|
|
342
|
+
body = []string{remainder}
|
|
343
|
+
continue
|
|
344
|
+
}
|
|
345
|
+
if strings.HasPrefix(line, "@") {
|
|
346
|
+
flush()
|
|
347
|
+
continue
|
|
348
|
+
}
|
|
349
|
+
if pending != nil {
|
|
350
|
+
body = append(body, line)
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
flush()
|
|
354
|
+
return reviews
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// reviewLine reports whether a line opens a review, which acknowledgement it
|
|
358
|
+
// answers for, and with what remainder.
|
|
359
|
+
//
|
|
360
|
+
// The whitespace boundary keeps a longer tag out. `@evidenceReviewed` is some
|
|
361
|
+
// other tool's tag or a typo, and consuming it would attach a review to a
|
|
362
|
+
// citation the author never reviewed. The same boundary is what separates
|
|
363
|
+
// `@evidenceExcludeReview` from `@evidenceExclude` in `declarationLine`, so
|
|
364
|
+
// neither parser can claim the other's tag.
|
|
365
|
+
func reviewLine(line string) (tagKind, string, bool) {
|
|
366
|
+
for _, candidate := range reviewMarkers {
|
|
367
|
+
if !strings.HasPrefix(line, candidate.marker) {
|
|
368
|
+
continue
|
|
369
|
+
}
|
|
370
|
+
remainder := line[len(candidate.marker):]
|
|
371
|
+
if remainder != "" && remainder[0] != ' ' && remainder[0] != '\t' {
|
|
372
|
+
continue
|
|
373
|
+
}
|
|
374
|
+
return candidate.tag, strings.TrimSpace(remainder), true
|
|
375
|
+
}
|
|
376
|
+
return "", "", false
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// splitReviewFingerprint separates an optional fingerprint from the description.
|
|
380
|
+
//
|
|
381
|
+
// The `#` prefix is required rather than inferred, for the reason
|
|
382
|
+
// `splitInlineLinkBody` records about targets: the token stays
|
|
383
|
+
// self-discriminating through a boundary character instead of a guess. A bare
|
|
384
|
+
// fixed-width hex token would reintroduce that guess, and ordinary prose
|
|
385
|
+
// supplies the counter-examples — `cafe`, `deadbeef`, `beefed`.
|
|
386
|
+
//
|
|
387
|
+
// The exact length is checked as well as the prefix, because `#` alone is not
|
|
388
|
+
// enough. A requirement anchor such as `#req-search-policies` opens a
|
|
389
|
+
// description in exactly that shape, and eating it would strip the author's
|
|
390
|
+
// first words and then report a malformed fingerprint they did not write.
|
|
391
|
+
//
|
|
392
|
+
// No target begins with `#`: a Markdown target carries its anchor after a path,
|
|
393
|
+
// a Prisma target carries its `prisma:` prefix, a Swagger target its method,
|
|
394
|
+
// and a code target its brace. So this only ever reads the position after a
|
|
395
|
+
// target that has already been consumed.
|
|
396
|
+
func splitReviewFingerprint(remainder string) (string, string) {
|
|
397
|
+
remainder = strings.TrimSpace(remainder)
|
|
398
|
+
if !strings.HasPrefix(remainder, "#") {
|
|
399
|
+
return "", remainder
|
|
400
|
+
}
|
|
401
|
+
candidate := remainder[1:]
|
|
402
|
+
description := ""
|
|
403
|
+
for index, char := range candidate {
|
|
404
|
+
if unicode.IsSpace(char) {
|
|
405
|
+
candidate, description = candidate[:index], strings.TrimSpace(candidate[index:])
|
|
406
|
+
break
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
if len(candidate) != reviewFingerprintLength || !isLowerHex(candidate) {
|
|
410
|
+
// A rejected token stays in the description, because it is usually prose: a
|
|
411
|
+
// requirement anchor such as `#req-search-policies` opens a sentence in
|
|
412
|
+
// exactly this shape and the author's words must survive.
|
|
413
|
+
//
|
|
414
|
+
// Unless it is the whole body. `#A3F9C1D` alone is a fingerprint whose case
|
|
415
|
+
// is wrong, not a description, and treating it as prose would let the
|
|
416
|
+
// shortest wrong path an author can take pass silently: paste the expected
|
|
417
|
+
// value, get the case wrong, stop, and ship a review that states nothing
|
|
418
|
+
// while satisfying the non-empty test. Reported as malformed instead.
|
|
419
|
+
if description == "" {
|
|
420
|
+
return "", ""
|
|
421
|
+
}
|
|
422
|
+
return "", remainder
|
|
423
|
+
}
|
|
424
|
+
return candidate, description
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
func isLowerHex(value string) bool {
|
|
428
|
+
for _, char := range value {
|
|
429
|
+
if (char < '0' || char > '9') && (char < 'a' || char > 'f') {
|
|
430
|
+
return false
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return value != ""
|
|
434
|
+
}
|
|
435
|
+
|
|
223
436
|
func containsWhitespace(value string) bool {
|
|
224
437
|
for _, char := range value {
|
|
225
438
|
if unicode.IsSpace(char) {
|
package/native/documented.go
CHANGED
|
@@ -251,17 +251,17 @@ func documentedHosts(file *shimast.SourceFile) []documentedHost {
|
|
|
251
251
|
|
|
252
252
|
// hostNodesOf keeps the declarations that can actually carry a citation.
|
|
253
253
|
//
|
|
254
|
-
// A
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
// exists to prevent.
|
|
254
|
+
// A unit's nodes are the positions the identity owns, and owning a position is
|
|
255
|
+
// not the same as being able to hold a tag there. A withdrawn identity records
|
|
256
|
+
// declarations that are deliberately not hosts, and the caller has skipped
|
|
257
|
+
// those before asking, so every position reaching this today is one. That makes
|
|
258
|
+
// the filter remove nothing, which is the point: it states the premise the rule
|
|
259
|
+
// depends on rather than waiting for a form that breaks it.
|
|
261
260
|
//
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
//
|
|
261
|
+
// Demanding a block at a position that is not a host would send an author's
|
|
262
|
+
// `@evidence` where `evidence/graph` rejects it as an unsupported host, the
|
|
263
|
+
// rule steering citations somewhere a citation cannot live, which is the
|
|
264
|
+
// failure it exists to prevent.
|
|
265
265
|
func hostNodesOf(
|
|
266
266
|
nodes []*shimast.Node,
|
|
267
267
|
supported map[*shimast.Node]symbolSet,
|
|
@@ -280,8 +280,8 @@ func hostNodesOf(
|
|
|
280
280
|
//
|
|
281
281
|
// The collector records them as it walks, and the merged forms are folded in
|
|
282
282
|
// afterwards, so the slice arrives in neither order. Placement is defined
|
|
283
|
-
// against the declaration that comes first, so that has to be `Node
|
|
284
|
-
//
|
|
283
|
+
// against the declaration that comes first, so that has to be `Node`, and for a
|
|
284
|
+
// variable the enclosing statement wins over its declarator, which is where
|
|
285
285
|
// TypeScript actually attaches the block.
|
|
286
286
|
func orderIdentityDeclarations(hosts []documentedHost) []documentedHost {
|
|
287
287
|
for index := range hosts {
|