@ttsc/evidence 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +279 -0
- package/lib/index.d.ts +65 -0
- package/lib/index.js +87 -0
- package/lib/internal/loadPrismaModels.d.ts +1 -0
- package/lib/internal/loadPrismaModels.js +197 -0
- package/lib/internal/loadSwaggerOperations.d.ts +1 -0
- package/lib/internal/loadSwaggerOperations.js +156 -0
- package/lib/internal/normalizeSwaggerDocument.d.ts +1 -0
- package/lib/internal/normalizeSwaggerDocument.js +7 -0
- package/lib/structures/ITtscEvidenceDocumentedConfig.d.ts +32 -0
- package/lib/structures/ITtscEvidenceDocumentedConfig.js +2 -0
- package/lib/structures/ITtscEvidenceGraphClaim.d.ts +14 -0
- package/lib/structures/ITtscEvidenceGraphClaim.js +2 -0
- package/lib/structures/ITtscEvidenceGraphClaimBase.d.ts +102 -0
- package/lib/structures/ITtscEvidenceGraphClaimBase.js +2 -0
- package/lib/structures/ITtscEvidenceGraphConfig.d.ts +19 -0
- package/lib/structures/ITtscEvidenceGraphConfig.js +2 -0
- package/lib/structures/ITtscEvidenceGraphMarkdownClaim.d.ts +40 -0
- package/lib/structures/ITtscEvidenceGraphMarkdownClaim.js +2 -0
- package/lib/structures/ITtscEvidenceGraphMarkdownReference.d.ts +76 -0
- package/lib/structures/ITtscEvidenceGraphMarkdownReference.js +2 -0
- package/lib/structures/ITtscEvidenceGraphPrismaClaim.d.ts +65 -0
- package/lib/structures/ITtscEvidenceGraphPrismaClaim.js +2 -0
- package/lib/structures/ITtscEvidenceGraphPrismaReference.d.ts +75 -0
- package/lib/structures/ITtscEvidenceGraphPrismaReference.js +2 -0
- package/lib/structures/ITtscEvidenceGraphReference.d.ts +14 -0
- package/lib/structures/ITtscEvidenceGraphReference.js +2 -0
- package/lib/structures/ITtscEvidenceGraphReferenceBase.d.ts +73 -0
- package/lib/structures/ITtscEvidenceGraphReferenceBase.js +2 -0
- package/lib/structures/ITtscEvidenceGraphSwaggerReference.d.ts +39 -0
- package/lib/structures/ITtscEvidenceGraphSwaggerReference.js +2 -0
- package/lib/structures/ITtscEvidenceGraphTypeScriptClaim.d.ts +54 -0
- package/lib/structures/ITtscEvidenceGraphTypeScriptClaim.js +2 -0
- package/lib/structures/ITtscEvidenceGraphTypeScriptReference.d.ts +83 -0
- package/lib/structures/ITtscEvidenceGraphTypeScriptReference.js +2 -0
- package/lib/structures/index.d.ts +13 -0
- package/lib/structures/index.js +29 -0
- package/lib/typings/ITtscLintContributorRules.d.ts +34 -0
- package/lib/typings/ITtscLintContributorRules.js +2 -0
- package/lib/typings/ITtscLintRuleOptionsMap.d.ts +20 -0
- package/lib/typings/ITtscLintRuleOptionsMap.js +2 -0
- package/lib/typings/TtscEvidenceGraphMarkdownSymbol.d.ts +32 -0
- package/lib/typings/TtscEvidenceGraphMarkdownSymbol.js +2 -0
- package/lib/typings/TtscEvidenceGraphPrismaSymbol.d.ts +38 -0
- package/lib/typings/TtscEvidenceGraphPrismaSymbol.js +2 -0
- package/lib/typings/TtscEvidenceGraphTypeScriptSymbol.d.ts +64 -0
- package/lib/typings/TtscEvidenceGraphTypeScriptSymbol.js +2 -0
- package/lib/typings/index.d.ts +5 -0
- package/lib/typings/index.js +21 -0
- package/native/base.go +298 -0
- package/native/config.go +798 -0
- package/native/declaration.go +240 -0
- package/native/documented.go +353 -0
- package/native/exports.go +484 -0
- package/native/glob.go +207 -0
- package/native/graph.go +1409 -0
- package/native/health.go +45 -0
- package/native/hints.go +261 -0
- package/native/imports.go +373 -0
- package/native/inputs.go +125 -0
- package/native/loader.go +424 -0
- package/native/markdown.go +423 -0
- package/native/model.go +296 -0
- package/native/prisma.go +719 -0
- package/native/prisma_cache.go +213 -0
- package/native/prisma_locator.go +417 -0
- package/native/singular.go +369 -0
- package/native/swagger.go +331 -0
- package/native/swagger_cache.go +274 -0
- package/native/todo.go +138 -0
- package/native/typescript.go +1199 -0
- package/native/typescript_cache.go +96 -0
- package/package.json +64 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
package evidence
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"strings"
|
|
5
|
+
"unicode"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
type parsedDeclaration struct {
|
|
9
|
+
Tag tagKind
|
|
10
|
+
Target string
|
|
11
|
+
Reason string
|
|
12
|
+
LineOffset int
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
func parseDeclarations(comment string) []parsedDeclaration {
|
|
16
|
+
return parseCommentDeclarations(comment, false)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// hiddenDeclarationTags are the documentation tags that withdraw a declaration
|
|
20
|
+
// from the public surface. They are equivalent here: each one is a statement
|
|
21
|
+
// that the declaration is not API, and the graph has no reason to grade the
|
|
22
|
+
// three against each other.
|
|
23
|
+
var hiddenDeclarationTags = []string{"@internal", "@hidden", "@ignore"}
|
|
24
|
+
|
|
25
|
+
// commentHidingTag returns the tag by which a documentation comment withdraws
|
|
26
|
+
// its declaration, or an empty string.
|
|
27
|
+
//
|
|
28
|
+
// The tag has to open its own line, the way every other tag in these comments
|
|
29
|
+
// is recognized. A prose line mentioning `@internal` is describing something,
|
|
30
|
+
// not declaring it, and treating a substring match as a declaration would let a
|
|
31
|
+
// sentence silently delete an obligation. Text after the tag is a comment for
|
|
32
|
+
// humans and is not read.
|
|
33
|
+
func commentHidingTag(comment string) string {
|
|
34
|
+
comment = strings.TrimSpace(comment)
|
|
35
|
+
comment = strings.TrimPrefix(comment, "/**")
|
|
36
|
+
comment = strings.TrimPrefix(comment, "/*")
|
|
37
|
+
comment = strings.TrimSuffix(comment, "*/")
|
|
38
|
+
for _, rawLine := range strings.Split(comment, "\n") {
|
|
39
|
+
line := strings.TrimSpace(rawLine)
|
|
40
|
+
line = strings.TrimSpace(strings.TrimPrefix(line, "*"))
|
|
41
|
+
line = strings.TrimSpace(strings.TrimPrefix(line, "///"))
|
|
42
|
+
for _, tag := range hiddenDeclarationTags {
|
|
43
|
+
if line != tag && !strings.HasPrefix(line, tag+" ") {
|
|
44
|
+
continue
|
|
45
|
+
}
|
|
46
|
+
return tag
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return ""
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// parseCommentDeclarations reads every declaration one comment body carries.
|
|
53
|
+
//
|
|
54
|
+
// tagBoundaries decides whether a line opening with some other `@tag` ends the
|
|
55
|
+
// declaration above it. A JSDoc block sets it by its own syntax, and a Prisma
|
|
56
|
+
// doc comment needs it set for the same reason without carrying that syntax:
|
|
57
|
+
// `///` comments routinely hold `@stance`, `@namespace`, and other tools' tags,
|
|
58
|
+
// and without a boundary the first of them is swallowed into the reason of the
|
|
59
|
+
// citation above it.
|
|
60
|
+
func parseCommentDeclarations(
|
|
61
|
+
comment string,
|
|
62
|
+
tagBoundaries bool,
|
|
63
|
+
) []parsedDeclaration {
|
|
64
|
+
trimmed := strings.TrimLeftFunc(comment, unicode.IsSpace)
|
|
65
|
+
leadingLines := strings.Count(comment[:len(comment)-len(trimmed)], "\n")
|
|
66
|
+
comment = trimmed
|
|
67
|
+
jsdoc := strings.HasPrefix(comment, "/**") || tagBoundaries
|
|
68
|
+
comment = strings.TrimPrefix(comment, "/**")
|
|
69
|
+
comment = strings.TrimPrefix(comment, "/*")
|
|
70
|
+
comment = strings.TrimSuffix(comment, "*/")
|
|
71
|
+
lines := strings.Split(comment, "\n")
|
|
72
|
+
type pendingDeclaration struct {
|
|
73
|
+
tag tagKind
|
|
74
|
+
body []string
|
|
75
|
+
lineOffset int
|
|
76
|
+
}
|
|
77
|
+
var pending *pendingDeclaration
|
|
78
|
+
parsed := []parsedDeclaration{}
|
|
79
|
+
flush := func() {
|
|
80
|
+
if pending == nil {
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
target, reason := splitDeclarationBody(strings.Join(pending.body, "\n"))
|
|
84
|
+
parsed = append(parsed, parsedDeclaration{
|
|
85
|
+
Tag: pending.tag,
|
|
86
|
+
Target: target,
|
|
87
|
+
Reason: reason,
|
|
88
|
+
LineOffset: leadingLines + pending.lineOffset,
|
|
89
|
+
})
|
|
90
|
+
pending = nil
|
|
91
|
+
}
|
|
92
|
+
for index, rawLine := range lines {
|
|
93
|
+
line := strings.TrimSpace(rawLine)
|
|
94
|
+
line = strings.TrimSpace(strings.TrimPrefix(line, "*"))
|
|
95
|
+
tag, body, found := declarationLine(line)
|
|
96
|
+
if found {
|
|
97
|
+
flush()
|
|
98
|
+
pending = &pendingDeclaration{
|
|
99
|
+
tag: tag,
|
|
100
|
+
body: []string{body},
|
|
101
|
+
lineOffset: index,
|
|
102
|
+
}
|
|
103
|
+
continue
|
|
104
|
+
}
|
|
105
|
+
if jsdoc && strings.HasPrefix(line, "@") {
|
|
106
|
+
flush()
|
|
107
|
+
continue
|
|
108
|
+
}
|
|
109
|
+
if pending != nil {
|
|
110
|
+
pending.body = append(pending.body, line)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
flush()
|
|
114
|
+
return parsed
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
func declarationLine(line string) (tagKind, string, bool) {
|
|
118
|
+
for _, candidate := range []struct {
|
|
119
|
+
marker string
|
|
120
|
+
tag tagKind
|
|
121
|
+
}{
|
|
122
|
+
{marker: "@evidenceExclude", tag: tagExclude},
|
|
123
|
+
{marker: "@evidence", tag: tagEvidence},
|
|
124
|
+
} {
|
|
125
|
+
if !strings.HasPrefix(line, candidate.marker) {
|
|
126
|
+
continue
|
|
127
|
+
}
|
|
128
|
+
remainder := line[len(candidate.marker):]
|
|
129
|
+
if remainder != "" && remainder[0] != ' ' && remainder[0] != '\t' {
|
|
130
|
+
continue
|
|
131
|
+
}
|
|
132
|
+
return candidate.tag, strings.TrimSpace(remainder), true
|
|
133
|
+
}
|
|
134
|
+
return "", "", false
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// inlineLinkTags are the JSDoc inline link forms a TypeScript target may use.
|
|
138
|
+
//
|
|
139
|
+
// TypeScript resolves the name inside one of these and counts it as a use,
|
|
140
|
+
// which is the only reason a citation-only import survives `noUnusedLocals`.
|
|
141
|
+
// No other tag earns that, so no other tag may open a target.
|
|
142
|
+
var inlineLinkTags = []string{"{@linkcode", "{@linkplain", "{@link"}
|
|
143
|
+
|
|
144
|
+
func splitDeclarationBody(body string) (string, string) {
|
|
145
|
+
body = strings.TrimSpace(body)
|
|
146
|
+
if body == "" {
|
|
147
|
+
return "", ""
|
|
148
|
+
}
|
|
149
|
+
if target, reason, found := splitInlineLinkBody(body); found {
|
|
150
|
+
return target, reason
|
|
151
|
+
}
|
|
152
|
+
for index, char := range body {
|
|
153
|
+
if unicode.IsSpace(char) {
|
|
154
|
+
return body[:index], strings.TrimSpace(body[index:])
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return body, ""
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// splitInlineLinkBody consumes a braced target through its closing brace.
|
|
161
|
+
//
|
|
162
|
+
// The brace supplies the boundary a whitespace-delimited token cannot, so the
|
|
163
|
+
// grammar stays self-discriminating: the parser decides which resolver applies
|
|
164
|
+
// from the token alone, with no reference context. That is what keeps the
|
|
165
|
+
// `POST /members` hazard from recurring in a new form.
|
|
166
|
+
func splitInlineLinkBody(body string) (string, string, bool) {
|
|
167
|
+
marker := ""
|
|
168
|
+
for _, candidate := range inlineLinkTags {
|
|
169
|
+
if strings.HasPrefix(body, candidate) {
|
|
170
|
+
marker = candidate
|
|
171
|
+
break
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if marker == "" {
|
|
175
|
+
return "", "", false
|
|
176
|
+
}
|
|
177
|
+
closing := strings.IndexByte(body, '}')
|
|
178
|
+
if closing < 0 {
|
|
179
|
+
// An unterminated link is a malformed declaration rather than a plain
|
|
180
|
+
// token: reporting it as the target `{@link` would name a repair the
|
|
181
|
+
// author cannot make.
|
|
182
|
+
return "", "", false
|
|
183
|
+
}
|
|
184
|
+
inner := strings.TrimSpace(body[len(marker):closing])
|
|
185
|
+
reason := strings.TrimSpace(body[closing+1:])
|
|
186
|
+
if inner == "" || containsWhitespace(inner) {
|
|
187
|
+
return "", "", false
|
|
188
|
+
}
|
|
189
|
+
return inlineLinkPrefix + inner, reason, true
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// inlineLinkPrefix marks a parsed target as import-scope resolved.
|
|
193
|
+
//
|
|
194
|
+
// Carrying the discrimination in the value keeps every downstream consumer —
|
|
195
|
+
// resolution, diagnostics, duplicate detection — reading one field instead of
|
|
196
|
+
// re-parsing the comment to recover what the parser already knew.
|
|
197
|
+
const inlineLinkPrefix = "\x00link:"
|
|
198
|
+
|
|
199
|
+
func isInlineLinkTarget(target string) bool {
|
|
200
|
+
return strings.HasPrefix(target, inlineLinkPrefix)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
func inlineLinkTarget(target string) string {
|
|
204
|
+
return strings.TrimPrefix(target, inlineLinkPrefix)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// displayTarget renders a target the way its author wrote it.
|
|
208
|
+
func displayTarget(target string) string {
|
|
209
|
+
if isInlineLinkTarget(target) {
|
|
210
|
+
return "{@link " + inlineLinkTarget(target) + "}"
|
|
211
|
+
}
|
|
212
|
+
return target
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
func normalizeMarkdownTarget(target string) string {
|
|
216
|
+
target = strings.ReplaceAll(target, "\\", "/")
|
|
217
|
+
for strings.HasPrefix(target, "./") {
|
|
218
|
+
target = strings.TrimPrefix(target, "./")
|
|
219
|
+
}
|
|
220
|
+
return target
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
func containsWhitespace(value string) bool {
|
|
224
|
+
for _, char := range value {
|
|
225
|
+
if unicode.IsSpace(char) {
|
|
226
|
+
return true
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return false
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
func lineAt(content string, offset int) int {
|
|
233
|
+
if offset < 0 {
|
|
234
|
+
return 1
|
|
235
|
+
}
|
|
236
|
+
if offset > len(content) {
|
|
237
|
+
offset = len(content)
|
|
238
|
+
}
|
|
239
|
+
return 1 + strings.Count(content[:offset], "\n")
|
|
240
|
+
}
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
package evidence
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"sort"
|
|
6
|
+
"strings"
|
|
7
|
+
|
|
8
|
+
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
9
|
+
|
|
10
|
+
"github.com/samchon/ttsc/packages/lint/rule"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
// documentedRule requires a JSDoc block on every selected export.
|
|
14
|
+
//
|
|
15
|
+
// Evidence declarations are read from one place only — the JSDoc blocks a node
|
|
16
|
+
// reports — so an export without a block is not merely undocumented, it is
|
|
17
|
+
// structurally unable to carry a citation. The graph never says so: coverage is
|
|
18
|
+
// counted from the evidence side, so an obligation is discharged by whichever
|
|
19
|
+
// host does have a block while every undocumented export in the same claim
|
|
20
|
+
// contributes nothing and looks exactly like a passing file.
|
|
21
|
+
type documentedRule struct{}
|
|
22
|
+
|
|
23
|
+
type documentedOptions struct {
|
|
24
|
+
Symbol json.RawMessage `json:"symbol"`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
func (documentedRule) Name() string { return documentedRuleName }
|
|
28
|
+
|
|
29
|
+
func (documentedRule) Visits() []shimast.Kind {
|
|
30
|
+
return []shimast.Kind{shimast.KindSourceFile}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
func (documentedRule) NeedsTypeChecker() bool { return false }
|
|
34
|
+
|
|
35
|
+
func (documentedRule) VisitsDeclarationFiles() bool { return false }
|
|
36
|
+
|
|
37
|
+
func (documentedRule) Check(ctx *rule.Context, node *shimast.Node) {
|
|
38
|
+
if ctx == nil || ctx.File == nil || node == nil {
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
if node.Kind != shimast.KindSourceFile {
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
selected, problems := decodeDocumentedOptions(ctx.Options)
|
|
45
|
+
if len(problems) != 0 {
|
|
46
|
+
for _, problem := range problems {
|
|
47
|
+
if !firstDocumentedCycleDiagnostic(ctx, problem) {
|
|
48
|
+
continue
|
|
49
|
+
}
|
|
50
|
+
ctx.Report(node, problem)
|
|
51
|
+
}
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
for _, host := range documentedHosts(ctx.File) {
|
|
55
|
+
if !selected.contains(host.Symbol) {
|
|
56
|
+
continue
|
|
57
|
+
}
|
|
58
|
+
blocks := host.documentation(ctx.File)
|
|
59
|
+
// Every finding anchors on the declaration rather than on a block.
|
|
60
|
+
// `Report` skips a node's leading trivia, and a JSDoc node is entirely
|
|
61
|
+
// trivia to the declaration it precedes, so anchoring there asks the
|
|
62
|
+
// host to underline the range it is built to skip past.
|
|
63
|
+
switch {
|
|
64
|
+
case len(blocks.Content) != 0:
|
|
65
|
+
continue
|
|
66
|
+
case len(blocks.Empty) != 0:
|
|
67
|
+
ctx.Report(
|
|
68
|
+
host.Node,
|
|
69
|
+
"Empty JSDoc on "+host.describe()+
|
|
70
|
+
". The block states nothing and carries no tag, so it documents nothing and cites nothing. Describe what this declaration is for, or cite its evidence with '@evidence <target> <reason>'.",
|
|
71
|
+
)
|
|
72
|
+
default:
|
|
73
|
+
ctx.Report(
|
|
74
|
+
host.Node,
|
|
75
|
+
"Missing JSDoc on "+host.describe()+
|
|
76
|
+
". An '@evidence' tag is only ever read from a JSDoc block, so without one this declaration can never cite anything. Add a '/** ... */' block above it.",
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// firstDocumentedCycleDiagnostic deduplicates configuration failures through
|
|
83
|
+
// the graph project rule's state, the only Program-cycle value the contributor
|
|
84
|
+
// API exposes to a file rule.
|
|
85
|
+
//
|
|
86
|
+
// Consumers normally enable documented beside graph, as the package's canonical
|
|
87
|
+
// configuration does. If graph is absent or off there is no cycle identity to
|
|
88
|
+
// coordinate through; reporting in every visited file is deliberately safer
|
|
89
|
+
// than suppressing a configuration failure across an unknown later cycle.
|
|
90
|
+
func firstDocumentedCycleDiagnostic(ctx *rule.Context, message string) bool {
|
|
91
|
+
if ctx == nil {
|
|
92
|
+
return false
|
|
93
|
+
}
|
|
94
|
+
result := ctx.ProjectResult(graphRuleName)
|
|
95
|
+
cycle, ok := result.State.(*graphCycleState)
|
|
96
|
+
if !ok || cycle == nil {
|
|
97
|
+
return true
|
|
98
|
+
}
|
|
99
|
+
return cycle.Diagnostics.first(documentedRuleName + "\x00" + message)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
func init() { rule.Register(documentedRule{}) }
|
|
103
|
+
|
|
104
|
+
func decodeDocumentedOptions(raw json.RawMessage) (symbolSet, []string) {
|
|
105
|
+
options := documentedOptions{}
|
|
106
|
+
if len(strings.TrimSpace(string(raw))) != 0 {
|
|
107
|
+
if err := json.Unmarshal(raw, &options); err != nil {
|
|
108
|
+
return nil, []string{
|
|
109
|
+
"Invalid evidence/documented configuration: expected an ITtscEvidenceDocumentedConfig object, or a bare severity for the default selection.",
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if problems := rejectUnknownDocumentedFields(raw); len(problems) != 0 {
|
|
113
|
+
return nil, problems
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
selected, problems := decodeSymbols(
|
|
117
|
+
options.Symbol,
|
|
118
|
+
artifactTypeScript,
|
|
119
|
+
false,
|
|
120
|
+
documentedRuleName,
|
|
121
|
+
"symbol",
|
|
122
|
+
)
|
|
123
|
+
if len(problems) != 0 {
|
|
124
|
+
return nil, problems
|
|
125
|
+
}
|
|
126
|
+
return selected, nil
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
func rejectUnknownDocumentedFields(raw json.RawMessage) []string {
|
|
130
|
+
object, problem := decodeObject(raw, "configuration")
|
|
131
|
+
if problem != "" {
|
|
132
|
+
return []string{
|
|
133
|
+
"Invalid evidence/documented configuration: expected an ITtscEvidenceDocumentedConfig object, or a bare severity for the default selection.",
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return rejectUnknownFields(
|
|
137
|
+
object,
|
|
138
|
+
[]string{"symbol"},
|
|
139
|
+
documentedRuleName,
|
|
140
|
+
"configuration",
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// documentedHost is one public identity that must carry a JSDoc block.
|
|
145
|
+
//
|
|
146
|
+
// The unit judged is an identity, never a declaration node. Declaration merging
|
|
147
|
+
// and overload sets give one identity several nodes — `interface I` beside
|
|
148
|
+
// `namespace I`, a callable beside its signatures — and judging the nodes would
|
|
149
|
+
// demand a block on every half of the very idiom `evidence/singular` blesses.
|
|
150
|
+
//
|
|
151
|
+
// Nodes holds them in source order, so Node is the declaration that founds the
|
|
152
|
+
// identity and the only one the rule reads.
|
|
153
|
+
type documentedHost struct {
|
|
154
|
+
Node *shimast.Node
|
|
155
|
+
Nodes []*shimast.Node
|
|
156
|
+
Symbol string
|
|
157
|
+
Targets []string
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// documentationBlocks separates the blocks on one declaration by whether they
|
|
161
|
+
// say anything.
|
|
162
|
+
//
|
|
163
|
+
// Only a block with content documents, and only a block with content can hold a
|
|
164
|
+
// citation. An empty block keeps its own finding, because telling an author a
|
|
165
|
+
// block is missing while they are looking straight at one helps nobody.
|
|
166
|
+
type documentationBlocks struct {
|
|
167
|
+
Content []*shimast.Node
|
|
168
|
+
Empty []*shimast.Node
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// documentation reads the blocks on the declaration that founds this identity.
|
|
172
|
+
//
|
|
173
|
+
// Only the first declaration is read. A merged identity is whatever its first
|
|
174
|
+
// declaration says it is, so a block on a later half neither satisfies the
|
|
175
|
+
// first nor counts against it — one unconditional rule, with nothing further to
|
|
176
|
+
// police.
|
|
177
|
+
func (host documentedHost) documentation(
|
|
178
|
+
file *shimast.SourceFile,
|
|
179
|
+
) documentationBlocks {
|
|
180
|
+
blocks := documentationBlocks{}
|
|
181
|
+
if host.Node == nil {
|
|
182
|
+
return blocks
|
|
183
|
+
}
|
|
184
|
+
content := file.Text()
|
|
185
|
+
for _, doc := range host.Node.JSDoc(file) {
|
|
186
|
+
if doc == nil || doc.Pos() < 0 || doc.End() > len(content) {
|
|
187
|
+
continue
|
|
188
|
+
}
|
|
189
|
+
if jsdocHasContent(content[doc.Pos():doc.End()]) {
|
|
190
|
+
blocks.Content = append(blocks.Content, doc)
|
|
191
|
+
continue
|
|
192
|
+
}
|
|
193
|
+
blocks.Empty = append(blocks.Empty, doc)
|
|
194
|
+
}
|
|
195
|
+
return blocks
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
func (host documentedHost) describe() string {
|
|
199
|
+
return "exported " + host.Symbol + " '" +
|
|
200
|
+
strings.Join(host.Targets, "', '") + "'"
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// documentedHosts lists the public identities of a file in source order.
|
|
204
|
+
//
|
|
205
|
+
// Both the population and its qualified names come from the collector
|
|
206
|
+
// `evidence/graph` uses. The population that must be able to hold a tag is by
|
|
207
|
+
// definition the population a claim can select as a host, and the name a
|
|
208
|
+
// diagnostic asks the author to cite has to be the name the graph resolves — a
|
|
209
|
+
// second walk here would let either drift, and the rule would then guarantee
|
|
210
|
+
// the wrong set under the wrong names.
|
|
211
|
+
func documentedHosts(file *shimast.SourceFile) []documentedHost {
|
|
212
|
+
inventory := &artifactInventory{
|
|
213
|
+
Type: artifactTypeScript,
|
|
214
|
+
UnitNodes: map[string][]*shimast.Node{},
|
|
215
|
+
}
|
|
216
|
+
supported := map[*shimast.Node]symbolSet{}
|
|
217
|
+
collectTypeScriptStatements(
|
|
218
|
+
file,
|
|
219
|
+
file.Statements,
|
|
220
|
+
nil,
|
|
221
|
+
"",
|
|
222
|
+
inventory,
|
|
223
|
+
supported,
|
|
224
|
+
map[string]*evidenceUnit{},
|
|
225
|
+
file.IsDeclarationFile,
|
|
226
|
+
false,
|
|
227
|
+
false,
|
|
228
|
+
"",
|
|
229
|
+
)
|
|
230
|
+
hosts := make([]documentedHost, 0, len(inventory.Units))
|
|
231
|
+
for _, unit := range inventory.Units {
|
|
232
|
+
if unit.Hidden != "" {
|
|
233
|
+
continue
|
|
234
|
+
}
|
|
235
|
+
nodes := hostNodesOf(inventory.UnitNodes[unit.ID], supported, unit.Symbol)
|
|
236
|
+
if len(nodes) == 0 {
|
|
237
|
+
continue
|
|
238
|
+
}
|
|
239
|
+
hosts = append(hosts, documentedHost{
|
|
240
|
+
Node: nodes[0],
|
|
241
|
+
Nodes: nodes,
|
|
242
|
+
Symbol: unit.Symbol,
|
|
243
|
+
Targets: []string{unit.Target},
|
|
244
|
+
})
|
|
245
|
+
}
|
|
246
|
+
sort.Slice(hosts, func(left int, right int) bool {
|
|
247
|
+
return hosts[left].Node.Pos() < hosts[right].Node.Pos()
|
|
248
|
+
})
|
|
249
|
+
return orderIdentityDeclarations(mergeSharedBlockHosts(hosts))
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// hostNodesOf keeps the declarations that can actually carry a citation.
|
|
253
|
+
//
|
|
254
|
+
// A name can be spelled by a declaration that hosts nothing: a class is not a
|
|
255
|
+
// type unit, so `class C` beside `namespace C` materializes the `C` type from
|
|
256
|
+
// the namespace alone, and the collector registers no host for the class at
|
|
257
|
+
// all. Demanding the block there would send an author's `@evidence` into a
|
|
258
|
+
// position where `evidence/graph` rejects it as an unsupported host — the rule
|
|
259
|
+
// steering citations somewhere a citation cannot live, which is the failure it
|
|
260
|
+
// exists to prevent.
|
|
261
|
+
//
|
|
262
|
+
// Narrowing to real hosts also keeps this rule's premise exact. The population
|
|
263
|
+
// that must be able to hold a tag is the population a claim can select as a
|
|
264
|
+
// host, and nothing wider.
|
|
265
|
+
func hostNodesOf(
|
|
266
|
+
nodes []*shimast.Node,
|
|
267
|
+
supported map[*shimast.Node]symbolSet,
|
|
268
|
+
symbol string,
|
|
269
|
+
) []*shimast.Node {
|
|
270
|
+
hosts := make([]*shimast.Node, 0, len(nodes))
|
|
271
|
+
for _, node := range nodes {
|
|
272
|
+
if supported[node].contains(symbol) {
|
|
273
|
+
hosts = append(hosts, node)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return hosts
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// orderIdentityDeclarations puts each identity's declarations in source order.
|
|
280
|
+
//
|
|
281
|
+
// The collector records them as it walks, and the merged forms are folded in
|
|
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` — and for
|
|
284
|
+
// a variable the enclosing statement wins over its binding, which is where
|
|
285
|
+
// TypeScript actually attaches the block.
|
|
286
|
+
func orderIdentityDeclarations(hosts []documentedHost) []documentedHost {
|
|
287
|
+
for index := range hosts {
|
|
288
|
+
nodes := hosts[index].Nodes
|
|
289
|
+
sort.SliceStable(nodes, func(left int, right int) bool {
|
|
290
|
+
return nodes[left].Pos() < nodes[right].Pos()
|
|
291
|
+
})
|
|
292
|
+
if len(nodes) != 0 {
|
|
293
|
+
hosts[index].Node = nodes[0]
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return hosts
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// mergeSharedBlockHosts folds identities that can only ever share one block.
|
|
300
|
+
//
|
|
301
|
+
// `export const a = 1, b = 2;` declares two identities, and TypeScript gives
|
|
302
|
+
// the statement a single JSDoc position serving both. They are two obligations
|
|
303
|
+
// with one repair, so reporting each separately tells the author the same thing
|
|
304
|
+
// twice — the duplication this project's diagnostics deliberately avoid.
|
|
305
|
+
func mergeSharedBlockHosts(hosts []documentedHost) []documentedHost {
|
|
306
|
+
merged := make([]documentedHost, 0, len(hosts))
|
|
307
|
+
byStatement := map[*shimast.Node]int{}
|
|
308
|
+
for _, host := range hosts {
|
|
309
|
+
statement := sharedVariableStatement(host)
|
|
310
|
+
if statement == nil {
|
|
311
|
+
merged = append(merged, host)
|
|
312
|
+
continue
|
|
313
|
+
}
|
|
314
|
+
if index, seen := byStatement[statement]; seen &&
|
|
315
|
+
merged[index].Symbol == host.Symbol {
|
|
316
|
+
merged[index].Targets = append(merged[index].Targets, host.Targets...)
|
|
317
|
+
continue
|
|
318
|
+
}
|
|
319
|
+
byStatement[statement] = len(merged)
|
|
320
|
+
merged = append(merged, host)
|
|
321
|
+
}
|
|
322
|
+
return merged
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
func sharedVariableStatement(host documentedHost) *shimast.Node {
|
|
326
|
+
for _, node := range host.Nodes {
|
|
327
|
+
if node != nil && node.Kind == shimast.KindVariableStatement {
|
|
328
|
+
return node
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return nil
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// jsdocHasContent reports whether a block says anything at all.
|
|
335
|
+
//
|
|
336
|
+
// Blocks are read through the same accessor the tag collector uses, so the rule
|
|
337
|
+
// accepts exactly what a citation could be found in. Accepting anything wider
|
|
338
|
+
// would pass a comment the graph cannot read, which is the silence this rule
|
|
339
|
+
// exists to remove.
|
|
340
|
+
func jsdocHasContent(comment string) bool {
|
|
341
|
+
comment = strings.TrimSpace(comment)
|
|
342
|
+
comment = strings.TrimPrefix(comment, "/**")
|
|
343
|
+
comment = strings.TrimPrefix(comment, "/*")
|
|
344
|
+
comment = strings.TrimSuffix(comment, "*/")
|
|
345
|
+
for _, line := range strings.Split(comment, "\n") {
|
|
346
|
+
line = strings.TrimSpace(line)
|
|
347
|
+
line = strings.TrimSpace(strings.TrimPrefix(line, "*"))
|
|
348
|
+
if line != "" {
|
|
349
|
+
return true
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return false
|
|
353
|
+
}
|