@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/comments.go
CHANGED
|
@@ -29,7 +29,7 @@ type sourceSpan struct {
|
|
|
29
29
|
end int
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
//
|
|
32
|
+
// forEachComment visits every real comment in `file` in source order.
|
|
33
33
|
//
|
|
34
34
|
// TypeScript's parser, rather than a context-free scanner, owns the lexical
|
|
35
35
|
// goal for regular expressions, templates, and JSX. The parsed AST retains
|
package/native/config.go
CHANGED
|
@@ -663,6 +663,9 @@ func normalizeSwaggerSource(value string) (string, string) {
|
|
|
663
663
|
}
|
|
664
664
|
parsed, err := url.Parse(value)
|
|
665
665
|
if err != nil {
|
|
666
|
+
if displaySwaggerSource(value) != value || strings.Contains(value, "://") {
|
|
667
|
+
return "", "invalid Swagger source '" + displaySwaggerSource(value) + "': the URL could not be parsed."
|
|
668
|
+
}
|
|
666
669
|
return "", "invalid Swagger source '" + value + "': " + causeText(err) + "."
|
|
667
670
|
}
|
|
668
671
|
if !drive && parsed.Scheme != "" {
|
|
@@ -670,15 +673,15 @@ func normalizeSwaggerSource(value string) (string, string) {
|
|
|
670
673
|
return "", "unsupported URL scheme '" + parsed.Scheme + "'; only http: and https: are supported."
|
|
671
674
|
}
|
|
672
675
|
if parsed.Host == "" {
|
|
673
|
-
return "", "Swagger URL '" + value + "' has no host."
|
|
676
|
+
return "", "Swagger URL '" + displaySwaggerSource(value) + "' has no host."
|
|
674
677
|
}
|
|
675
678
|
if parsed.Fragment != "" {
|
|
676
|
-
return "", "Swagger URL '" + value + "' must not contain a fragment."
|
|
679
|
+
return "", "Swagger URL '" + displaySwaggerSource(value) + "' must not contain a fragment."
|
|
677
680
|
}
|
|
678
681
|
return value, ""
|
|
679
682
|
}
|
|
680
683
|
if !drive && strings.Contains(value, "://") {
|
|
681
|
-
return "", "invalid Swagger source URL '" + value + "'."
|
|
684
|
+
return "", "invalid Swagger source URL '" + displaySwaggerSource(value) + "'."
|
|
682
685
|
}
|
|
683
686
|
// A trailing separator is read before cleaning, because `path.Clean` removes
|
|
684
687
|
// it — after which `docs/` and `docs` are one string and the author who
|
package/native/declaration.go
CHANGED
|
@@ -31,18 +31,12 @@ var hiddenDeclarationTags = []string{"@internal", "@hidden", "@ignore"}
|
|
|
31
31
|
// sentence silently delete an obligation. Text after the tag is a comment for
|
|
32
32
|
// humans and is not read.
|
|
33
33
|
func commentHidingTag(comment string) string {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
comment = strings.TrimSuffix(comment, "*/")
|
|
38
|
-
fence := commentFence{}
|
|
39
|
-
for _, rawLine := range strings.Split(comment, "\n") {
|
|
40
|
-
line := strings.TrimSpace(rawLine)
|
|
41
|
-
line = strings.TrimSpace(strings.TrimPrefix(line, "*"))
|
|
42
|
-
line = strings.TrimSpace(strings.TrimPrefix(line, "///"))
|
|
43
|
-
if fence.consume(line) {
|
|
34
|
+
body, _, _ := documentationCommentBody(comment)
|
|
35
|
+
for _, region := range documentationRegions(body, true) {
|
|
36
|
+
if region.fence {
|
|
44
37
|
continue
|
|
45
38
|
}
|
|
39
|
+
line := region.text
|
|
46
40
|
for _, tag := range hiddenDeclarationTags {
|
|
47
41
|
if line != tag && !strings.HasPrefix(line, tag+" ") {
|
|
48
42
|
continue
|
|
@@ -65,21 +59,14 @@ func parseCommentDeclarations(
|
|
|
65
59
|
comment string,
|
|
66
60
|
tagBoundaries bool,
|
|
67
61
|
) []parsedDeclaration {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
comment = trimmed
|
|
71
|
-
jsdoc := strings.HasPrefix(comment, "/**") || tagBoundaries
|
|
72
|
-
comment = strings.TrimPrefix(comment, "/**")
|
|
73
|
-
comment = strings.TrimPrefix(comment, "/*")
|
|
74
|
-
comment = strings.TrimSuffix(comment, "*/")
|
|
75
|
-
lines := strings.Split(comment, "\n")
|
|
62
|
+
body, leadingLines, documented := documentationCommentBody(comment)
|
|
63
|
+
jsdoc := documented || tagBoundaries
|
|
76
64
|
type pendingDeclaration struct {
|
|
77
65
|
tag tagKind
|
|
78
66
|
body []string
|
|
79
67
|
lineOffset int
|
|
80
68
|
}
|
|
81
69
|
var pending *pendingDeclaration
|
|
82
|
-
fence := commentFence{}
|
|
83
70
|
parsed := []parsedDeclaration{}
|
|
84
71
|
flush := func() {
|
|
85
72
|
if pending == nil {
|
|
@@ -94,15 +81,12 @@ func parseCommentDeclarations(
|
|
|
94
81
|
})
|
|
95
82
|
pending = nil
|
|
96
83
|
}
|
|
97
|
-
for index,
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if fence.consume(line) {
|
|
101
|
-
if pending != nil {
|
|
102
|
-
pending.body = append(pending.body, line)
|
|
103
|
-
}
|
|
84
|
+
for index, region := range documentationRegions(body, false) {
|
|
85
|
+
if region.fence {
|
|
86
|
+
flush()
|
|
104
87
|
continue
|
|
105
88
|
}
|
|
89
|
+
line := region.text
|
|
106
90
|
tag, body, found := declarationLine(line)
|
|
107
91
|
if found {
|
|
108
92
|
flush()
|
|
@@ -329,15 +313,9 @@ func (review parsedReview) marker() string {
|
|
|
329
313
|
// other `@`-opening line closes the review above it, so a review sharing a
|
|
330
314
|
// block with `@param` or a following `@evidence` swallows neither.
|
|
331
315
|
func parseReviews(comment string) []parsedReview {
|
|
332
|
-
|
|
333
|
-
leadingLines := strings.Count(comment[:len(comment)-len(trimmed)], "\n")
|
|
334
|
-
comment = trimmed
|
|
335
|
-
comment = strings.TrimPrefix(comment, "/**")
|
|
336
|
-
comment = strings.TrimPrefix(comment, "/*")
|
|
337
|
-
comment = strings.TrimSuffix(comment, "*/")
|
|
316
|
+
comment, leadingLines, _ := documentationCommentBody(comment)
|
|
338
317
|
reviews := []parsedReview{}
|
|
339
318
|
var pending *parsedReview
|
|
340
|
-
fence := commentFence{}
|
|
341
319
|
var body []string
|
|
342
320
|
flush := func() {
|
|
343
321
|
if pending == nil {
|
|
@@ -352,16 +330,12 @@ func parseReviews(comment string) []parsedReview {
|
|
|
352
330
|
pending = nil
|
|
353
331
|
body = nil
|
|
354
332
|
}
|
|
355
|
-
for index,
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
line = strings.TrimSpace(strings.TrimPrefix(line, "///"))
|
|
359
|
-
if fence.consume(line) {
|
|
360
|
-
if pending != nil {
|
|
361
|
-
body = append(body, line)
|
|
362
|
-
}
|
|
333
|
+
for index, region := range documentationRegions(comment, true) {
|
|
334
|
+
if region.fence {
|
|
335
|
+
flush()
|
|
363
336
|
continue
|
|
364
337
|
}
|
|
338
|
+
line := region.text
|
|
365
339
|
if reviews, remainder, opened := reviewLine(line); opened {
|
|
366
340
|
flush()
|
|
367
341
|
pending = &parsedReview{
|
|
@@ -471,8 +445,8 @@ func containsWhitespace(value string) bool {
|
|
|
471
445
|
return false
|
|
472
446
|
}
|
|
473
447
|
|
|
474
|
-
//
|
|
475
|
-
//
|
|
448
|
+
// commentFence recognizes example delimiters. Its callers decide whether a
|
|
449
|
+
// recognized example terminates annotation prose or belongs to a TODO's text.
|
|
476
450
|
type commentFence struct {
|
|
477
451
|
marker rune
|
|
478
452
|
length int
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
package evidence
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"strings"
|
|
5
|
+
"unicode"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
// documentationRegionLine preserves one source line while separating annotation
|
|
9
|
+
// prose from a fenced example. A fence is also a pending-annotation boundary.
|
|
10
|
+
type documentationRegionLine struct {
|
|
11
|
+
// text is visible annotation prose with the original line slot retained.
|
|
12
|
+
text string
|
|
13
|
+
|
|
14
|
+
// fence ends pending prose and suppresses tags throughout the example.
|
|
15
|
+
fence bool
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// documentationCommentBody removes only a real native wrapper. Mapped Prisma
|
|
19
|
+
// and Markdown bodies have no wrapper, so their initial indentation and blank
|
|
20
|
+
// lines must survive to retain relative columns and diagnostic line offsets.
|
|
21
|
+
func documentationCommentBody(comment string) (string, int, bool) {
|
|
22
|
+
trimmed := strings.TrimLeftFunc(comment, unicode.IsSpace)
|
|
23
|
+
prefix := ""
|
|
24
|
+
jsdoc := strings.HasPrefix(trimmed, "/**")
|
|
25
|
+
if jsdoc {
|
|
26
|
+
prefix = "/**"
|
|
27
|
+
} else if strings.HasPrefix(trimmed, "/*") {
|
|
28
|
+
prefix = "/*"
|
|
29
|
+
}
|
|
30
|
+
if prefix == "" {
|
|
31
|
+
return comment, 0, false
|
|
32
|
+
}
|
|
33
|
+
leading := strings.Count(comment[:len(comment)-len(trimmed)], "\n")
|
|
34
|
+
body := strings.TrimPrefix(trimmed, prefix)
|
|
35
|
+
body = strings.TrimSuffix(strings.TrimRightFunc(body, unicode.IsSpace), "*/")
|
|
36
|
+
return body, leading, jsdoc
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// documentationRegions reads examples after removing comment decoration and
|
|
40
|
+
// ordinary common indentation. It shares region decisions between citations,
|
|
41
|
+
// reviews and withdrawal without merging their distinct semantic records.
|
|
42
|
+
// Fences take precedence over HTML and code spans; HTML contents cannot open a
|
|
43
|
+
// fence. Equal backtick runs protect literal HTML delimiters, including spans
|
|
44
|
+
// crossing a newline. Unmatched code delimiters remain ordinary prose.
|
|
45
|
+
func documentationRegions(body string, slashDecoration bool) []documentationRegionLine {
|
|
46
|
+
lines := strings.Split(body, "\n")
|
|
47
|
+
baseline := -1
|
|
48
|
+
for index, line := range lines {
|
|
49
|
+
line = strings.TrimSuffix(line, "\r")
|
|
50
|
+
stripped := strings.TrimLeft(line, " \t")
|
|
51
|
+
if strings.HasPrefix(stripped, "*") {
|
|
52
|
+
line = strings.TrimPrefix(stripped[1:], " ")
|
|
53
|
+
} else if slashDecoration && strings.HasPrefix(stripped, "///") {
|
|
54
|
+
line = strings.TrimPrefix(stripped[3:], " ")
|
|
55
|
+
}
|
|
56
|
+
lines[index] = line
|
|
57
|
+
if strings.TrimSpace(line) != "" {
|
|
58
|
+
columns, _ := documentationColumns(line)
|
|
59
|
+
if baseline < 0 || columns < baseline {
|
|
60
|
+
baseline = columns
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
content := strings.Join(lines, "\n")
|
|
65
|
+
mates := documentationCodeMates(content)
|
|
66
|
+
result := make([]documentationRegionLine, len(lines))
|
|
67
|
+
fence := commentFence{}
|
|
68
|
+
html := false
|
|
69
|
+
codeEnd := 0
|
|
70
|
+
tagEnd := 0
|
|
71
|
+
offset := 0
|
|
72
|
+
for index, line := range lines {
|
|
73
|
+
columns, cursor := documentationColumns(line)
|
|
74
|
+
relative := columns - baseline
|
|
75
|
+
delimiter := " " + line[cursor:]
|
|
76
|
+
if relative >= 0 && relative <= 3 {
|
|
77
|
+
delimiter = strings.Repeat(" ", relative) + line[cursor:]
|
|
78
|
+
}
|
|
79
|
+
if !html && fence.consume(delimiter) {
|
|
80
|
+
result[index].fence = true
|
|
81
|
+
codeEnd, tagEnd = 0, 0
|
|
82
|
+
offset += len(line) + 1
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
85
|
+
masked := []byte(line)
|
|
86
|
+
for at := 0; at < len(line); {
|
|
87
|
+
if html {
|
|
88
|
+
end := len(line)
|
|
89
|
+
if closing := strings.Index(line[at:], "-->"); closing >= 0 {
|
|
90
|
+
end = at + closing + 3
|
|
91
|
+
html = false
|
|
92
|
+
}
|
|
93
|
+
for position := at; position < end; position++ {
|
|
94
|
+
masked[position] = ' '
|
|
95
|
+
}
|
|
96
|
+
at = end
|
|
97
|
+
continue
|
|
98
|
+
}
|
|
99
|
+
if codeEnd > offset+at || tagEnd > offset+at {
|
|
100
|
+
at++
|
|
101
|
+
continue
|
|
102
|
+
}
|
|
103
|
+
if relative < 4 {
|
|
104
|
+
if line[at] == '\\' && at+1 < len(line) {
|
|
105
|
+
at += 2
|
|
106
|
+
continue
|
|
107
|
+
}
|
|
108
|
+
if end, ok := mates[offset+at]; ok {
|
|
109
|
+
codeEnd = end
|
|
110
|
+
at++
|
|
111
|
+
continue
|
|
112
|
+
}
|
|
113
|
+
if strings.HasPrefix(line[at:], "<!--") {
|
|
114
|
+
html = true
|
|
115
|
+
// Empty HTML comments may close with an overlapping final dash.
|
|
116
|
+
if strings.HasPrefix(line[at:], "<!-->") {
|
|
117
|
+
for position := at; position < at+5; position++ {
|
|
118
|
+
masked[position] = ' '
|
|
119
|
+
}
|
|
120
|
+
at += 5
|
|
121
|
+
html = false
|
|
122
|
+
} else if strings.HasPrefix(line[at:], "<!--->") {
|
|
123
|
+
for position := at; position < at+6; position++ {
|
|
124
|
+
masked[position] = ' '
|
|
125
|
+
}
|
|
126
|
+
at += 6
|
|
127
|
+
html = false
|
|
128
|
+
}
|
|
129
|
+
continue
|
|
130
|
+
}
|
|
131
|
+
if line[at] == '<' {
|
|
132
|
+
if end := documentationTagEnd(content, offset+at); end > offset+at {
|
|
133
|
+
tagEnd = end
|
|
134
|
+
at++
|
|
135
|
+
continue
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
at++
|
|
140
|
+
}
|
|
141
|
+
result[index].text = strings.TrimSpace(string(masked))
|
|
142
|
+
offset += len(line) + 1
|
|
143
|
+
}
|
|
144
|
+
return result
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// documentationCodeMates indexes the next equal backtick run in one linear
|
|
148
|
+
// reverse pass. A run without a closing mate must not hide later HTML syntax.
|
|
149
|
+
func documentationCodeMates(content string) map[int]int {
|
|
150
|
+
mates := map[int]int{}
|
|
151
|
+
next := map[int]int{}
|
|
152
|
+
for end := len(content); end > 0; {
|
|
153
|
+
end--
|
|
154
|
+
if content[end] != '`' {
|
|
155
|
+
continue
|
|
156
|
+
}
|
|
157
|
+
start := end
|
|
158
|
+
for start > 0 && content[start-1] == '`' {
|
|
159
|
+
start--
|
|
160
|
+
}
|
|
161
|
+
// Escapes apply to opening prose, not to a closing run inside code. The
|
|
162
|
+
// forward reader skips escaped openers before consulting this mate index.
|
|
163
|
+
length := end - start + 1
|
|
164
|
+
if closing, ok := next[length]; ok {
|
|
165
|
+
mates[start] = closing + length
|
|
166
|
+
}
|
|
167
|
+
next[length] = start
|
|
168
|
+
end = start
|
|
169
|
+
}
|
|
170
|
+
return mates
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// documentationTagEnd keeps quoted attribute delimiters literal without adding
|
|
174
|
+
// an element-specific policy such as treating every pre element as an example.
|
|
175
|
+
func documentationTagEnd(line string, start int) int {
|
|
176
|
+
cursor := start + 1
|
|
177
|
+
if cursor < len(line) && line[cursor] == '/' {
|
|
178
|
+
cursor++
|
|
179
|
+
}
|
|
180
|
+
if cursor >= len(line) || !((line[cursor] >= 'a' && line[cursor] <= 'z') || (line[cursor] >= 'A' && line[cursor] <= 'Z')) {
|
|
181
|
+
return start
|
|
182
|
+
}
|
|
183
|
+
quote := byte(0)
|
|
184
|
+
for cursor++; cursor < len(line); cursor++ {
|
|
185
|
+
char := line[cursor]
|
|
186
|
+
if quote != 0 {
|
|
187
|
+
if char == quote {
|
|
188
|
+
quote = 0
|
|
189
|
+
}
|
|
190
|
+
} else if char == '\'' || char == '"' {
|
|
191
|
+
quote = char
|
|
192
|
+
} else if char == '>' {
|
|
193
|
+
return cursor + 1
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return len(line)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// documentationColumns measures four-column tab stops while retaining the byte
|
|
200
|
+
// position where content starts. Fence grammar consumes the relative columns.
|
|
201
|
+
func documentationColumns(line string) (int, int) {
|
|
202
|
+
columns, cursor := 0, 0
|
|
203
|
+
for cursor < len(line) {
|
|
204
|
+
if line[cursor] == ' ' {
|
|
205
|
+
columns++
|
|
206
|
+
} else if line[cursor] == '\t' {
|
|
207
|
+
columns += 4 - columns%4
|
|
208
|
+
} else {
|
|
209
|
+
break
|
|
210
|
+
}
|
|
211
|
+
cursor++
|
|
212
|
+
}
|
|
213
|
+
return columns, cursor
|
|
214
|
+
}
|
package/native/documented.go
CHANGED
|
@@ -14,10 +14,11 @@ import (
|
|
|
14
14
|
//
|
|
15
15
|
// Evidence declarations are read from one place only — the JSDoc blocks a node
|
|
16
16
|
// reports — so an export without a block is not merely undocumented, it is
|
|
17
|
-
// structurally unable to carry a citation.
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
17
|
+
// structurally unable to carry a citation. Ordinary graph coverage is counted
|
|
18
|
+
// from the evidence side, so another host can discharge the obligation while
|
|
19
|
+
// an undocumented export contributes nothing. A checklist instead keeps every
|
|
20
|
+
// selected host's obligation, including hosts without a block. This rule checks
|
|
21
|
+
// the documentation itself in either mode.
|
|
21
22
|
type documentedRule struct{}
|
|
22
23
|
|
|
23
24
|
type documentedOptions struct {
|
package/native/exports.go
CHANGED
|
@@ -214,7 +214,6 @@ func dedupeModuleExports(exports []moduleExport) []moduleExport {
|
|
|
214
214
|
return unique
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
// reachedSymbol is one public address an entry traversal arrived at.
|
|
218
217
|
// traversedPopulation is what selecting modules and walking their exports
|
|
219
218
|
// yields: the obligations, the scopes above them, and the addresses that reach
|
|
220
219
|
// them.
|
|
@@ -243,6 +242,7 @@ type publishedAddress struct {
|
|
|
243
242
|
Unit *evidenceUnit
|
|
244
243
|
}
|
|
245
244
|
|
|
245
|
+
// reachedSymbol is one public address an entry traversal arrived at.
|
|
246
246
|
type reachedSymbol struct {
|
|
247
247
|
// Address is the accessor path from the entry, segment by segment.
|
|
248
248
|
Address []string
|
package/native/fingerprint.go
CHANGED
|
@@ -31,14 +31,8 @@ func normalizeFingerprintText(text string) string {
|
|
|
31
31
|
for len(lines) != 0 && lines[len(lines)-1] == "" {
|
|
32
32
|
lines = lines[:len(lines)-1]
|
|
33
33
|
}
|
|
34
|
-
// Leading
|
|
35
|
-
//
|
|
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.
|
|
34
|
+
// Leading whitespace remains artifact content. TypeScript declaration spans
|
|
35
|
+
// discard their unowned leading trivia separately in withoutLeadingTrivia.
|
|
42
36
|
return strings.Join(lines, "\n")
|
|
43
37
|
}
|
|
44
38
|
|
package/native/glob.go
CHANGED
|
@@ -49,7 +49,10 @@ func compileGlob(raw string) (globPattern, error) {
|
|
|
49
49
|
value = strings.TrimPrefix(value, "./")
|
|
50
50
|
}
|
|
51
51
|
if value == "" {
|
|
52
|
-
|
|
52
|
+
if exclude && strings.TrimPrefix(raw, "!") == "" {
|
|
53
|
+
return globPattern{}, errors.New("the exclusion marker '!' must be followed by a glob")
|
|
54
|
+
}
|
|
55
|
+
return globPattern{}, errors.New("glob '" + raw + "' names no path below the project root")
|
|
53
56
|
}
|
|
54
57
|
if filepath.IsAbs(value) ||
|
|
55
58
|
hasWindowsDrivePrefix(value) ||
|
package/native/graph.go
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
package evidence
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
-
"os"
|
|
5
4
|
"path/filepath"
|
|
6
5
|
"sort"
|
|
7
6
|
"strings"
|
|
@@ -13,8 +12,25 @@ type graphRule struct{}
|
|
|
13
12
|
|
|
14
13
|
func (graphRule) Name() string { return graphRuleName }
|
|
15
14
|
|
|
15
|
+
// UsesProjectInputReader declares the actual external-input read boundary.
|
|
16
|
+
// Native readers use the supplied generation reader; external bridges withdraw
|
|
17
|
+
// completeness because their independent module/input reads are not observed.
|
|
18
|
+
func (graphRule) UsesProjectInputReader() bool { return true }
|
|
19
|
+
|
|
16
20
|
func (graphRule) NeedsTypeChecker() bool { return false }
|
|
17
21
|
|
|
22
|
+
// Check builds artifact populations and checks each active claim's obligations.
|
|
23
|
+
//
|
|
24
|
+
// Claim populations are loaded first because a healthy empty population owes
|
|
25
|
+
// nothing; its references must not trigger unrelated reads or failures. Failed
|
|
26
|
+
// populations remain active, and their health prevents partial inventories from
|
|
27
|
+
// producing a false coverage result. TypeScript identities come from parsed
|
|
28
|
+
// declarations, while Markdown, Prisma and Swagger retain their own hierarchies.
|
|
29
|
+
//
|
|
30
|
+
// Inventory construction, address resolution and obligation evaluation are
|
|
31
|
+
// separate stages so selectors never invent declarations or decide identity.
|
|
32
|
+
// Successful publication reuses those inventories for editor hints; diagnostics
|
|
33
|
+
// and Program-cycle coordination remain owned by the host's project-rule API.
|
|
18
34
|
func (graphRule) Check(ctx *rule.ProjectContext) {
|
|
19
35
|
if ctx == nil {
|
|
20
36
|
return
|
|
@@ -31,6 +47,7 @@ func (graphRule) Check(ctx *rule.ProjectContext) {
|
|
|
31
47
|
reportProblems(ctx, problems)
|
|
32
48
|
return
|
|
33
49
|
}
|
|
50
|
+
config.inputs = evidenceInputReader{host: ctx.Inputs}
|
|
34
51
|
resolveGraphSeverities(&config, ctx.Severity)
|
|
35
52
|
config = enabledGraphConfig(config)
|
|
36
53
|
if len(config.Claims) == 0 {
|
|
@@ -42,7 +59,7 @@ func (graphRule) Check(ctx *rule.ProjectContext) {
|
|
|
42
59
|
ctx.Report("Evidence graph could not resolve the project root. Run ttsc with a project config or explicit project root so project-relative evidence globs have one stable base.")
|
|
43
60
|
return
|
|
44
61
|
}
|
|
45
|
-
info, err :=
|
|
62
|
+
info, err := config.inputs.Stat(root)
|
|
46
63
|
if err != nil || !info.IsDir() {
|
|
47
64
|
ctx.Report("Evidence graph project root '" + root + "' is not a readable directory. Fix the ttsc project identity before evaluating evidence globs.")
|
|
48
65
|
return
|
|
@@ -62,9 +79,11 @@ func (graphRule) Check(ctx *rule.ProjectContext) {
|
|
|
62
79
|
ctx.Sources,
|
|
63
80
|
claimPopulationConfig(config, artifactTypeScript),
|
|
64
81
|
)
|
|
82
|
+
markdownSnapshot := &markdownCapture{}
|
|
65
83
|
markdownClaims, markdownClaimProblems := loadMarkdownInventories(
|
|
66
84
|
root,
|
|
67
85
|
claimPopulationConfig(config, artifactMarkdown),
|
|
86
|
+
markdownSnapshot,
|
|
68
87
|
)
|
|
69
88
|
prismaClaims, prismaClaimProblems := loadPrismaInventories(
|
|
70
89
|
root,
|
|
@@ -89,15 +108,15 @@ func (graphRule) Check(ctx *rule.ProjectContext) {
|
|
|
89
108
|
config = activeGraphConfig(config, markdownClaims, prismaClaims, typescript)
|
|
90
109
|
governed := map[string]bool{}
|
|
91
110
|
extendTypeScriptInventories(root, ctx.Sources, config, typescript, nil)
|
|
92
|
-
recordGovernedTypeScriptFiles(ctx.Sources, declared, governed)
|
|
93
|
-
markdown, markdownProblems := loadMarkdownInventories(root, config)
|
|
111
|
+
recordGovernedTypeScriptFiles(root, ctx.Sources, declared, governed)
|
|
112
|
+
markdown, markdownProblems := loadMarkdownInventories(root, config, markdownSnapshot)
|
|
94
113
|
prisma, prismaProblems := loadPrismaInventories(root, config)
|
|
95
114
|
swagger, swaggerProblems := loadSwaggerInventories(root, config)
|
|
96
115
|
diagnostics = append(diagnostics, markdownProblems...)
|
|
97
116
|
diagnostics = append(diagnostics, prismaProblems...)
|
|
98
117
|
diagnostics = append(diagnostics, swaggerProblems...)
|
|
99
118
|
diagnostics = append(diagnostics, unreadableTypeScriptTags(typescript, governed, declared)...)
|
|
100
|
-
loader := newTypeScriptLoader(root, typescript)
|
|
119
|
+
loader := newTypeScriptLoader(root, typescript, config.inputs)
|
|
101
120
|
states, stateProblems := materializeClaimStates(
|
|
102
121
|
config,
|
|
103
122
|
markdown,
|
|
@@ -112,7 +131,9 @@ func (graphRule) Check(ctx *rule.ProjectContext) {
|
|
|
112
131
|
if len(diagnostics) == 0 {
|
|
113
132
|
// Published only on a clean evaluation, because the host reads state
|
|
114
133
|
// from a rule that passed and reporting anything marks this one failed
|
|
115
|
-
// (`
|
|
134
|
+
// (`collectProjectHints` in `linthost/hints.go` skips a rule whose status is
|
|
135
|
+
// not passed, and `projectReporter` in `linthost/project_engine.go` marks a
|
|
136
|
+
// rule failed once it reports).
|
|
116
137
|
// Setting it unconditionally would not widen the gate; it would only
|
|
117
138
|
// hide where the gate is.
|
|
118
139
|
cycle.Corpus = graphCorpus{
|
|
@@ -164,7 +185,7 @@ func claimPopulationConfig(
|
|
|
164
185
|
claim.References = nil
|
|
165
186
|
claims = append(claims, claim)
|
|
166
187
|
}
|
|
167
|
-
return graphConfig{Claims: claims}
|
|
188
|
+
return graphConfig{Claims: claims, inputs: config.inputs}
|
|
168
189
|
}
|
|
169
190
|
|
|
170
191
|
// activeGraphConfig omits healthy claim populations that contain no own unit
|
|
@@ -660,14 +681,13 @@ func evaluateEvidenceGraph(
|
|
|
660
681
|
continue
|
|
661
682
|
}
|
|
662
683
|
candidates := declarationCandidates(declaration.Target, targets, markdownTargets)
|
|
663
|
-
// The
|
|
664
|
-
//
|
|
665
|
-
//
|
|
666
|
-
//
|
|
667
|
-
//
|
|
668
|
-
//
|
|
669
|
-
//
|
|
670
|
-
// that are addressed by path, where a shared map costs nothing.
|
|
684
|
+
// The address map is built from every claim at once, so a claim that
|
|
685
|
+
// addresses code only through a file-qualified link, such as a Markdown
|
|
686
|
+
// claim, could still land on a symbol materialized by some *other* claim's
|
|
687
|
+
// TypeScript reference through a plain token. That would resolve silently
|
|
688
|
+
// and make repository-wide name uniqueness load-bearing. Refusing it here
|
|
689
|
+
// rather than scoping the whole map keeps resolution global for the
|
|
690
|
+
// artifacts that are addressed by path, where a shared map costs nothing.
|
|
671
691
|
if declaration.Type != artifactTypeScript {
|
|
672
692
|
addressable, code := splitCodeCandidates(candidates)
|
|
673
693
|
if len(addressable) == 0 && len(code) != 0 {
|
|
@@ -937,9 +957,16 @@ func evaluateEvidenceGraph(
|
|
|
937
957
|
for _, unit := range covered {
|
|
938
958
|
targets = append(targets, "'"+unit.Target+"'")
|
|
939
959
|
}
|
|
960
|
+
// The exclusion alternative is offered only where this reference
|
|
961
|
+
// accepts one: a checklist is the pairing `noEvidenceExclude` is allowed
|
|
962
|
+
// beside, and there the exclusion would be refused as forbidden.
|
|
963
|
+
repair := "Cite each item this host answers for, or write @evidenceExclude on this scope when none of it applies here."
|
|
964
|
+
if reference.Spec.Policy.NoExclude {
|
|
965
|
+
repair = "Cite each item this host answers for; this reference forbids @evidenceExclude."
|
|
966
|
+
}
|
|
940
967
|
problems = problems.add(
|
|
941
968
|
severity,
|
|
942
|
-
"Aggregate @evidence target '"+scopesByID[scopeID].Target+"' at "+declaration.location()+" in "+claimLabel(state.Spec)+" "+referenceLabel(reference.Spec)+": this reference is a checklist, so a citation answers for the item it names, and this target names a scope containing "+decimal(len(covered))+" item(s) ("+strings.Join(targets, ", ")+") rather than one of them.
|
|
969
|
+
"Aggregate @evidence target '"+scopesByID[scopeID].Target+"' at "+declaration.location()+" in "+claimLabel(state.Spec)+" "+referenceLabel(reference.Spec)+": this reference is a checklist, so a citation answers for the item it names, and this target names a scope containing "+decimal(len(covered))+" item(s) ("+strings.Join(targets, ", ")+") rather than one of them. "+repair+untrueTagWarning,
|
|
943
970
|
)
|
|
944
971
|
for _, unit := range covered {
|
|
945
972
|
for _, hostID := range keyHosts {
|
|
@@ -976,7 +1003,7 @@ func evaluateEvidenceGraph(
|
|
|
976
1003
|
if first := byScope[scopeID]; first != nil {
|
|
977
1004
|
problems = problems.add(
|
|
978
1005
|
severity,
|
|
979
|
-
"Duplicate @evidence for '"+scopesByID[scopeID].Target+"' on the same host at "+declaration.location()+"; first declared at "+first.location()+".",
|
|
1006
|
+
"Duplicate @evidence for '"+scopesByID[scopeID].Target+"' on the same host at "+declaration.location()+"; first declared at "+first.location()+". Delete the repeated citation.",
|
|
980
1007
|
)
|
|
981
1008
|
} else {
|
|
982
1009
|
byScope[scopeID] = declaration
|
|
@@ -1084,13 +1111,13 @@ func evaluateEvidenceGraph(
|
|
|
1084
1111
|
}
|
|
1085
1112
|
problems = problems.add(
|
|
1086
1113
|
severity,
|
|
1087
|
-
"Conflicting acknowledgements for '"+conflictingUnit.Target+"' in "+claimLabel(state.Spec)+" "+referenceLabel(reference.Spec)+": @evidence at "+evidence.location()+" overlaps @evidenceExclude at "+exclusion.location()+". Delete whichever
|
|
1114
|
+
"Conflicting acknowledgements for '"+conflictingUnit.Target+"' in "+claimLabel(state.Spec)+" "+referenceLabel(reference.Spec)+": @evidence at "+evidence.location()+" overlaps @evidenceExclude at "+exclusion.location()+". Delete whichever of the two is untrue."+untrueTagWarning,
|
|
1088
1115
|
)
|
|
1089
1116
|
}
|
|
1090
1117
|
if duplicateExclusionUnit != nil {
|
|
1091
1118
|
problems = problems.add(
|
|
1092
1119
|
severity,
|
|
1093
|
-
"Duplicate @evidenceExclude for '"+duplicateExclusionUnit.Target+"' in "+claimLabel(state.Spec)+" "+referenceLabel(reference.Spec)+": exclusion at "+declaration.location()+" overlaps exclusion at "+firstExclusion.location()+".",
|
|
1120
|
+
"Duplicate @evidenceExclude for '"+duplicateExclusionUnit.Target+"' in "+claimLabel(state.Spec)+" "+referenceLabel(reference.Spec)+": exclusion at "+declaration.location()+" overlaps exclusion at "+firstExclusion.location()+". Delete one of the two exclusions.",
|
|
1094
1121
|
)
|
|
1095
1122
|
}
|
|
1096
1123
|
}
|
|
@@ -1108,7 +1135,7 @@ func evaluateEvidenceGraph(
|
|
|
1108
1135
|
}
|
|
1109
1136
|
problems = problems.add(
|
|
1110
1137
|
severity,
|
|
1111
|
-
"Evidence host "+host.Readable+" at "+host.location()+" in "+claimLabel(state.Spec)+" "+referenceLabel(reference.Spec)+" cites "+decimal(count)+" distinct selected evidence unit(s); singleEvidencePerSymbol requires exactly 1.
|
|
1138
|
+
"Evidence host "+host.Readable+" at "+host.location()+" in "+claimLabel(state.Spec)+" "+referenceLabel(reference.Spec)+" cites "+decimal(count)+" distinct selected evidence unit(s); singleEvidencePerSymbol requires exactly 1. Keep positive @evidence citations on this semantic host to exactly one distinct unit: cite the one unit it answers for, or split the host so that each unit has a host of its own."+untrueTagWarning,
|
|
1112
1139
|
)
|
|
1113
1140
|
}
|
|
1114
1141
|
}
|
|
@@ -1206,9 +1233,18 @@ func evaluateEvidenceGraph(
|
|
|
1206
1233
|
host = "unsupported or non-exported declaration"
|
|
1207
1234
|
}
|
|
1208
1235
|
if declaration.Tag == tagExclude {
|
|
1236
|
+
// The carriers differ by artifact kind, so the repair names only the ones
|
|
1237
|
+
// this kind has: an unattached run exists in Prisma schemas alone.
|
|
1238
|
+
repair := "Move the exclusion onto a selected host of the claim."
|
|
1239
|
+
switch declaration.Type {
|
|
1240
|
+
case artifactTypeScript:
|
|
1241
|
+
repair = "Move the exclusion onto a supported public export in a matching claim file."
|
|
1242
|
+
case artifactPrisma:
|
|
1243
|
+
repair = "Move the exclusion onto a selected model, column, or relation, or into a top-level unattached '///' run in a matching claim file."
|
|
1244
|
+
}
|
|
1209
1245
|
problems = problems.add(
|
|
1210
1246
|
outOfScopeLevels[id],
|
|
1211
|
-
"Out-of-scope @evidenceExclude carrier at "+declaration.location()+" for "+strings.Join(obligations, "; ")+", target '"+displayTarget(declaration.Target)+"': '"+host+"' is not an eligible exclusion carrier in these matching claim files.
|
|
1247
|
+
"Out-of-scope @evidenceExclude carrier at "+declaration.location()+" for "+strings.Join(obligations, "; ")+", target '"+displayTarget(declaration.Target)+"': '"+host+"' is not an eligible exclusion carrier in these matching claim files. "+repair+untrueTagWarning,
|
|
1212
1248
|
)
|
|
1213
1249
|
continue
|
|
1214
1250
|
}
|
|
@@ -1421,7 +1457,7 @@ func hiddenTargetProblem(
|
|
|
1421
1457
|
hidden *evidenceUnit,
|
|
1422
1458
|
context string,
|
|
1423
1459
|
) string {
|
|
1424
|
-
return "Hidden evidence target '" + displayTarget(declaration.Target) + "' at " + declaration.location() + " for " + context + ": " + hidden.Readable + " at " + hidden.location() + " carries '" + hidden.Hidden + "' in its documentation comment, which removes it from the evidence population along with everything nested inside it. Remove the tag if the declaration is public contract, or drop this citation if it is not. Do not remove '
|
|
1460
|
+
return "Hidden evidence target '" + displayTarget(declaration.Target) + "' at " + declaration.location() + " for " + context + ": " + hidden.Readable + " at " + hidden.location() + " carries '" + hidden.Hidden + "' in its documentation comment, which removes it from the evidence population along with everything nested inside it. Remove the tag if the declaration is public contract, or drop this citation if it is not. Do not remove '" + hidden.Hidden + "' from the target to make this citation legal." + untrueTagWarning
|
|
1425
1461
|
}
|
|
1426
1462
|
|
|
1427
1463
|
func declarationResolutionUncertain(owners []claimState) bool {
|
|
@@ -1659,7 +1695,7 @@ func resolveReferenceEntry(
|
|
|
1659
1695
|
) (string, string) {
|
|
1660
1696
|
entry := loader.packageEntryModule(reference.Package)
|
|
1661
1697
|
if entry == "" {
|
|
1662
|
-
return "", claimLabel(claim) + " " + referenceLabel(reference) + " could not resolve the declaration entry of package '" + reference.Package + "'. Install it, or select its declarations with 'files'; the entry is read from the 'types' condition of 'exports', then 'typesVersions', then 'types'."
|
|
1698
|
+
return "", claimLabel(claim) + " " + referenceLabel(reference) + " could not resolve the declaration entry of package '" + reference.Package + "'. Install it, or select its declarations with 'files'; the entry is read from the 'types' condition of 'exports', then 'typesVersions', then 'types' or 'typings'. A package with none of those resolves only when its 'exports' target or 'main' names TypeScript source itself."
|
|
1663
1699
|
}
|
|
1664
1700
|
return entry, ""
|
|
1665
1701
|
}
|
package/native/graph_nodes.go
CHANGED
|
@@ -30,7 +30,8 @@ func (graphRule) GraphNodes(ctx *rule.GraphContext) []rule.GraphNode {
|
|
|
30
30
|
corpus := cycle.Corpus
|
|
31
31
|
// Addresses of every materialized unit, so a parent can be named by the
|
|
32
32
|
// address a citation would use rather than by the rule's internal id. A
|
|
33
|
-
// parent outside the selected set
|
|
33
|
+
// parent outside the selected set is still named by its address here, and the
|
|
34
|
+
// host clears a parent that no published node carries (`linthost/graph_nodes.go`),
|
|
34
35
|
// which leaves the child at the top of its chain instead of inventing one.
|
|
35
36
|
addresses := map[string]string{}
|
|
36
37
|
for _, inventories := range []map[string]*artifactInventory{
|