@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,274 @@
|
|
|
1
|
+
package evidence
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"crypto/sha256"
|
|
5
|
+
"encoding/hex"
|
|
6
|
+
"os"
|
|
7
|
+
"strings"
|
|
8
|
+
"sync"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// swaggerCacheLimit bounds the cache so a resident host cannot grow without
|
|
12
|
+
// end. A project configures a handful of Swagger documents, not hundreds, so
|
|
13
|
+
// this is a backstop against a pathological configuration rather than a tuning
|
|
14
|
+
// knob — and the oldest entry is dropped rather than the whole map cleared, so
|
|
15
|
+
// a project sitting exactly on the limit still gets hits.
|
|
16
|
+
const swaggerCacheLimit = 64
|
|
17
|
+
|
|
18
|
+
// swaggerDocuments remembers normalization outcomes by document content.
|
|
19
|
+
//
|
|
20
|
+
// This is the rule's own state, not the project state the host publishes. The
|
|
21
|
+
// `@ttsc/lint` contributor contract forbids caching a `SetState` value across Program
|
|
22
|
+
// cycles because the host owns that wrapper's lifetime; nothing there governs
|
|
23
|
+
// memory a rule keeps for itself, and the parallel walk never reaches this
|
|
24
|
+
// because a project rule runs before any file rule. The mutex is not for that
|
|
25
|
+
// walk — it is because a resident host may hold several projects at once, and
|
|
26
|
+
// paying for a lock on a path that skips a process spawn is not a trade worth
|
|
27
|
+
// thinking about.
|
|
28
|
+
//
|
|
29
|
+
// The entry lives as long as the process does. Both hosts that reuse it are
|
|
30
|
+
// line-protocol loops in one process, so exit disposes it; the contributor API
|
|
31
|
+
// exposes no earlier disposal hook, which is why the bound above exists rather
|
|
32
|
+
// than being a tuning knob.
|
|
33
|
+
var swaggerDocuments = newSwaggerCache()
|
|
34
|
+
|
|
35
|
+
// swaggerRemoteDocuments remembers a URL's operations for the process lifetime,
|
|
36
|
+
// keyed by the address rather than by content.
|
|
37
|
+
//
|
|
38
|
+
// A remote document has no key without fetching it, so it cannot be revalidated
|
|
39
|
+
// the way a local file is. The choice is therefore not "cache or revalidate" but
|
|
40
|
+
// "fetch once or fetch forever", and a resident session that refetched on every
|
|
41
|
+
// rebuild would put a network round trip on the edit loop — and make an editor
|
|
42
|
+
// depend on connectivity to report anything at all.
|
|
43
|
+
//
|
|
44
|
+
// The freshness this gives up is real and bounded by the process: a served
|
|
45
|
+
// document that changes mid-session is not seen until the session restarts. A
|
|
46
|
+
// one-shot `ttsc check` is a fresh process, so it always fetches.
|
|
47
|
+
//
|
|
48
|
+
// **A rejection is never remembered here.** The key is the address, so nothing
|
|
49
|
+
// an author can do would clear it — one refused connection would poison the URL
|
|
50
|
+
// for the whole session with no edit able to invalidate the entry. Failure stays
|
|
51
|
+
// per-evaluation, which is also what makes a transient outage recoverable
|
|
52
|
+
// without restarting the editor.
|
|
53
|
+
var swaggerRemoteDocuments = newSwaggerCache()
|
|
54
|
+
|
|
55
|
+
// swaggerDocumentOutcome is what one document's bytes normalized to.
|
|
56
|
+
//
|
|
57
|
+
// Failure is remembered as deliberately as success. A document the normalizer
|
|
58
|
+
// rejects is one the author is midway through fixing, and while they fix it
|
|
59
|
+
// every unrelated TypeScript save would otherwise pay a fresh process start to
|
|
60
|
+
// be told the same thing again. Nothing is cached forever: repairing the
|
|
61
|
+
// document changes its bytes, which changes the key, which misses.
|
|
62
|
+
//
|
|
63
|
+
// Rejected is a separate flag rather than a non-empty Problem, because the
|
|
64
|
+
// reason is the normalizer's string and this rule does not get to assume it is
|
|
65
|
+
// non-empty. Inferring rejection from the message would turn a reason-less
|
|
66
|
+
// failure into zero operations and no diagnostic — a rejected document that
|
|
67
|
+
// reads exactly like an empty but passing one, which is the shape
|
|
68
|
+
// `test_evidence_graph_reports_swagger_source_failures` exists to forbid.
|
|
69
|
+
type swaggerDocumentOutcome struct {
|
|
70
|
+
Operations []swaggerOperation
|
|
71
|
+
Rejected bool
|
|
72
|
+
Problem string
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
func newSwaggerCache() *swaggerCache {
|
|
76
|
+
return &swaggerCache{entries: map[string]swaggerDocumentOutcome{}}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
type swaggerCache struct {
|
|
80
|
+
mutex sync.Mutex
|
|
81
|
+
entries map[string]swaggerDocumentOutcome
|
|
82
|
+
order []string
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
func (cache *swaggerCache) lookup(digest string) (swaggerDocumentOutcome, bool) {
|
|
86
|
+
if digest == "" {
|
|
87
|
+
return swaggerDocumentOutcome{}, false
|
|
88
|
+
}
|
|
89
|
+
cache.mutex.Lock()
|
|
90
|
+
defer cache.mutex.Unlock()
|
|
91
|
+
outcome, hit := cache.entries[digest]
|
|
92
|
+
if !hit {
|
|
93
|
+
return swaggerDocumentOutcome{}, false
|
|
94
|
+
}
|
|
95
|
+
// Copied out because the caller builds units from it while another cycle
|
|
96
|
+
// may be reading the same entry.
|
|
97
|
+
return swaggerDocumentOutcome{
|
|
98
|
+
Operations: append([]swaggerOperation(nil), outcome.Operations...),
|
|
99
|
+
Rejected: outcome.Rejected,
|
|
100
|
+
Problem: outcome.Problem,
|
|
101
|
+
}, true
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
func (cache *swaggerCache) store(digest string, outcome swaggerDocumentOutcome) {
|
|
105
|
+
if digest == "" {
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
cache.mutex.Lock()
|
|
109
|
+
defer cache.mutex.Unlock()
|
|
110
|
+
if _, exists := cache.entries[digest]; exists {
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
if len(cache.order) >= swaggerCacheLimit {
|
|
114
|
+
delete(cache.entries, cache.order[0])
|
|
115
|
+
cache.order = cache.order[1:]
|
|
116
|
+
}
|
|
117
|
+
cache.entries[digest] = swaggerDocumentOutcome{
|
|
118
|
+
Operations: append([]swaggerOperation(nil), outcome.Operations...),
|
|
119
|
+
Rejected: outcome.Rejected,
|
|
120
|
+
Problem: outcome.Problem,
|
|
121
|
+
}
|
|
122
|
+
cache.order = append(cache.order, digest)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// swaggerContentDigests hashes each local source's bytes.
|
|
126
|
+
//
|
|
127
|
+
// The digest is the whole cache key, which is what makes staleness structural
|
|
128
|
+
// rather than improbable: identical bytes normalize to identical operations, so
|
|
129
|
+
// a hit can only return what the current file means. An edit, a truncation, or
|
|
130
|
+
// a same-length replacement all change the digest and miss. A missing or
|
|
131
|
+
// unreadable file yields no digest and is normalized as before, which keeps the
|
|
132
|
+
// normalizer's own diagnostic for it.
|
|
133
|
+
//
|
|
134
|
+
// An HTTP(S) source never participates. A URL has no validator without a
|
|
135
|
+
// fetch, and the fetch is most of what the normalizer costs, so a remote
|
|
136
|
+
// document cannot be shown unchanged without paying the price of finding out.
|
|
137
|
+
func swaggerContentDigests(root string, sources []string) map[string]string {
|
|
138
|
+
digests := map[string]string{}
|
|
139
|
+
for _, source := range sources {
|
|
140
|
+
digest := swaggerContentDigest(root, source)
|
|
141
|
+
if digest != "" {
|
|
142
|
+
digests[source] = digest
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return digests
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
func swaggerContentDigest(root string, source string) string {
|
|
149
|
+
if isRemoteSwaggerSource(source) {
|
|
150
|
+
return ""
|
|
151
|
+
}
|
|
152
|
+
content, err := os.ReadFile(swaggerSourcePath(root, source))
|
|
153
|
+
if err != nil {
|
|
154
|
+
return ""
|
|
155
|
+
}
|
|
156
|
+
return swaggerDigestOf(content)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
func swaggerDigestOf(content []byte) string {
|
|
160
|
+
sum := sha256.Sum256(content)
|
|
161
|
+
return hex.EncodeToString(sum[:])
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// swaggerSourcePath locates a local document, which may sit outside the project.
|
|
165
|
+
func swaggerSourcePath(root string, source string) string {
|
|
166
|
+
return resolveProjectPath(root, source)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// rememberSwaggerDocument records an outcome under the bytes it was produced
|
|
170
|
+
// from, as reported by the process that produced it.
|
|
171
|
+
//
|
|
172
|
+
// The digest is the normalizer's own, never the one this process took
|
|
173
|
+
// beforehand. The normalizer opens the file again, in another process, after
|
|
174
|
+
// this one read it — so keying on the earlier digest would let a write landing
|
|
175
|
+
// inside that window bind one document's operations to another document's
|
|
176
|
+
// bytes. That is a hit which answers with the wrong document and keeps doing
|
|
177
|
+
// so, which is the one failure a cache must not be able to produce.
|
|
178
|
+
//
|
|
179
|
+
// A source is skipped when the normalizer reports no digest, which is how a
|
|
180
|
+
// remote document stays out of the cache no matter what it returns.
|
|
181
|
+
func rememberSwaggerDocument(
|
|
182
|
+
source string,
|
|
183
|
+
digest string,
|
|
184
|
+
outcome swaggerDocumentOutcome,
|
|
185
|
+
) {
|
|
186
|
+
if isRemoteSwaggerSource(source) {
|
|
187
|
+
if !outcome.Rejected {
|
|
188
|
+
swaggerRemoteDocuments.store(source, outcome)
|
|
189
|
+
}
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
if digest == "" {
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
swaggerDocuments.store(digest, outcome)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// lookupSwaggerDocument answers from whichever memory owns this source.
|
|
199
|
+
//
|
|
200
|
+
// The two are separate caches rather than one map with two key shapes, because
|
|
201
|
+
// they answer different questions: a local entry means "these bytes normalize
|
|
202
|
+
// to this", which stays true forever, while a remote entry means "this address
|
|
203
|
+
// answered this, once", which is a decision about freshness rather than a fact
|
|
204
|
+
// about content.
|
|
205
|
+
func lookupSwaggerDocument(
|
|
206
|
+
source string,
|
|
207
|
+
digest string,
|
|
208
|
+
) (swaggerDocumentOutcome, bool) {
|
|
209
|
+
if isRemoteSwaggerSource(source) {
|
|
210
|
+
return swaggerRemoteDocuments.lookup(source)
|
|
211
|
+
}
|
|
212
|
+
return swaggerDocuments.lookup(digest)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// swaggerUnitsFromOutcome rebuilds one source's units from a remembered
|
|
216
|
+
// outcome.
|
|
217
|
+
//
|
|
218
|
+
// Units are rebuilt per source rather than remembered, because a unit carries
|
|
219
|
+
// its source in its identity while the operations do not. That is also what
|
|
220
|
+
// lets two sources holding identical bytes share one entry: what was cached is
|
|
221
|
+
// a property of the document, not of where it was found.
|
|
222
|
+
func swaggerUnitsFromOutcome(
|
|
223
|
+
source string,
|
|
224
|
+
inventory *artifactInventory,
|
|
225
|
+
outcome swaggerDocumentOutcome,
|
|
226
|
+
) []string {
|
|
227
|
+
if inventory == nil {
|
|
228
|
+
return nil
|
|
229
|
+
}
|
|
230
|
+
if outcome.Rejected {
|
|
231
|
+
message := swaggerNormalizationFailure(source, outcome.Problem)
|
|
232
|
+
inventory.LoadFailed = true
|
|
233
|
+
inventory.Problems = append(inventory.Problems, inventoryProblem{
|
|
234
|
+
Symbol: "operation",
|
|
235
|
+
Message: message,
|
|
236
|
+
})
|
|
237
|
+
return []string{message}
|
|
238
|
+
}
|
|
239
|
+
problems := []string{}
|
|
240
|
+
for _, operation := range outcome.Operations {
|
|
241
|
+
unit, problem := swaggerOperationUnit(source, operation)
|
|
242
|
+
if problem != "" {
|
|
243
|
+
inventory.Problems = append(inventory.Problems, inventoryProblem{
|
|
244
|
+
Symbol: "operation",
|
|
245
|
+
Message: problem,
|
|
246
|
+
})
|
|
247
|
+
problems = append(problems, problem)
|
|
248
|
+
continue
|
|
249
|
+
}
|
|
250
|
+
inventory.Units = append(inventory.Units, unit)
|
|
251
|
+
}
|
|
252
|
+
sortUnits(inventory.Units)
|
|
253
|
+
return problems
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// swaggerNormalizationFailure words a rejected source identically whether the
|
|
257
|
+
// rejection arrived from the normalizer this cycle or from memory, so a reader
|
|
258
|
+
// cannot tell the two apart and has no reason to want to.
|
|
259
|
+
//
|
|
260
|
+
// A reason-less rejection still gets a sentence. The reason comes from another
|
|
261
|
+
// process and this rule cannot require it to be non-empty, and a diagnostic
|
|
262
|
+
// that trails off after a colon reads like a formatting bug rather than a
|
|
263
|
+
// broken document.
|
|
264
|
+
func swaggerNormalizationFailure(source string, message string) string {
|
|
265
|
+
reason := strings.TrimSpace(message)
|
|
266
|
+
if reason == "" {
|
|
267
|
+
reason = "the normalizer reported no reason"
|
|
268
|
+
}
|
|
269
|
+
return "Evidence graph could not normalize Swagger source '" +
|
|
270
|
+
displaySwaggerSource(source) +
|
|
271
|
+
"' to @typia/interface OpenApi.IDocument: " +
|
|
272
|
+
reason +
|
|
273
|
+
". Fix the file or URL so @typia/utils can upgrade it."
|
|
274
|
+
}
|
package/native/todo.go
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
package evidence
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"strings"
|
|
5
|
+
|
|
6
|
+
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
7
|
+
|
|
8
|
+
"github.com/samchon/ttsc/packages/lint/rule"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// todoRule reports every JSDoc '@todo' tag in a checked file.
|
|
12
|
+
//
|
|
13
|
+
// A stub enters the world carrying a '@todo' that names what its realization
|
|
14
|
+
// still owes, and no other rule ever calls that debt: the graph judges
|
|
15
|
+
// citations, `evidence/documented` judges presence, and a block whose only
|
|
16
|
+
// content is a '@todo' satisfies both. A remaining tag is therefore an
|
|
17
|
+
// unrealized contract shipping as done, so the rule turns each one into a
|
|
18
|
+
// finding — the realize ledger, read straight out of the build.
|
|
19
|
+
//
|
|
20
|
+
// There is no selection to configure. A debt on a local helper is as unrealized
|
|
21
|
+
// as one on an export, so every declaration's block is read, and the rule takes
|
|
22
|
+
// no options; per-directory scoping belongs in the outer `files` setting of
|
|
23
|
+
// `lint.config.ts`, and the host refuses a configured options object on the
|
|
24
|
+
// rule's behalf.
|
|
25
|
+
type todoRule struct{}
|
|
26
|
+
|
|
27
|
+
func (todoRule) Name() string { return todoRuleName }
|
|
28
|
+
|
|
29
|
+
func (todoRule) Visits() []shimast.Kind {
|
|
30
|
+
return []shimast.Kind{shimast.KindSourceFile}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
func (todoRule) NeedsTypeChecker() bool { return false }
|
|
34
|
+
|
|
35
|
+
func (todoRule) VisitsDeclarationFiles() bool { return false }
|
|
36
|
+
|
|
37
|
+
func (todoRule) AcceptsTtscLintOptions() bool { return false }
|
|
38
|
+
|
|
39
|
+
func (todoRule) Check(ctx *rule.Context, node *shimast.Node) {
|
|
40
|
+
if ctx == nil || ctx.File == nil || node == nil {
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
if node.Kind != shimast.KindSourceFile {
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
content := ctx.File.Text()
|
|
47
|
+
// One physical block is one ledger entry. TypeScript cascades a leading
|
|
48
|
+
// block onto nested nodes — a variable statement and each of its
|
|
49
|
+
// declarations all answer for one comment — so blocks are deduplicated by
|
|
50
|
+
// position. The walk visits parents before children, which anchors each
|
|
51
|
+
// finding on the outermost declaration the block precedes: the node whose
|
|
52
|
+
// leading trivia the block actually is, and the one `Report` underlines
|
|
53
|
+
// correctly because it skips that trivia.
|
|
54
|
+
seen := map[int]bool{}
|
|
55
|
+
walkTypeScriptNode(node, func(current *shimast.Node) {
|
|
56
|
+
for _, doc := range current.JSDoc(ctx.File) {
|
|
57
|
+
if doc == nil || doc.Pos() < 0 || doc.End() > len(content) {
|
|
58
|
+
continue
|
|
59
|
+
}
|
|
60
|
+
if seen[doc.Pos()] {
|
|
61
|
+
continue
|
|
62
|
+
}
|
|
63
|
+
seen[doc.Pos()] = true
|
|
64
|
+
for _, entry := range todoEntries(content[doc.Pos():doc.End()]) {
|
|
65
|
+
ctx.Report(current, todoMessage(entry))
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
func init() { rule.Register(todoRule{}) }
|
|
72
|
+
|
|
73
|
+
func todoMessage(entry string) string {
|
|
74
|
+
found := "Unrealized '@todo'"
|
|
75
|
+
if entry != "" {
|
|
76
|
+
found += ": '" + entry + "'"
|
|
77
|
+
}
|
|
78
|
+
return found +
|
|
79
|
+
". A '@todo' tag names work this declaration still owes, so the contract it promises is not realized yet. Realize the declaration and remove the tag."
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// todoEntries lists the text of every '@todo' tag one JSDoc block carries.
|
|
83
|
+
//
|
|
84
|
+
// The scan mirrors the declaration parser's shape: strip the block decoration,
|
|
85
|
+
// walk lines, and let any other '@'-opening line close the entry above it, so a
|
|
86
|
+
// '@todo' shares a block with '@param' or '@evidence' without swallowing them.
|
|
87
|
+
// The tag name matches case-insensitively, because '@TODO' is the same promise
|
|
88
|
+
// shouted.
|
|
89
|
+
func todoEntries(comment string) []string {
|
|
90
|
+
comment = strings.TrimSpace(comment)
|
|
91
|
+
comment = strings.TrimPrefix(comment, "/**")
|
|
92
|
+
comment = strings.TrimPrefix(comment, "/*")
|
|
93
|
+
comment = strings.TrimSuffix(comment, "*/")
|
|
94
|
+
entries := []string{}
|
|
95
|
+
var pending []string
|
|
96
|
+
flush := func() {
|
|
97
|
+
if pending == nil {
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
entries = append(entries, strings.TrimSpace(strings.Join(pending, " ")))
|
|
101
|
+
pending = nil
|
|
102
|
+
}
|
|
103
|
+
for _, rawLine := range strings.Split(comment, "\n") {
|
|
104
|
+
line := strings.TrimSpace(rawLine)
|
|
105
|
+
line = strings.TrimSpace(strings.TrimPrefix(line, "*"))
|
|
106
|
+
if remainder, opened := todoLine(line); opened {
|
|
107
|
+
flush()
|
|
108
|
+
pending = []string{remainder}
|
|
109
|
+
continue
|
|
110
|
+
}
|
|
111
|
+
if strings.HasPrefix(line, "@") {
|
|
112
|
+
flush()
|
|
113
|
+
continue
|
|
114
|
+
}
|
|
115
|
+
if pending != nil && line != "" {
|
|
116
|
+
pending = append(pending, line)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
flush()
|
|
120
|
+
return entries
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// todoLine reports whether a line opens a '@todo' tag, and with what remainder.
|
|
124
|
+
//
|
|
125
|
+
// The boundary check keeps a longer tag out: '@todos' is some other tool's tag,
|
|
126
|
+
// not a shouting variant of this one, and matching it would report a debt the
|
|
127
|
+
// author never recorded.
|
|
128
|
+
func todoLine(line string) (string, bool) {
|
|
129
|
+
const marker = "@todo"
|
|
130
|
+
if len(line) < len(marker) || !strings.EqualFold(line[:len(marker)], marker) {
|
|
131
|
+
return "", false
|
|
132
|
+
}
|
|
133
|
+
remainder := line[len(marker):]
|
|
134
|
+
if remainder != "" && remainder[0] != ' ' && remainder[0] != '\t' {
|
|
135
|
+
return "", false
|
|
136
|
+
}
|
|
137
|
+
return strings.TrimSpace(remainder), true
|
|
138
|
+
}
|