@ttsc/evidence 0.30.4 → 0.31.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 +14 -6
- package/lib/index.d.ts +2 -3
- package/lib/index.js +2 -3
- package/lib/internal/canonicalDigest.js +12 -6
- package/lib/internal/loadPrismaModels.js +52 -7
- package/lib/internal/loadSwaggerOperations.js +253 -59
- package/lib/internal/normalizeSwaggerDocument.js +320 -2
- package/lib/structures/ITtscEvidenceDocumentedConfig.d.ts +16 -8
- package/lib/structures/ITtscEvidenceGraphClaim.d.ts +6 -0
- package/lib/structures/ITtscEvidenceGraphClaimBase.d.ts +8 -2
- package/lib/structures/ITtscEvidenceGraphConfig.d.ts +6 -0
- package/lib/structures/ITtscEvidenceGraphMarkdownClaim.d.ts +7 -1
- package/lib/structures/ITtscEvidenceGraphMarkdownReference.d.ts +7 -1
- package/lib/structures/ITtscEvidenceGraphPrismaClaim.d.ts +7 -1
- package/lib/structures/ITtscEvidenceGraphPrismaReference.d.ts +7 -1
- package/lib/structures/ITtscEvidenceGraphReference.d.ts +10 -4
- package/lib/structures/ITtscEvidenceGraphReferenceBase.d.ts +6 -0
- package/lib/structures/ITtscEvidenceGraphSwaggerReference.d.ts +6 -0
- package/lib/structures/ITtscEvidenceGraphTypeScriptClaim.d.ts +7 -1
- package/lib/structures/ITtscEvidenceGraphTypeScriptReference.d.ts +11 -3
- package/lib/structures/ITtscEvidenceRules.d.ts +93 -0
- package/lib/structures/TtscEvidenceGraphMarkdownSymbol.d.ts +39 -0
- package/lib/{typings → structures}/TtscEvidenceGraphPrismaSymbol.d.ts +6 -0
- package/lib/{typings → structures}/TtscEvidenceGraphTypeScriptSymbol.d.ts +6 -0
- package/lib/structures/index.d.ts +4 -0
- package/lib/structures/index.js +4 -0
- package/native/base.go +25 -23
- package/native/bridge_trace.go +348 -0
- package/native/comments.go +1 -1
- package/native/config.go +6 -3
- package/native/declaration.go +17 -43
- package/native/documentation_regions.go +214 -0
- package/native/documented.go +5 -4
- package/native/exports.go +1 -1
- package/native/fingerprint.go +2 -8
- package/native/glob.go +4 -1
- package/native/graph.go +59 -23
- package/native/graph_nodes.go +2 -1
- package/native/hints.go +22 -18
- package/native/imports.go +7 -6
- package/native/input_reader.go +83 -0
- package/native/inputs.go +13 -12
- package/native/loader.go +36 -24
- package/native/markdown.go +587 -177
- package/native/markdown_capture.go +223 -0
- package/native/model.go +2 -1
- package/native/prisma.go +110 -25
- package/native/prisma_cache.go +12 -8
- package/native/prisma_locator.go +70 -33
- package/native/review.go +8 -7
- package/native/rooted_typescript.go +3 -4
- package/native/swagger.go +123 -15
- package/native/swagger_cache.go +16 -12
- package/native/todo.go +12 -0
- package/native/typescript.go +129 -25
- package/package.json +9 -7
- package/lib/typings/ITtscLintContributorRules.d.ts +0 -57
- package/lib/typings/ITtscLintRuleOptionsMap.d.ts +0 -20
- package/lib/typings/ITtscLintRuleOptionsMap.js +0 -2
- package/lib/typings/TtscEvidenceGraphMarkdownSymbol.d.ts +0 -32
- package/lib/typings/index.d.ts +0 -5
- package/lib/typings/index.js +0 -21
- /package/lib/{typings/ITtscLintContributorRules.js → structures/ITtscEvidenceRules.js} +0 -0
- /package/lib/{typings → structures}/TtscEvidenceGraphMarkdownSymbol.js +0 -0
- /package/lib/{typings → structures}/TtscEvidenceGraphPrismaSymbol.js +0 -0
- /package/lib/{typings → structures}/TtscEvidenceGraphTypeScriptSymbol.js +0 -0
package/native/prisma_locator.go
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
package evidence
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
-
"os"
|
|
5
4
|
"strings"
|
|
6
5
|
"unicode"
|
|
7
6
|
)
|
|
@@ -23,14 +22,16 @@ const (
|
|
|
23
22
|
prismaBlockComment prismaCommentForm = "block"
|
|
24
23
|
)
|
|
25
24
|
|
|
26
|
-
// prismaCommentRun is one
|
|
27
|
-
// attaches to.
|
|
25
|
+
// prismaCommentRun is one comment run and the declaration it attaches to.
|
|
28
26
|
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
27
|
+
// A run is either the contiguous documentation block above one declaration or a
|
|
28
|
+
// single comment line that can never document one: a `//` line, or a comment
|
|
29
|
+
// that shares its line with code. Body preserves a documentation run's own line
|
|
30
|
+
// spacing — a line the run skipped is empty rather than absent — so a tag's
|
|
31
|
+
// offset inside the body is still its offset in the file. Key is the
|
|
32
|
+
// declaration the run documents, empty when it documents nothing unless
|
|
33
|
+
// FileLevel identifies a top-level triple-slash exclusion carrier. Trailing
|
|
34
|
+
// marks the single-line run whose comment shares its line with code.
|
|
34
35
|
type prismaCommentRun struct {
|
|
35
36
|
Form prismaCommentForm
|
|
36
37
|
Path string
|
|
@@ -38,6 +39,7 @@ type prismaCommentRun struct {
|
|
|
38
39
|
Body string
|
|
39
40
|
Key string
|
|
40
41
|
FileLevel bool
|
|
42
|
+
Trailing bool
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
// prismaFileScan is everything one schema file yields to the native side.
|
|
@@ -46,12 +48,20 @@ type prismaFileScan struct {
|
|
|
46
48
|
Comments []prismaCommentRun
|
|
47
49
|
}
|
|
48
50
|
|
|
51
|
+
// prismaByteOrderMark is the UTF-8 encoding of U+FEFF, spelled as bytes so the
|
|
52
|
+
// constant is visible in an editor that hides the character.
|
|
53
|
+
const prismaByteOrderMark = "\xef\xbb\xbf"
|
|
54
|
+
|
|
55
|
+
// prismaLineBreaks rewrites every line terminator Prisma's grammar accepts to
|
|
56
|
+
// `\n`, taking `\r\n` as one terminator rather than two.
|
|
57
|
+
var prismaLineBreaks = strings.NewReplacer("\r\n", "\n", "\r", "\n")
|
|
58
|
+
|
|
49
59
|
// prismaBlockKeywords are the top-level block openers Prisma's grammar accepts.
|
|
50
60
|
//
|
|
51
|
-
// All six are recognized even though only `model
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
61
|
+
// All six are recognized even though only `model` and `view` own a unit, because
|
|
62
|
+
// recognizing a block is how the scan knows when it is *not* inside one.
|
|
63
|
+
// Dropping `datasource` and `generator` would leave their settings looking like
|
|
64
|
+
// members of whatever block was declared before them.
|
|
55
65
|
var prismaBlockKeywords = map[string]bool{
|
|
56
66
|
"model": true,
|
|
57
67
|
"view": true,
|
|
@@ -61,7 +71,13 @@ var prismaBlockKeywords = map[string]bool{
|
|
|
61
71
|
"generator": true,
|
|
62
72
|
}
|
|
63
73
|
|
|
64
|
-
// prismaMemberBlocks are the blocks whose members
|
|
74
|
+
// prismaMemberBlocks are the blocks whose names and members the scan locates.
|
|
75
|
+
//
|
|
76
|
+
// Locating a block is not owning a unit for it. A `type` block is a composite
|
|
77
|
+
// type, which is outside the unit model: the parser reports it apart from the
|
|
78
|
+
// models the units are built from, so its locations are recorded and never
|
|
79
|
+
// looked up, and a citation written in one is reported as documenting something
|
|
80
|
+
// that is not a model, column, or relation.
|
|
65
81
|
var prismaMemberBlocks = map[string]bool{
|
|
66
82
|
"model": true,
|
|
67
83
|
"view": true,
|
|
@@ -90,11 +106,13 @@ var prismaMemberBlocks = map[string]bool{
|
|
|
90
106
|
func locatePrismaDeclarations(
|
|
91
107
|
root string,
|
|
92
108
|
sources []string,
|
|
109
|
+
readers ...evidenceInputReader,
|
|
93
110
|
) (map[string]prismaLocation, []prismaCommentRun) {
|
|
111
|
+
inputs := inputReader(readers)
|
|
94
112
|
locations := map[string]prismaLocation{}
|
|
95
113
|
comments := []prismaCommentRun{}
|
|
96
114
|
for _, source := range sources {
|
|
97
|
-
content, err :=
|
|
115
|
+
content, err := inputs.ReadFile(resolveProjectPath(root, source))
|
|
98
116
|
if err != nil {
|
|
99
117
|
continue
|
|
100
118
|
}
|
|
@@ -120,14 +138,27 @@ func locatePrismaDeclarations(
|
|
|
120
138
|
// - A run followed by a block attribute or by the block's closing brace
|
|
121
139
|
// documents nothing.
|
|
122
140
|
//
|
|
141
|
+
// A comment that shares its line with code never joins a run. Prisma reads a
|
|
142
|
+
// trailing `///` or `/* */` as documentation of the field on its own line, and
|
|
143
|
+
// never of the declaration below it, and this graph reads a citation only from
|
|
144
|
+
// the lines above a declaration, so such a comment is reported instead.
|
|
145
|
+
//
|
|
123
146
|
// A run that documents nothing keeps an empty Key rather than being dropped,
|
|
124
147
|
// because a citation written there is a citation that will never be honoured —
|
|
125
148
|
// and this rule reports that rather than letting it pass for a tag that works.
|
|
149
|
+
//
|
|
150
|
+
// The scan reads the text the parser was handed, not the bytes on disk: the
|
|
151
|
+
// bridge decodes a file as UTF-8, which drops a leading byte order mark, and
|
|
152
|
+
// Prisma's grammar ends a line at `\n`, `\r\n`, or a lone `\r`. A scan that kept
|
|
153
|
+
// the mark would fail to recognise a first-line block opener or to attach a
|
|
154
|
+
// first-line comment, and one that split on `\n` alone would see a lone-`\r`
|
|
155
|
+
// file as a single line.
|
|
126
156
|
func scanPrismaFile(
|
|
127
157
|
source string,
|
|
128
158
|
content string,
|
|
129
159
|
locations map[string]prismaLocation,
|
|
130
160
|
) prismaFileScan {
|
|
161
|
+
content = prismaLineBreaks.Replace(strings.TrimPrefix(content, prismaByteOrderMark))
|
|
131
162
|
scan := prismaFileScan{Locations: locations}
|
|
132
163
|
record := func(key string, line int) {
|
|
133
164
|
if key == "" {
|
|
@@ -155,8 +186,7 @@ func scanPrismaFile(
|
|
|
155
186
|
pending = nil
|
|
156
187
|
}
|
|
157
188
|
for index, raw := range lines {
|
|
158
|
-
|
|
159
|
-
code, comment, form, stillCommented := prismaPartsOf(line, commented)
|
|
189
|
+
code, comment, form, stillCommented := prismaPartsOf(raw, commented)
|
|
160
190
|
commented = stillCommented
|
|
161
191
|
trimmed := strings.TrimSpace(code)
|
|
162
192
|
if comment != "" || form != "" {
|
|
@@ -222,9 +252,9 @@ type prismaPendingComment struct {
|
|
|
222
252
|
Form prismaCommentForm
|
|
223
253
|
Line int
|
|
224
254
|
Text string
|
|
225
|
-
// Trailing marks a comment that shared its line with code.
|
|
226
|
-
//
|
|
227
|
-
//
|
|
255
|
+
// Trailing marks a comment that shared its line with code. Such a comment
|
|
256
|
+
// belongs to the line it sits on, never to the declaration below it, so it
|
|
257
|
+
// cannot join the documentation run above that declaration.
|
|
228
258
|
Trailing bool
|
|
229
259
|
// TopLevel marks a comment written outside every Prisma block. A detached
|
|
230
260
|
// documentation run there may carry claim-local exclusions for the file
|
|
@@ -232,12 +262,16 @@ type prismaPendingComment struct {
|
|
|
232
262
|
TopLevel bool
|
|
233
263
|
}
|
|
234
264
|
|
|
235
|
-
// prismaCommentRuns
|
|
265
|
+
// prismaCommentRuns turns the pending comment lines above one declaration into
|
|
266
|
+
// runs.
|
|
236
267
|
//
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
//
|
|
268
|
+
// Every `///` and `/* */` line that does not share its line with code is joined
|
|
269
|
+
// into the one documentation run, and that run alone can carry a citation. Every
|
|
270
|
+
// other line, a `//` comment or a comment trailing code, becomes a run of its
|
|
271
|
+
// own, and exists solely to be reported. Keeping the documentation run's body
|
|
272
|
+
// line-aligned is what lets a tag inside it be reported at the line it was
|
|
273
|
+
// written on: a line the run skipped becomes an empty line rather than
|
|
274
|
+
// disappearing.
|
|
241
275
|
func prismaCommentRuns(
|
|
242
276
|
source string,
|
|
243
277
|
pending []prismaPendingComment,
|
|
@@ -258,11 +292,12 @@ func prismaCommentRuns(
|
|
|
258
292
|
continue
|
|
259
293
|
}
|
|
260
294
|
runs = append(runs, prismaCommentRun{
|
|
261
|
-
Form:
|
|
262
|
-
Path:
|
|
263
|
-
Line:
|
|
264
|
-
Body:
|
|
265
|
-
Key:
|
|
295
|
+
Form: comment.Form,
|
|
296
|
+
Path: source,
|
|
297
|
+
Line: comment.Line,
|
|
298
|
+
Body: comment.Text,
|
|
299
|
+
Key: "",
|
|
300
|
+
Trailing: comment.Trailing,
|
|
266
301
|
})
|
|
267
302
|
}
|
|
268
303
|
if len(documenting) != 0 {
|
|
@@ -294,8 +329,8 @@ func prismaCommentRuns(
|
|
|
294
329
|
// String contents are dropped from the code rather than kept, because a brace
|
|
295
330
|
// inside one must not open or close a block — `@default("}")` is a legal field
|
|
296
331
|
// that a naive brace count reads as the end of its model. The comment text is
|
|
297
|
-
// returned rather than discarded, because
|
|
298
|
-
//
|
|
332
|
+
// returned rather than discarded, because a `//` comment cannot carry a citation
|
|
333
|
+
// and this rule reports one written there instead of ignoring it.
|
|
299
334
|
func prismaPartsOf(
|
|
300
335
|
line string,
|
|
301
336
|
commented bool,
|
|
@@ -345,7 +380,9 @@ func prismaPartsOf(
|
|
|
345
380
|
} else if form == "" {
|
|
346
381
|
form = prismaLineComment
|
|
347
382
|
}
|
|
348
|
-
|
|
383
|
+
// One ordinary padding space follows the documentation prefix; further
|
|
384
|
+
// indentation belongs to the body and decides whether a fence is literal.
|
|
385
|
+
comment.WriteString(strings.TrimPrefix(string(runes[rest:]), " "))
|
|
349
386
|
break
|
|
350
387
|
}
|
|
351
388
|
if char == '/' && index+1 < len(runes) && runes[index+1] == '*' {
|
|
@@ -358,7 +395,7 @@ func prismaPartsOf(
|
|
|
358
395
|
}
|
|
359
396
|
code.WriteRune(char)
|
|
360
397
|
}
|
|
361
|
-
return code.String(), strings.
|
|
398
|
+
return code.String(), strings.TrimRight(comment.String(), " \t\r"), form, commented
|
|
362
399
|
}
|
|
363
400
|
|
|
364
401
|
// prismaBlockHead reads `model <Name>` from a block opener.
|
package/native/review.go
CHANGED
|
@@ -18,7 +18,8 @@ import (
|
|
|
18
18
|
// second one is never put.
|
|
19
19
|
//
|
|
20
20
|
// The rule puts it. Every `@evidence` and `@evidenceExclude` on a public
|
|
21
|
-
// identity must be answered by
|
|
21
|
+
// identity must be answered by `@evidenceReview` or `@evidenceExcludeReview`,
|
|
22
|
+
// respectively, naming the same target, so a
|
|
22
23
|
// citation cannot be produced without a separately addressed statement written
|
|
23
24
|
// as its own act.
|
|
24
25
|
//
|
|
@@ -145,12 +146,6 @@ func judgeReviewedHost(ctx *rule.Context, host documentedHost) {
|
|
|
145
146
|
}
|
|
146
147
|
}
|
|
147
148
|
|
|
148
|
-
// acknowledgementQuestion is what a review of this tag has to answer.
|
|
149
|
-
//
|
|
150
|
-
// The two are not the same question and the diagnostic says which one is open. A
|
|
151
|
-
// citation is verified by checking that this declaration does what the cited unit
|
|
152
|
-
// describes. An exclusion is verified by checking that the unit genuinely does not
|
|
153
|
-
// apply here, which no reading of this declaration can establish on its own.
|
|
154
149
|
// reviewExample shows the shape of a check that answers this tag's question.
|
|
155
150
|
//
|
|
156
151
|
// The two examples differ because the two checks do. A citation is answered by
|
|
@@ -164,6 +159,12 @@ func reviewExample(tag tagKind) string {
|
|
|
164
159
|
return "A review of a citation names what you read or ran, as in 'read the section's three rules and ran the checkout test'."
|
|
165
160
|
}
|
|
166
161
|
|
|
162
|
+
// acknowledgementQuestion is what a review of this tag has to answer.
|
|
163
|
+
//
|
|
164
|
+
// The two are not the same question and the diagnostic says which one is open. A
|
|
165
|
+
// citation is verified by checking that this declaration does what the cited unit
|
|
166
|
+
// describes. An exclusion is verified by checking that the unit genuinely does not
|
|
167
|
+
// apply here, which no reading of this declaration can establish on its own.
|
|
167
168
|
func acknowledgementQuestion(tag tagKind) string {
|
|
168
169
|
if tag == tagExclude {
|
|
169
170
|
return "The exclusion states that this claim does not cover that target."
|
|
@@ -2,7 +2,6 @@ package evidence
|
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
4
|
"io/fs"
|
|
5
|
-
"os"
|
|
6
5
|
"path/filepath"
|
|
7
6
|
"sort"
|
|
8
7
|
"strings"
|
|
@@ -14,7 +13,7 @@ func materializeRootedTypeScriptReference(claim claimSpec, reference referenceSp
|
|
|
14
13
|
state := referenceState{Spec: reference, Healthy: false, UnitsByScope: map[string][]*evidenceUnit{}}
|
|
15
14
|
context := claimLabel(claim) + " " + referenceLabel(reference)
|
|
16
15
|
base := reference.Base
|
|
17
|
-
if info, err :=
|
|
16
|
+
if info, err := loader.inputs.Stat(base.Absolute); err != nil || !info.IsDir() {
|
|
18
17
|
cause := "the path names a file"
|
|
19
18
|
if err != nil {
|
|
20
19
|
cause = causeText(err)
|
|
@@ -25,13 +24,13 @@ func materializeRootedTypeScriptReference(claim claimSpec, reference referenceSp
|
|
|
25
24
|
if !resolved {
|
|
26
25
|
return state, []string{unresolvedBaseProblem(base, artifactTypeScript)}
|
|
27
26
|
}
|
|
28
|
-
from, resolved = physicalTypeScriptPath(from)
|
|
27
|
+
from, resolved = physicalTypeScriptPath(from, loader.inputs)
|
|
29
28
|
if !resolved {
|
|
30
29
|
return state, []string{unresolvedBaseProblem(base, artifactTypeScript)}
|
|
31
30
|
}
|
|
32
31
|
entries := map[string]bool{}
|
|
33
32
|
problems := []string{}
|
|
34
|
-
err :=
|
|
33
|
+
err := loader.inputs.WalkDir(from, func(current string, entry fs.DirEntry, walkErr error) error {
|
|
35
34
|
if walkErr != nil {
|
|
36
35
|
return walkErr
|
|
37
36
|
}
|
package/native/swagger.go
CHANGED
|
@@ -8,6 +8,7 @@ import (
|
|
|
8
8
|
"net/url"
|
|
9
9
|
"os"
|
|
10
10
|
"os/exec"
|
|
11
|
+
"regexp"
|
|
11
12
|
"sort"
|
|
12
13
|
"strings"
|
|
13
14
|
"time"
|
|
@@ -65,7 +66,8 @@ type swaggerNormalizationResult struct {
|
|
|
65
66
|
// document's bytes — an entry that answers a later cycle with the wrong
|
|
66
67
|
// document, forever.
|
|
67
68
|
//
|
|
68
|
-
// It is empty for a remote source,
|
|
69
|
+
// It is empty for a remote source, whose successful result is reused by URL
|
|
70
|
+
// rather than by content for the process lifetime.
|
|
69
71
|
type swaggerDocumentInventory struct {
|
|
70
72
|
Source string `json:"source"`
|
|
71
73
|
Operations []swaggerOperation `json:"operations"`
|
|
@@ -114,16 +116,37 @@ func loadSwaggerInventories(
|
|
|
114
116
|
// two-hundred-operation one pay nearly the same. A resident host repeats
|
|
115
117
|
// this every cycle, so an unchanged document is re-normalized on every
|
|
116
118
|
// TypeScript keystroke that triggers a rebuild.
|
|
117
|
-
|
|
119
|
+
config.inputs.Unavailable()
|
|
120
|
+
digests := swaggerContentDigests(root, sources, config.inputs)
|
|
121
|
+
trace := newEvidenceBridgeTrace("swagger")
|
|
122
|
+
if trace != nil {
|
|
123
|
+
trace.nativeLookup = true
|
|
124
|
+
trace.record("bridge-lookup", os.Getpid(), map[string]any{
|
|
125
|
+
"bridge": "swagger", "root": root, "sources": sources,
|
|
126
|
+
"nativeDigests": digests, "nativeLookup": true,
|
|
127
|
+
})
|
|
128
|
+
}
|
|
118
129
|
pending := []string{}
|
|
119
130
|
problems := graphDiagnostics{}
|
|
120
131
|
severity := artifactSeverity(config, artifactSwagger)
|
|
121
132
|
for _, source := range sources {
|
|
122
133
|
outcome, hit := lookupSwaggerDocument(source, digests[source])
|
|
123
134
|
if !hit {
|
|
135
|
+
if trace != nil {
|
|
136
|
+
_, present := digests[source]
|
|
137
|
+
trace.record("bridge-cache-miss", os.Getpid(), map[string]any{
|
|
138
|
+
"bridge": "swagger", "source": source, "nativeDigest": digests[source], "nativeDigestPresent": present,
|
|
139
|
+
})
|
|
140
|
+
}
|
|
124
141
|
pending = append(pending, source)
|
|
125
142
|
continue
|
|
126
143
|
}
|
|
144
|
+
if trace != nil {
|
|
145
|
+
_, present := digests[source]
|
|
146
|
+
trace.record("bridge-cache-hit", os.Getpid(), map[string]any{
|
|
147
|
+
"bridge": "swagger", "source": source, "nativeDigest": digests[source], "nativeDigestPresent": present,
|
|
148
|
+
})
|
|
149
|
+
}
|
|
127
150
|
problems = problems.add(
|
|
128
151
|
swaggerSeverity(config, source),
|
|
129
152
|
swaggerUnitsFromOutcome(source, inventories[source], outcome)...,
|
|
@@ -133,9 +156,13 @@ func loadSwaggerInventories(
|
|
|
133
156
|
return inventories, problems
|
|
134
157
|
}
|
|
135
158
|
|
|
136
|
-
result, err := normalizeSwaggerSources(root, pending)
|
|
159
|
+
result, err := normalizeSwaggerSources(root, pending, trace)
|
|
137
160
|
if err != nil {
|
|
138
|
-
|
|
161
|
+
reason := causeText(err)
|
|
162
|
+
for _, source := range pending {
|
|
163
|
+
reason = redactSwaggerError(source, reason)
|
|
164
|
+
}
|
|
165
|
+
message := "Evidence graph could not run its Swagger normalizer: " + reason + ". Swagger references require Node.js and the installed @typia/interface, @typia/utils, and yaml dependencies."
|
|
139
166
|
for _, source := range pending {
|
|
140
167
|
inventories[source].LoadFailed = true
|
|
141
168
|
inventories[source].Problems = append(
|
|
@@ -229,22 +256,41 @@ func configuredSwaggerSources(config graphConfig) []string {
|
|
|
229
256
|
return sources
|
|
230
257
|
}
|
|
231
258
|
|
|
259
|
+
// normalizeSwaggerSources retains the real Node request/Run/unmarshal boundary.
|
|
260
|
+
// Optional private trace context pairs the caller's existing local digest map;
|
|
261
|
+
// direct calls remain unpaired and do not invent native lookup values. Actual
|
|
262
|
+
// pending source order, wire bytes, returned outcomes and cache keys are kept.
|
|
232
263
|
func normalizeSwaggerSources(
|
|
233
264
|
root string,
|
|
234
265
|
sources []string,
|
|
266
|
+
traces ...*evidenceBridgeTrace,
|
|
235
267
|
) (swaggerNormalizationResult, error) {
|
|
268
|
+
var trace *evidenceBridgeTrace
|
|
269
|
+
if len(traces) != 0 {
|
|
270
|
+
trace = traces[0]
|
|
271
|
+
} else {
|
|
272
|
+
trace = newEvidenceBridgeTrace("swagger")
|
|
273
|
+
}
|
|
236
274
|
request, err := json.Marshal(swaggerNormalizationRequest{
|
|
237
275
|
Root: root,
|
|
238
276
|
Sources: sources,
|
|
239
277
|
})
|
|
240
278
|
if err != nil {
|
|
279
|
+
trace.preparationFailure(err)
|
|
241
280
|
return swaggerNormalizationResult{}, err
|
|
242
281
|
}
|
|
282
|
+
if trace != nil {
|
|
283
|
+
trace.record("bridge-request", os.Getpid(), map[string]any{
|
|
284
|
+
"bridge": "swagger", "root": root, "sources": sources, "nativeLookup": trace.nativeLookup,
|
|
285
|
+
})
|
|
286
|
+
}
|
|
243
287
|
node := os.Getenv("TTSC_NODE_BINARY")
|
|
244
288
|
if node == "" {
|
|
245
289
|
node, err = exec.LookPath("node")
|
|
246
290
|
if err != nil {
|
|
247
|
-
|
|
291
|
+
failure := errors.New("Node.js executable was not found")
|
|
292
|
+
trace.preparationFailure(failure)
|
|
293
|
+
return swaggerNormalizationResult{}, failure
|
|
248
294
|
}
|
|
249
295
|
}
|
|
250
296
|
ctx, cancel := context.WithTimeout(context.Background(), swaggerBridgeTimeout)
|
|
@@ -256,20 +302,43 @@ func normalizeSwaggerSources(
|
|
|
256
302
|
stderr := &limitedBuffer{Limit: swaggerBridgeErrorLimit}
|
|
257
303
|
command.Stdout = stdout
|
|
258
304
|
command.Stderr = stderr
|
|
259
|
-
|
|
260
|
-
|
|
305
|
+
start := trace.attempt(command)
|
|
306
|
+
runErr := command.Run()
|
|
307
|
+
deadlineExceeded := false
|
|
308
|
+
if runErr != nil {
|
|
309
|
+
deadlineExceeded = ctx.Err() == context.DeadlineExceeded
|
|
310
|
+
}
|
|
311
|
+
end := time.Time{}
|
|
312
|
+
if trace != nil {
|
|
313
|
+
end = time.Now().UTC()
|
|
314
|
+
}
|
|
315
|
+
if runErr != nil {
|
|
316
|
+
trace.result(command, stdout, stderr, start, end, runErr, "not-attempted", nil, nil, nil)
|
|
317
|
+
if deadlineExceeded {
|
|
261
318
|
return swaggerNormalizationResult{}, errors.New("Swagger normalizer exceeded its 60 second timeout")
|
|
262
319
|
}
|
|
263
320
|
detail := strings.TrimSpace(stderr.String())
|
|
264
321
|
if detail == "" {
|
|
265
|
-
detail =
|
|
322
|
+
detail = runErr.Error()
|
|
266
323
|
}
|
|
267
324
|
return swaggerNormalizationResult{}, errors.New(detail)
|
|
268
325
|
}
|
|
269
326
|
var result swaggerNormalizationResult
|
|
270
327
|
if err := json.Unmarshal(stdout.Bytes(), &result); err != nil {
|
|
328
|
+
trace.result(command, stdout, stderr, start, end, nil, "failed", err, nil, nil)
|
|
271
329
|
return swaggerNormalizationResult{}, errors.New("Swagger normalizer returned invalid JSON: " + err.Error())
|
|
272
330
|
}
|
|
331
|
+
if trace != nil {
|
|
332
|
+
documents := make([]string, 0, len(result.Documents))
|
|
333
|
+
problems := make([]string, 0, len(result.Problems))
|
|
334
|
+
for _, document := range result.Documents {
|
|
335
|
+
documents = append(documents, document.Source)
|
|
336
|
+
}
|
|
337
|
+
for _, problem := range result.Problems {
|
|
338
|
+
problems = append(problems, problem.Source)
|
|
339
|
+
}
|
|
340
|
+
trace.result(command, stdout, stderr, start, end, nil, "succeeded", nil, documents, problems)
|
|
341
|
+
}
|
|
273
342
|
return result, nil
|
|
274
343
|
}
|
|
275
344
|
|
|
@@ -315,27 +384,66 @@ func isRemoteSwaggerSource(source string) bool {
|
|
|
315
384
|
return err == nil && (parsed.Scheme == "http" || parsed.Scheme == "https")
|
|
316
385
|
}
|
|
317
386
|
|
|
387
|
+
var swaggerErrorURL = regexp.MustCompile(`(?i)https?://[^\s"'<>]+`)
|
|
388
|
+
|
|
389
|
+
// displaySwaggerSource presents a URL without userinfo, query or fragment.
|
|
390
|
+
// Lexical boundaries also cover malformed URLs rejected by net/url. Local
|
|
391
|
+
// filesystem paths retain their spelling; this value never enters cache or
|
|
392
|
+
// transport identity. A malformed HTTP spelling without an authority has no
|
|
393
|
+
// reliably separable host/path and is shown only with its scheme.
|
|
318
394
|
func displaySwaggerSource(source string) string {
|
|
319
|
-
|
|
320
|
-
if
|
|
395
|
+
lower := strings.ToLower(source)
|
|
396
|
+
if !strings.Contains(source, "://") && !strings.HasPrefix(lower, "http:") && !strings.HasPrefix(lower, "https:") {
|
|
321
397
|
return source
|
|
322
398
|
}
|
|
323
|
-
|
|
324
|
-
|
|
399
|
+
suffix := strings.IndexAny(source, "?#")
|
|
400
|
+
base := source
|
|
401
|
+
if suffix >= 0 {
|
|
402
|
+
base = source[:suffix]
|
|
403
|
+
}
|
|
404
|
+
scheme := strings.Index(base, "://")
|
|
405
|
+
if scheme < 0 {
|
|
406
|
+
return source[:strings.Index(source, ":")+1] + "[redacted]"
|
|
325
407
|
}
|
|
326
|
-
|
|
327
|
-
|
|
408
|
+
start := scheme + 3
|
|
409
|
+
end := len(base)
|
|
410
|
+
if slash := strings.Index(base[start:], "/"); slash >= 0 {
|
|
411
|
+
end = start + slash
|
|
412
|
+
}
|
|
413
|
+
if user := strings.LastIndex(base[start:end], "@"); user >= 0 {
|
|
414
|
+
base = base[:start] + "***@" + base[start+user+1:]
|
|
415
|
+
} else if _, err := url.Parse(base[:end]); err != nil {
|
|
416
|
+
// A malformed authority can be truncated userinfo, not a separable host.
|
|
417
|
+
base = base[:start] + "[redacted]" + base[end:]
|
|
418
|
+
}
|
|
419
|
+
if suffix >= 0 {
|
|
420
|
+
base += "?[redacted]"
|
|
421
|
+
}
|
|
422
|
+
return base
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// redactSwaggerError sanitizes remote diagnostic presentation, including every
|
|
426
|
+
// repeated or foreign URL reflected by an external error. Exact source lookup
|
|
427
|
+
// and cache state retain their raw keys. Local filesystem causes stay intact.
|
|
428
|
+
func redactSwaggerError(source string, message string) string {
|
|
429
|
+
if displaySwaggerSource(source) == source && !isRemoteSwaggerSource(source) {
|
|
430
|
+
return message
|
|
328
431
|
}
|
|
329
|
-
|
|
432
|
+
message = strings.ReplaceAll(message, source, displaySwaggerSource(source))
|
|
433
|
+
return swaggerErrorURL.ReplaceAllStringFunc(message, displaySwaggerSource)
|
|
330
434
|
}
|
|
331
435
|
|
|
332
436
|
type limitedBuffer struct {
|
|
333
437
|
bytes.Buffer
|
|
334
438
|
Limit int
|
|
439
|
+
// Exceeded observes the existing failure boundary without changing its bytes
|
|
440
|
+
// or return error. A captured prefix cannot be a complete wire observation.
|
|
441
|
+
Exceeded bool
|
|
335
442
|
}
|
|
336
443
|
|
|
337
444
|
func (buffer *limitedBuffer) Write(content []byte) (int, error) {
|
|
338
445
|
if buffer.Len()+len(content) > buffer.Limit {
|
|
446
|
+
buffer.Exceeded = true
|
|
339
447
|
remaining := buffer.Limit - buffer.Len()
|
|
340
448
|
if remaining > 0 {
|
|
341
449
|
_, _ = buffer.Buffer.Write(content[:remaining])
|
package/native/swagger_cache.go
CHANGED
|
@@ -3,7 +3,6 @@ package evidence
|
|
|
3
3
|
import (
|
|
4
4
|
"crypto/sha256"
|
|
5
5
|
"encoding/hex"
|
|
6
|
-
"os"
|
|
7
6
|
"strings"
|
|
8
7
|
"sync"
|
|
9
8
|
)
|
|
@@ -33,7 +32,9 @@ const swaggerCacheLimit = 64
|
|
|
33
32
|
var swaggerDocuments = newSwaggerCache()
|
|
34
33
|
|
|
35
34
|
// swaggerRemoteDocuments remembers a URL's operations for the process lifetime,
|
|
36
|
-
// keyed by the address rather than by content
|
|
35
|
+
// keyed by the address rather than by content, within the same entry bound as
|
|
36
|
+
// the local cache: a session reading more distinct URLs than the bound fetches
|
|
37
|
+
// the oldest again.
|
|
37
38
|
//
|
|
38
39
|
// A remote document has no key without fetching it, so it cannot be revalidated
|
|
39
40
|
// the way a local file is. The choice is therefore not "cache or revalidate" but
|
|
@@ -65,7 +66,7 @@ var swaggerRemoteDocuments = newSwaggerCache()
|
|
|
65
66
|
// non-empty. Inferring rejection from the message would turn a reason-less
|
|
66
67
|
// failure into zero operations and no diagnostic — a rejected document that
|
|
67
68
|
// reads exactly like an empty but passing one, which is the shape
|
|
68
|
-
// `
|
|
69
|
+
// `TestSwaggerReportsARejectionThatCarriesNoReason` exists to forbid.
|
|
69
70
|
type swaggerDocumentOutcome struct {
|
|
70
71
|
Operations []swaggerOperation
|
|
71
72
|
Rejected bool
|
|
@@ -126,18 +127,20 @@ func (cache *swaggerCache) store(digest string, outcome swaggerDocumentOutcome)
|
|
|
126
127
|
//
|
|
127
128
|
// The digest is the whole cache key, which is what makes staleness structural
|
|
128
129
|
// rather than improbable: identical bytes normalize to identical operations, so
|
|
129
|
-
// a hit
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
130
|
+
// a hit returns what the current file means to the normalizer that first read
|
|
131
|
+
// it. The key names no project root and no normalizer installation. An edit, a
|
|
132
|
+
// truncation, or a same-length replacement all change the digest and miss. A
|
|
133
|
+
// missing or unreadable file yields no digest and is normalized as before, which
|
|
134
|
+
// keeps the normalizer's own diagnostic for it.
|
|
133
135
|
//
|
|
134
136
|
// An HTTP(S) source never participates. A URL has no validator without a
|
|
135
137
|
// fetch, and the fetch is most of what the normalizer costs, so a remote
|
|
136
138
|
// document cannot be shown unchanged without paying the price of finding out.
|
|
137
|
-
func swaggerContentDigests(root string, sources []string) map[string]string {
|
|
139
|
+
func swaggerContentDigests(root string, sources []string, readers ...evidenceInputReader) map[string]string {
|
|
140
|
+
inputs := inputReader(readers)
|
|
138
141
|
digests := map[string]string{}
|
|
139
142
|
for _, source := range sources {
|
|
140
|
-
digest := swaggerContentDigest(root, source)
|
|
143
|
+
digest := swaggerContentDigest(root, source, inputs)
|
|
141
144
|
if digest != "" {
|
|
142
145
|
digests[source] = digest
|
|
143
146
|
}
|
|
@@ -145,11 +148,12 @@ func swaggerContentDigests(root string, sources []string) map[string]string {
|
|
|
145
148
|
return digests
|
|
146
149
|
}
|
|
147
150
|
|
|
148
|
-
func swaggerContentDigest(root string, source string) string {
|
|
151
|
+
func swaggerContentDigest(root string, source string, readers ...evidenceInputReader) string {
|
|
152
|
+
inputs := inputReader(readers)
|
|
149
153
|
if isRemoteSwaggerSource(source) {
|
|
150
154
|
return ""
|
|
151
155
|
}
|
|
152
|
-
content, err :=
|
|
156
|
+
content, err := inputs.ReadFile(swaggerSourcePath(root, source))
|
|
153
157
|
if err != nil {
|
|
154
158
|
return ""
|
|
155
159
|
}
|
|
@@ -262,7 +266,7 @@ func swaggerUnitsFromOutcome(
|
|
|
262
266
|
// that trails off after a colon reads like a formatting bug rather than a
|
|
263
267
|
// broken document.
|
|
264
268
|
func swaggerNormalizationFailure(source string, message string) string {
|
|
265
|
-
reason := causeReason(strings.TrimSpace(message))
|
|
269
|
+
reason := causeReason(strings.TrimSpace(redactSwaggerError(source, message)))
|
|
266
270
|
if reason == "" {
|
|
267
271
|
reason = "the normalizer reported no reason"
|
|
268
272
|
}
|
package/native/todo.go
CHANGED
|
@@ -86,6 +86,11 @@ func todoMessage(entry string) string {
|
|
|
86
86
|
// '@todo' shares a block with '@param' or '@evidence' without swallowing them.
|
|
87
87
|
// The tag name matches case-insensitively, because '@TODO' is the same promise
|
|
88
88
|
// shouted.
|
|
89
|
+
//
|
|
90
|
+
// A fenced example is not read for tags, as it is not by the declaration parser:
|
|
91
|
+
// a '@todo' shown inside one is documentation of the tag rather than a debt. A
|
|
92
|
+
// fence inside an entry's text stays part of it, so the example is not cut out
|
|
93
|
+
// of the debt it follows.
|
|
89
94
|
func todoEntries(comment string) []string {
|
|
90
95
|
comment = strings.TrimSpace(comment)
|
|
91
96
|
comment = strings.TrimPrefix(comment, "/**")
|
|
@@ -100,9 +105,16 @@ func todoEntries(comment string) []string {
|
|
|
100
105
|
entries = append(entries, strings.TrimSpace(strings.Join(pending, " ")))
|
|
101
106
|
pending = nil
|
|
102
107
|
}
|
|
108
|
+
fence := commentFence{}
|
|
103
109
|
for _, rawLine := range strings.Split(comment, "\n") {
|
|
104
110
|
line := strings.TrimSpace(rawLine)
|
|
105
111
|
line = strings.TrimSpace(strings.TrimPrefix(line, "*"))
|
|
112
|
+
if fence.consume(line) {
|
|
113
|
+
if pending != nil && line != "" {
|
|
114
|
+
pending = append(pending, line)
|
|
115
|
+
}
|
|
116
|
+
continue
|
|
117
|
+
}
|
|
106
118
|
if remainder, opened := todoLine(line); opened {
|
|
107
119
|
flush()
|
|
108
120
|
pending = []string{remainder}
|