@ttsc/evidence 0.25.0 → 0.26.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.
@@ -0,0 +1,155 @@
1
+ package evidence
2
+
3
+ import (
4
+ "sort"
5
+ "strings"
6
+
7
+ shimast "github.com/microsoft/typescript-go/shim/ast"
8
+ )
9
+
10
+ // attachedCommentEnds indexes, by end offset, where each attached documentation
11
+ // block's own text begins.
12
+ //
13
+ // The end is what identifies a comment: the parser stops reading one exactly
14
+ // where the scanner does. A block's reported start is not, because it is a full
15
+ // start, and a full start reaches back to the previous token and swallows every
16
+ // blank line and every comment between. Testing containment against that made
17
+ // the reporter answer differently depending on what followed a tag, so a
18
+ // citation in a `//` comment was reported above an undocumented declaration and
19
+ // silent above a documented one, which is the shape an author is most likely to
20
+ // write in a codebase that documents its exports.
21
+ type attachedCommentEnds map[int]int
22
+
23
+ // holds reports whether a comment is one the parser handed to a node.
24
+ func (ends attachedCommentEnds) holds(start int, end int) bool {
25
+ attachedStart, taken := ends[end]
26
+ return taken && attachedStart <= start
27
+ }
28
+
29
+ // reportUnreadableTypeScriptTags records every tag written in a comment the
30
+ // parser attached to no declaration.
31
+ //
32
+ // The graph reads tags only from the blocks a node reports, which is what keeps
33
+ // a citation, the host it lands on, and the exclusion that must cut it out of a
34
+ // digest all naming the same position. A comment the parser attached to nothing
35
+ // is outside that agreement: no node reports it, so the tag in it reaches no
36
+ // host and is not excluded from anything either.
37
+ //
38
+ // Discarding it silently is the failure this rule exists to remove. The comment
39
+ // is real, the file keeps it, and an author reading the source sees a citation
40
+ // that does nothing while the coverage diagnostic that follows names the
41
+ // reference and suggests writing the citation they already wrote.
42
+ //
43
+ // The shapes are ordinary rather than exotic. TypeScript attaches no
44
+ // documentation to a binding element, so a block between the braces of a
45
+ // destructuring pattern is read by nobody; a `//` comment is not documentation
46
+ // at all, which is one keystroke from a block that is; and a documented
47
+ // declaration an author commented out keeps a citation that now proves nothing.
48
+ func reportUnreadableTypeScriptTags(
49
+ file *shimast.SourceFile,
50
+ location string,
51
+ attached attachedCommentEnds,
52
+ inventory *artifactInventory,
53
+ ) {
54
+ if file == nil || inventory == nil {
55
+ return
56
+ }
57
+ content := file.Text()
58
+ forEachComment(file, func(_ shimast.Kind, start int, end int) {
59
+ if start < 0 || end > len(content) || start >= end {
60
+ return
61
+ }
62
+ if attached.holds(start, end) {
63
+ return
64
+ }
65
+ body := readableCommentBody(content[start:end])
66
+ baseLine := lineAt(content, start)
67
+ for _, parsed := range parseDeclarations(body) {
68
+ inventory.Unreadable = append(inventory.Unreadable, unreadableTagProblem(
69
+ "@"+string(parsed.Tag),
70
+ location,
71
+ baseLine+parsed.LineOffset,
72
+ ))
73
+ }
74
+ for _, review := range parseReviews(body) {
75
+ inventory.Unreadable = append(inventory.Unreadable, unreadableTagProblem(
76
+ review.marker(),
77
+ location,
78
+ baseLine+review.LineOffset,
79
+ ))
80
+ }
81
+ })
82
+ }
83
+
84
+ // unreadableTagProblem names the position and the moves that fix it, which is
85
+ // the whole value of reporting a tag nothing can read.
86
+ //
87
+ // Two moves rather than one, because the tag reaches nothing for two different
88
+ // reasons. A citation an author meant to keep belongs in a documentation block
89
+ // on a declaration; one left behind in code that is commented out belongs
90
+ // nowhere, and naming only the first would send that author to move a tag they
91
+ // should be deleting.
92
+ func unreadableTagProblem(tag string, location string, line int) string {
93
+ return "Unreadable " + tag + " at " + location + ":" + decimal(line) +
94
+ ": the parser attaches this comment to no declaration, so nothing reads the tag." +
95
+ " Move it into a documentation block written directly above the declaration it answers for," +
96
+ " or delete it with the code it was written against." +
97
+ untrueTagWarning
98
+ }
99
+
100
+ // readableCommentBody strips the syntax a comment opens with, so a line that
101
+ // carries a tag is recognized by the same reader every other comment goes
102
+ // through.
103
+ //
104
+ // The declaration parser removes `/**`, `/*`, and a leading `*`, because those
105
+ // are the shapes a readable block takes. It does not remove a run of slashes,
106
+ // and it must not learn to: a documentation block's line never opens that way,
107
+ // and teaching the shared parser to accept one would let a `//` line inside a
108
+ // block declare something the graph then reads from an unreadable position.
109
+ //
110
+ // Every run of two or more slashes comes off, so a tag buried behind a third or
111
+ // a fourth is answered like one behind two. They are unreadable for one reason
112
+ // and by one keystroke, and answering only some of them split one comment
113
+ // against itself: the review parser removes `///` and the declaration parser
114
+ // does not, so `/// @evidenceReview` was reported while the `/// @evidence`
115
+ // beside it stayed silent.
116
+ func readableCommentBody(comment string) string {
117
+ lines := strings.Split(comment, "\n")
118
+ for index, line := range lines {
119
+ trimmed := strings.TrimSpace(line)
120
+ if !strings.HasPrefix(trimmed, "//") {
121
+ continue
122
+ }
123
+ lines[index] = strings.TrimSpace(strings.TrimLeft(trimmed, "/"))
124
+ }
125
+ return strings.Join(lines, "\n")
126
+ }
127
+
128
+ // unreadableTypeScriptTags collects every unreadable tag the scanned TypeScript
129
+ // populations found.
130
+ //
131
+ // One file reached through two configured roots is two inventories of the same
132
+ // text, so the same comment is found twice. The graph's own reporter sorts and
133
+ // drops exact duplicates, which is what collapses them; this only has to gather
134
+ // them in a defined order so its input does not depend on map iteration.
135
+ //
136
+ // Only a file some configured glob selects is reported. A base is a directory
137
+ // and a population is a glob inside it, so a file is scanned whenever it sits
138
+ // under a declared root and belongs to a population only if the glob takes it.
139
+ // Reporting every scanned file made the rule answer for source it does not
140
+ // govern: a stray tag in a consumer's `node_modules` failed their build, naming
141
+ // a repair in a file they did not write.
142
+ func unreadableTypeScriptTags(
143
+ inventories map[string]*artifactInventory,
144
+ governed map[string]bool,
145
+ ) []string {
146
+ reported := []string{}
147
+ for address, inventory := range inventories {
148
+ if inventory == nil || !governed[address] {
149
+ continue
150
+ }
151
+ reported = append(reported, inventory.Unreadable...)
152
+ }
153
+ sort.Strings(reported)
154
+ return reported
155
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttsc/evidence",
3
- "version": "0.25.0",
3
+ "version": "0.26.0",
4
4
  "description": "Evidence-graph lint contributor for @ttsc/lint: declare provenance with a JSDoc @evidence tag and fail the build when it is missing or dangling.",
5
5
  "main": "lib/index.js",
6
6
  "exports": {
@@ -48,8 +48,8 @@
48
48
  "@types/node": "^25.3.0",
49
49
  "rimraf": "^6.1.2",
50
50
  "typescript": "^7.0.2",
51
- "@ttsc/lint": "0.25.0",
52
- "ttsc": "0.25.0"
51
+ "@ttsc/lint": "0.26.0",
52
+ "ttsc": "0.26.0"
53
53
  },
54
54
  "dependencies": {
55
55
  "@prisma/prisma-schema-wasm": "7.10.0-2.d0368e25c88e68caa4d283b5f2d88d1b76ed9356",