@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,213 @@
|
|
|
1
|
+
package evidence
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"crypto/sha256"
|
|
5
|
+
"encoding/hex"
|
|
6
|
+
"os"
|
|
7
|
+
"strings"
|
|
8
|
+
"sync"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// prismaCacheLimit bounds the cache so a resident host cannot grow without end.
|
|
12
|
+
// A project declares one schema set, so several entries only accumulate as its
|
|
13
|
+
// bytes change; the bound is a backstop against a pathological session rather
|
|
14
|
+
// than a tuning knob.
|
|
15
|
+
const prismaCacheLimit = 16
|
|
16
|
+
|
|
17
|
+
// prismaSchemas remembers parse outcomes by schema content.
|
|
18
|
+
//
|
|
19
|
+
// This is the rule's own state, not the project state the host publishes, for
|
|
20
|
+
// the reason `swaggerDocuments` states at length: the contributor API exposes
|
|
21
|
+
// no disposal hook, both resident hosts are line-protocol loops in one process,
|
|
22
|
+
// and a project rule runs before any file rule so the parallel walk never
|
|
23
|
+
// reaches it.
|
|
24
|
+
var prismaSchemas = newPrismaCache()
|
|
25
|
+
|
|
26
|
+
// prismaSetOutcome is what one schema set's bytes parsed to.
|
|
27
|
+
//
|
|
28
|
+
// Failure is remembered as deliberately as success. A schema the parser rejects
|
|
29
|
+
// is one the author is midway through fixing, and while they fix it every
|
|
30
|
+
// unrelated TypeScript save would otherwise pay a fresh process start to be
|
|
31
|
+
// told the same thing again. Nothing is cached forever: repairing the schema
|
|
32
|
+
// changes its bytes, which changes the key, which misses.
|
|
33
|
+
//
|
|
34
|
+
// Rejected is a separate flag rather than a non-empty Problem, because the
|
|
35
|
+
// reason is the parser's string and this rule does not get to assume it is
|
|
36
|
+
// non-empty. Inferring rejection from the message would turn a reason-less
|
|
37
|
+
// failure into zero models and no diagnostic — a rejected schema that reads
|
|
38
|
+
// exactly like an empty but passing one.
|
|
39
|
+
type prismaSetOutcome struct {
|
|
40
|
+
Models []prismaModel
|
|
41
|
+
Rejected bool
|
|
42
|
+
Problem string
|
|
43
|
+
// digest is the loader's own, carried out of the bridge answer so the
|
|
44
|
+
// caller stores under the bytes the loader read rather than the ones this
|
|
45
|
+
// process read beforehand.
|
|
46
|
+
digest string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
func newPrismaCache() *prismaCache {
|
|
50
|
+
return &prismaCache{entries: map[string]prismaSetOutcome{}}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type prismaCache struct {
|
|
54
|
+
mutex sync.Mutex
|
|
55
|
+
entries map[string]prismaSetOutcome
|
|
56
|
+
order []string
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
func (cache *prismaCache) lookup(digest string) (prismaSetOutcome, bool) {
|
|
60
|
+
if digest == "" {
|
|
61
|
+
return prismaSetOutcome{}, false
|
|
62
|
+
}
|
|
63
|
+
cache.mutex.Lock()
|
|
64
|
+
defer cache.mutex.Unlock()
|
|
65
|
+
outcome, hit := cache.entries[digest]
|
|
66
|
+
if !hit {
|
|
67
|
+
return prismaSetOutcome{}, false
|
|
68
|
+
}
|
|
69
|
+
return copyPrismaOutcome(outcome), true
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
func (cache *prismaCache) store(digest string, outcome prismaSetOutcome) {
|
|
73
|
+
if digest == "" {
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
cache.mutex.Lock()
|
|
77
|
+
defer cache.mutex.Unlock()
|
|
78
|
+
if _, exists := cache.entries[digest]; exists {
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
if len(cache.order) >= prismaCacheLimit {
|
|
82
|
+
delete(cache.entries, cache.order[0])
|
|
83
|
+
cache.order = cache.order[1:]
|
|
84
|
+
}
|
|
85
|
+
cache.entries[digest] = copyPrismaOutcome(outcome)
|
|
86
|
+
cache.order = append(cache.order, digest)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// copyPrismaOutcome deep-copies the models, because a caller builds units from
|
|
90
|
+
// them while another cycle may be reading the same entry.
|
|
91
|
+
func copyPrismaOutcome(outcome prismaSetOutcome) prismaSetOutcome {
|
|
92
|
+
models := make([]prismaModel, 0, len(outcome.Models))
|
|
93
|
+
for _, model := range outcome.Models {
|
|
94
|
+
models = append(models, prismaModel{
|
|
95
|
+
Name: model.Name,
|
|
96
|
+
Documentation: model.Documentation,
|
|
97
|
+
Fields: append([]prismaField(nil), model.Fields...),
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
return prismaSetOutcome{
|
|
101
|
+
Models: models,
|
|
102
|
+
Rejected: outcome.Rejected,
|
|
103
|
+
Problem: outcome.Problem,
|
|
104
|
+
digest: outcome.digest,
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// prismaContentDigest composes one key from every file of the ordered set.
|
|
109
|
+
//
|
|
110
|
+
// The composition is the whole cache key, which is what makes staleness
|
|
111
|
+
// structural rather than improbable: identical bytes in identical order parse
|
|
112
|
+
// to identical models, so a hit can only return what the current schema means.
|
|
113
|
+
// The path is folded in beside each file's own hash, so moving a model between
|
|
114
|
+
// two files of one set — which changes nothing about the bytes as a whole —
|
|
115
|
+
// still changes the key, and so does adding or removing a file.
|
|
116
|
+
//
|
|
117
|
+
// An unreadable file yields no digest at all, which keeps the set out of the
|
|
118
|
+
// cache and leaves the loader's own diagnostic for it.
|
|
119
|
+
func prismaContentDigest(root string, sources []string) string {
|
|
120
|
+
composite := sha256.New()
|
|
121
|
+
for _, source := range sources {
|
|
122
|
+
content, err := os.ReadFile(resolveProjectPath(root, source))
|
|
123
|
+
if err != nil {
|
|
124
|
+
return ""
|
|
125
|
+
}
|
|
126
|
+
file := sha256.Sum256(content)
|
|
127
|
+
composite.Write([]byte(source))
|
|
128
|
+
// NUL, matching `SEPARATOR` in the bridge. A path may contain a space
|
|
129
|
+
// while a hex digest contains nothing outside `[0-9a-f]`, so an
|
|
130
|
+
// ambiguous separator would let two different sets compose one key.
|
|
131
|
+
// Spelled as a byte rather than as a literal character for the reason
|
|
132
|
+
// the bridge records: this separator was once an invisible control
|
|
133
|
+
// character in one source and a space in the other, and the two hashed
|
|
134
|
+
// differently with every result still correct.
|
|
135
|
+
composite.Write([]byte{0})
|
|
136
|
+
composite.Write([]byte(hex.EncodeToString(file[:])))
|
|
137
|
+
composite.Write([]byte("\n"))
|
|
138
|
+
}
|
|
139
|
+
return hex.EncodeToString(composite.Sum(nil))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// rememberPrismaSchema records an outcome under the bytes it was produced from,
|
|
143
|
+
// as reported by the process that produced it.
|
|
144
|
+
//
|
|
145
|
+
// The digest is the loader's own, never the one this process took beforehand.
|
|
146
|
+
// The loader opens the files again, in another process, after this one read
|
|
147
|
+
// them — so keying on the earlier digest would let a write landing inside that
|
|
148
|
+
// window bind one schema's models to another schema's bytes, and that entry
|
|
149
|
+
// would answer every later cycle with the wrong schema.
|
|
150
|
+
func rememberPrismaSchema(digest string, outcome prismaSetOutcome) {
|
|
151
|
+
prismaSchemas.store(digest, outcome)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// prismaUnitsFromOutcome rebuilds the set's units and files them under the
|
|
155
|
+
// schema file each one is written in.
|
|
156
|
+
//
|
|
157
|
+
// Units are rebuilt per cycle rather than remembered, because a unit carries
|
|
158
|
+
// its location while the parsed models do not: what was cached is a property of
|
|
159
|
+
// the schema, not of where its text sits.
|
|
160
|
+
func prismaUnitsFromOutcome(
|
|
161
|
+
root string,
|
|
162
|
+
sources []string,
|
|
163
|
+
inventories map[string]*artifactInventory,
|
|
164
|
+
outcome prismaSetOutcome,
|
|
165
|
+
) []string {
|
|
166
|
+
if outcome.Rejected {
|
|
167
|
+
return []string{failPrismaSet(
|
|
168
|
+
inventories,
|
|
169
|
+
sources,
|
|
170
|
+
prismaNormalizationFailure(outcome.Problem),
|
|
171
|
+
)}
|
|
172
|
+
}
|
|
173
|
+
locations, comments := locatePrismaDeclarations(root, sources)
|
|
174
|
+
fallback := ""
|
|
175
|
+
if len(sources) != 0 {
|
|
176
|
+
fallback = sources[0]
|
|
177
|
+
}
|
|
178
|
+
indexed := prismaInventoriesByDisplay(inventories)
|
|
179
|
+
hosts := map[string]*evidenceUnit{}
|
|
180
|
+
for _, model := range outcome.Models {
|
|
181
|
+
for _, unit := range prismaModelUnits(model) {
|
|
182
|
+
key := joinPrismaIdentity(unit.Identity)
|
|
183
|
+
location, found := locations[key]
|
|
184
|
+
if !found {
|
|
185
|
+
// Locating is subordinate: a name the scan did not find keeps a
|
|
186
|
+
// file-level location and its full participation in coverage,
|
|
187
|
+
// because every file of the set matches the globs that selected
|
|
188
|
+
// it. A missing line costs precision, never an obligation.
|
|
189
|
+
location = prismaLocation{Path: fallback}
|
|
190
|
+
}
|
|
191
|
+
unit.Path = location.Path
|
|
192
|
+
unit.Line = location.Line
|
|
193
|
+
hosts[key] = unit
|
|
194
|
+
for _, inventory := range indexed[unit.Path] {
|
|
195
|
+
inventory.Units = append(inventory.Units, unit)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
for _, inventory := range inventories {
|
|
200
|
+
sortUnits(inventory.Units)
|
|
201
|
+
}
|
|
202
|
+
return prismaDeclarationsFromComments(comments, hosts, indexed)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// joinPrismaIdentity renders a unit's identity the way the locator keys one.
|
|
206
|
+
//
|
|
207
|
+
// A Prisma identifier is unicode alphanumeric plus `_` and `-` and can never
|
|
208
|
+
// contain a dot (`psl/schema-ast/src/parser/datamodel.pest`), so joining on one
|
|
209
|
+
// is lossless here — the ambiguity that forces a TypeScript identity to stay
|
|
210
|
+
// segmented does not exist in this grammar.
|
|
211
|
+
func joinPrismaIdentity(identity []string) string {
|
|
212
|
+
return strings.Join(identity, ".")
|
|
213
|
+
}
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
package evidence
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"os"
|
|
5
|
+
"strings"
|
|
6
|
+
"unicode"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
// prismaLocation is where one declared name is written.
|
|
10
|
+
type prismaLocation struct {
|
|
11
|
+
Path string
|
|
12
|
+
Line int
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// prismaCommentForm distinguishes the three comment syntaxes Prisma's grammar
|
|
16
|
+
// accepts: line comments carry no citation, and only triple-slash documentation
|
|
17
|
+
// can become an unattached file-level exclusion carrier.
|
|
18
|
+
type prismaCommentForm string
|
|
19
|
+
|
|
20
|
+
const (
|
|
21
|
+
prismaDocComment prismaCommentForm = "doc"
|
|
22
|
+
prismaLineComment prismaCommentForm = "line"
|
|
23
|
+
prismaBlockComment prismaCommentForm = "block"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
// prismaCommentRun is one contiguous comment block and the declaration it
|
|
27
|
+
// attaches to.
|
|
28
|
+
//
|
|
29
|
+
// Body preserves the run's own line spacing — a line the run skipped is empty
|
|
30
|
+
// rather than absent — so a tag's offset inside the body is still its offset in
|
|
31
|
+
// the file. Key is the declaration the run documents, empty when it documents
|
|
32
|
+
// nothing unless FileLevel identifies a top-level triple-slash exclusion
|
|
33
|
+
// carrier.
|
|
34
|
+
type prismaCommentRun struct {
|
|
35
|
+
Form prismaCommentForm
|
|
36
|
+
Path string
|
|
37
|
+
Line int
|
|
38
|
+
Body string
|
|
39
|
+
Key string
|
|
40
|
+
FileLevel bool
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// prismaFileScan is everything one schema file yields to the native side.
|
|
44
|
+
type prismaFileScan struct {
|
|
45
|
+
Locations map[string]prismaLocation
|
|
46
|
+
Comments []prismaCommentRun
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// prismaBlockKeywords are the top-level block openers Prisma's grammar accepts.
|
|
50
|
+
//
|
|
51
|
+
// All six are recognized even though only `model`, `view`, and `type` can own a
|
|
52
|
+
// unit, because recognizing a block is how the scan knows when it is *not*
|
|
53
|
+
// inside one. Dropping `datasource` and `generator` would leave their settings
|
|
54
|
+
// looking like members of whatever block was declared before them.
|
|
55
|
+
var prismaBlockKeywords = map[string]bool{
|
|
56
|
+
"model": true,
|
|
57
|
+
"view": true,
|
|
58
|
+
"type": true,
|
|
59
|
+
"enum": true,
|
|
60
|
+
"datasource": true,
|
|
61
|
+
"generator": true,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// prismaMemberBlocks are the blocks whose members this graph addresses.
|
|
65
|
+
var prismaMemberBlocks = map[string]bool{
|
|
66
|
+
"model": true,
|
|
67
|
+
"view": true,
|
|
68
|
+
"type": true,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// locatePrismaDeclarations finds the line each declared name is written on and
|
|
72
|
+
// the comment runs that document them.
|
|
73
|
+
//
|
|
74
|
+
// This is a locator, not a parser. The names it answers for come from Prisma's
|
|
75
|
+
// own parser, which returns no positions at all, so the only questions asked
|
|
76
|
+
// here are *where* a name is written and *what comment precedes it* — never
|
|
77
|
+
// what exists. A name it fails to find loses a precise line and nothing else,
|
|
78
|
+
// which is what makes a native scan safe on a grammar this rule does not own.
|
|
79
|
+
//
|
|
80
|
+
// Two clauses of that grammar do the structural work
|
|
81
|
+
// (`prisma/prisma-engines`, `psl/schema-ast/src/parser/datamodel.pest`):
|
|
82
|
+
// `field_declaration` terminates at `NEWLINE`, and a block opener is likewise
|
|
83
|
+
// its own line. Measured against the real parser, a field whose attribute
|
|
84
|
+
// arguments span two lines and a single-line `model X { ... }` are both hard
|
|
85
|
+
// errors — so "one declaration, one line" is Prisma's rule, inherited rather
|
|
86
|
+
// than approximated.
|
|
87
|
+
//
|
|
88
|
+
// The first spelling of a name wins. A duplicate name across the set is a
|
|
89
|
+
// schema Prisma itself rejects, so this scan never has to arbitrate one.
|
|
90
|
+
func locatePrismaDeclarations(
|
|
91
|
+
root string,
|
|
92
|
+
sources []string,
|
|
93
|
+
) (map[string]prismaLocation, []prismaCommentRun) {
|
|
94
|
+
locations := map[string]prismaLocation{}
|
|
95
|
+
comments := []prismaCommentRun{}
|
|
96
|
+
for _, source := range sources {
|
|
97
|
+
content, err := os.ReadFile(resolveProjectPath(root, source))
|
|
98
|
+
if err != nil {
|
|
99
|
+
continue
|
|
100
|
+
}
|
|
101
|
+
scan := scanPrismaFile(source, string(content), locations)
|
|
102
|
+
comments = append(comments, scan.Comments...)
|
|
103
|
+
}
|
|
104
|
+
return locations, comments
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// scanPrismaFile walks one file once, recording where names are written and
|
|
108
|
+
// which comment run documents each of them.
|
|
109
|
+
//
|
|
110
|
+
// Attachment reproduces what Prisma itself does with a comment, measured rather
|
|
111
|
+
// than inferred, because the two answers differ in a way no reading of the
|
|
112
|
+
// grammar suggests:
|
|
113
|
+
//
|
|
114
|
+
// - A run attaches to the next declaration, and an intervening `//` line does
|
|
115
|
+
// not break it — two `///` lines around one `//` document the same
|
|
116
|
+
// declaration together.
|
|
117
|
+
// - At top level a blank line detaches the run entirely; inside a block it
|
|
118
|
+
// does not. `/// doc` then a blank line then `model Sale` documents
|
|
119
|
+
// nothing, while the same shape above a field still documents that field.
|
|
120
|
+
// - A run followed by a block attribute or by the block's closing brace
|
|
121
|
+
// documents nothing.
|
|
122
|
+
//
|
|
123
|
+
// A run that documents nothing keeps an empty Key rather than being dropped,
|
|
124
|
+
// because a citation written there is a citation that will never be honoured —
|
|
125
|
+
// and this rule reports that rather than letting it pass for a tag that works.
|
|
126
|
+
func scanPrismaFile(
|
|
127
|
+
source string,
|
|
128
|
+
content string,
|
|
129
|
+
locations map[string]prismaLocation,
|
|
130
|
+
) prismaFileScan {
|
|
131
|
+
scan := prismaFileScan{Locations: locations}
|
|
132
|
+
record := func(key string, line int) {
|
|
133
|
+
if key == "" {
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
if _, exists := locations[key]; exists {
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
locations[key] = prismaLocation{Path: source, Line: line}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
lines := strings.Split(content, "\n")
|
|
143
|
+
depth := 0
|
|
144
|
+
block := ""
|
|
145
|
+
// addressable separates "this comment documents nothing" from "this comment
|
|
146
|
+
// documents something this graph cannot cite". An enum, a datasource, and a
|
|
147
|
+
// generator all take a doc comment and none of them owns a unit, so a
|
|
148
|
+
// citation there needs the second diagnostic — the first would tell its
|
|
149
|
+
// author to move a comment that is already exactly where they meant it.
|
|
150
|
+
addressable := false
|
|
151
|
+
commented := false
|
|
152
|
+
pending := []prismaPendingComment{}
|
|
153
|
+
flush := func(key string) {
|
|
154
|
+
scan.Comments = append(scan.Comments, prismaCommentRuns(source, pending, key)...)
|
|
155
|
+
pending = nil
|
|
156
|
+
}
|
|
157
|
+
for index, raw := range lines {
|
|
158
|
+
line := strings.TrimSuffix(raw, "\r")
|
|
159
|
+
code, comment, form, stillCommented := prismaPartsOf(line, commented)
|
|
160
|
+
commented = stillCommented
|
|
161
|
+
trimmed := strings.TrimSpace(code)
|
|
162
|
+
if comment != "" || form != "" {
|
|
163
|
+
pending = append(pending, prismaPendingComment{
|
|
164
|
+
Form: form,
|
|
165
|
+
Line: index + 1,
|
|
166
|
+
Text: comment,
|
|
167
|
+
Trailing: trimmed != "",
|
|
168
|
+
TopLevel: depth == 0,
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
if trimmed == "" {
|
|
172
|
+
// A blank line ends a top-level run. Inside a block it does not,
|
|
173
|
+
// which is measured behaviour rather than a simplification.
|
|
174
|
+
if comment == "" && form == "" && depth == 0 {
|
|
175
|
+
flush("")
|
|
176
|
+
}
|
|
177
|
+
continue
|
|
178
|
+
}
|
|
179
|
+
opens := strings.Count(code, "{")
|
|
180
|
+
closes := strings.Count(code, "}")
|
|
181
|
+
switch {
|
|
182
|
+
case depth == 0 && opens != 0:
|
|
183
|
+
keyword, name, ok := prismaBlockHead(trimmed)
|
|
184
|
+
block, addressable = "", false
|
|
185
|
+
if !ok {
|
|
186
|
+
flush("")
|
|
187
|
+
break
|
|
188
|
+
}
|
|
189
|
+
block = name
|
|
190
|
+
addressable = prismaMemberBlocks[keyword]
|
|
191
|
+
if addressable {
|
|
192
|
+
record(name, index+1)
|
|
193
|
+
}
|
|
194
|
+
flush(name)
|
|
195
|
+
case depth == 1 && block != "":
|
|
196
|
+
name, ok := prismaMemberName(trimmed)
|
|
197
|
+
if !ok {
|
|
198
|
+
// A block attribute or the closing brace. Prisma documents
|
|
199
|
+
// neither, so a run stopping here documents nothing.
|
|
200
|
+
flush("")
|
|
201
|
+
break
|
|
202
|
+
}
|
|
203
|
+
if addressable {
|
|
204
|
+
record(block+"."+name, index+1)
|
|
205
|
+
}
|
|
206
|
+
flush(block + "." + name)
|
|
207
|
+
default:
|
|
208
|
+
flush("")
|
|
209
|
+
}
|
|
210
|
+
depth += opens - closes
|
|
211
|
+
if depth <= 0 {
|
|
212
|
+
depth = 0
|
|
213
|
+
block = ""
|
|
214
|
+
addressable = false
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
flush("")
|
|
218
|
+
return scan
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
type prismaPendingComment struct {
|
|
222
|
+
Form prismaCommentForm
|
|
223
|
+
Line int
|
|
224
|
+
Text string
|
|
225
|
+
// Trailing marks a comment that shared its line with code. Prisma treats
|
|
226
|
+
// such a comment as the field's trailing comment rather than as
|
|
227
|
+
// documentation, so it can never document the declaration below it.
|
|
228
|
+
Trailing bool
|
|
229
|
+
// TopLevel marks a comment written outside every Prisma block. A detached
|
|
230
|
+
// documentation run there may carry claim-local exclusions for the file
|
|
231
|
+
// without becoming part of Prisma's generated schema documentation.
|
|
232
|
+
TopLevel bool
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// prismaCommentRuns groups pending comment lines into one run per form.
|
|
236
|
+
//
|
|
237
|
+
// Only the doc-comment run can carry a citation, so the other two are grouped
|
|
238
|
+
// solely to be reported. Keeping the doc run's body line-aligned is what lets a
|
|
239
|
+
// tag inside it be reported at the line it was written on: a line the run
|
|
240
|
+
// skipped becomes an empty line rather than disappearing.
|
|
241
|
+
func prismaCommentRuns(
|
|
242
|
+
source string,
|
|
243
|
+
pending []prismaPendingComment,
|
|
244
|
+
key string,
|
|
245
|
+
) []prismaCommentRun {
|
|
246
|
+
if len(pending) == 0 {
|
|
247
|
+
return nil
|
|
248
|
+
}
|
|
249
|
+
runs := []prismaCommentRun{}
|
|
250
|
+
documenting := []prismaPendingComment{}
|
|
251
|
+
for _, comment := range pending {
|
|
252
|
+
// `///` and `/* */` are both documentation to Prisma — measured, both
|
|
253
|
+
// reach the parser's `documentation` and both are emitted into the
|
|
254
|
+
// generated client and into prisma-markdown's ERD. Only `//` is
|
|
255
|
+
// discarded, so only `//` cannot carry a citation.
|
|
256
|
+
if comment.Form != prismaLineComment && !comment.Trailing {
|
|
257
|
+
documenting = append(documenting, comment)
|
|
258
|
+
continue
|
|
259
|
+
}
|
|
260
|
+
runs = append(runs, prismaCommentRun{
|
|
261
|
+
Form: comment.Form,
|
|
262
|
+
Path: source,
|
|
263
|
+
Line: comment.Line,
|
|
264
|
+
Body: comment.Text,
|
|
265
|
+
Key: "",
|
|
266
|
+
})
|
|
267
|
+
}
|
|
268
|
+
if len(documenting) != 0 {
|
|
269
|
+
first := documenting[0].Line
|
|
270
|
+
body := make([]string, documenting[len(documenting)-1].Line-first+1)
|
|
271
|
+
fileLevel := key == ""
|
|
272
|
+
for _, comment := range documenting {
|
|
273
|
+
body[comment.Line-first] = comment.Text
|
|
274
|
+
fileLevel =
|
|
275
|
+
fileLevel &&
|
|
276
|
+
comment.TopLevel &&
|
|
277
|
+
comment.Form == prismaDocComment
|
|
278
|
+
}
|
|
279
|
+
runs = append(runs, prismaCommentRun{
|
|
280
|
+
Form: prismaDocComment,
|
|
281
|
+
Path: source,
|
|
282
|
+
Line: first,
|
|
283
|
+
Body: strings.Join(body, "\n"),
|
|
284
|
+
Key: key,
|
|
285
|
+
FileLevel: fileLevel,
|
|
286
|
+
})
|
|
287
|
+
}
|
|
288
|
+
return runs
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// prismaPartsOf splits one line into the code it declares and the comment it
|
|
292
|
+
// carries.
|
|
293
|
+
//
|
|
294
|
+
// String contents are dropped from the code rather than kept, because a brace
|
|
295
|
+
// inside one must not open or close a block — `@default("}")` is a legal field
|
|
296
|
+
// that a naive brace count reads as the end of its model. The comment text is
|
|
297
|
+
// returned rather than discarded, because two of the three forms cannot carry a
|
|
298
|
+
// citation and this rule reports one written there instead of ignoring it.
|
|
299
|
+
func prismaPartsOf(
|
|
300
|
+
line string,
|
|
301
|
+
commented bool,
|
|
302
|
+
) (string, string, prismaCommentForm, bool) {
|
|
303
|
+
var code strings.Builder
|
|
304
|
+
var comment strings.Builder
|
|
305
|
+
form := prismaCommentForm("")
|
|
306
|
+
if commented {
|
|
307
|
+
form = prismaBlockComment
|
|
308
|
+
}
|
|
309
|
+
runes := []rune(line)
|
|
310
|
+
quoted := false
|
|
311
|
+
for index := 0; index < len(runes); index++ {
|
|
312
|
+
char := runes[index]
|
|
313
|
+
if commented {
|
|
314
|
+
if char == '*' && index+1 < len(runes) && runes[index+1] == '/' {
|
|
315
|
+
commented = false
|
|
316
|
+
index++
|
|
317
|
+
continue
|
|
318
|
+
}
|
|
319
|
+
comment.WriteRune(char)
|
|
320
|
+
continue
|
|
321
|
+
}
|
|
322
|
+
if quoted {
|
|
323
|
+
if char == '\\' {
|
|
324
|
+
index++
|
|
325
|
+
continue
|
|
326
|
+
}
|
|
327
|
+
if char == '"' {
|
|
328
|
+
quoted = false
|
|
329
|
+
}
|
|
330
|
+
continue
|
|
331
|
+
}
|
|
332
|
+
if char == '"' {
|
|
333
|
+
quoted = true
|
|
334
|
+
continue
|
|
335
|
+
}
|
|
336
|
+
if char == '/' && index+1 < len(runes) && runes[index+1] == '/' {
|
|
337
|
+
// A third slash makes it a doc comment; a fourth is content, which
|
|
338
|
+
// is what Prisma's `(!"///") ~ "//"` lookahead decides.
|
|
339
|
+
rest := index + 2
|
|
340
|
+
if rest < len(runes) && runes[rest] == '/' {
|
|
341
|
+
if form == "" {
|
|
342
|
+
form = prismaDocComment
|
|
343
|
+
}
|
|
344
|
+
rest++
|
|
345
|
+
} else if form == "" {
|
|
346
|
+
form = prismaLineComment
|
|
347
|
+
}
|
|
348
|
+
comment.WriteString(strings.TrimSpace(string(runes[rest:])))
|
|
349
|
+
break
|
|
350
|
+
}
|
|
351
|
+
if char == '/' && index+1 < len(runes) && runes[index+1] == '*' {
|
|
352
|
+
commented = true
|
|
353
|
+
if form == "" {
|
|
354
|
+
form = prismaBlockComment
|
|
355
|
+
}
|
|
356
|
+
index++
|
|
357
|
+
continue
|
|
358
|
+
}
|
|
359
|
+
code.WriteRune(char)
|
|
360
|
+
}
|
|
361
|
+
return code.String(), strings.TrimSpace(comment.String()), form, commented
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// prismaBlockHead reads `model <Name>` from a block opener.
|
|
365
|
+
func prismaBlockHead(trimmed string) (string, string, bool) {
|
|
366
|
+
fields := strings.Fields(trimmed)
|
|
367
|
+
if len(fields) < 2 {
|
|
368
|
+
return "", "", false
|
|
369
|
+
}
|
|
370
|
+
if !prismaBlockKeywords[fields[0]] {
|
|
371
|
+
return "", "", false
|
|
372
|
+
}
|
|
373
|
+
name := strings.TrimSuffix(fields[1], "{")
|
|
374
|
+
if !isPrismaIdentifier(name) {
|
|
375
|
+
return "", "", false
|
|
376
|
+
}
|
|
377
|
+
return fields[0], name, true
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// prismaMemberName reads the field name a member line opens with.
|
|
381
|
+
//
|
|
382
|
+
// A line opening with `@` is a block attribute such as `@@index([a, b])` rather
|
|
383
|
+
// than a field, and a line opening with `}` closes the block. Neither declares
|
|
384
|
+
// a member, and both would otherwise be read as one named `@@index` or `}`.
|
|
385
|
+
func prismaMemberName(trimmed string) (string, bool) {
|
|
386
|
+
if strings.HasPrefix(trimmed, "@") || strings.HasPrefix(trimmed, "}") {
|
|
387
|
+
return "", false
|
|
388
|
+
}
|
|
389
|
+
fields := strings.Fields(trimmed)
|
|
390
|
+
if len(fields) == 0 {
|
|
391
|
+
return "", false
|
|
392
|
+
}
|
|
393
|
+
if !isPrismaIdentifier(fields[0]) {
|
|
394
|
+
return "", false
|
|
395
|
+
}
|
|
396
|
+
return fields[0], true
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// isPrismaIdentifier mirrors the grammar's `identifier` rule: unicode
|
|
400
|
+
// alphanumeric, then unicode alphanumeric or `_` or `-`. No dot can appear in
|
|
401
|
+
// one, which is what lets a member address join on a dot without ambiguity.
|
|
402
|
+
func isPrismaIdentifier(value string) bool {
|
|
403
|
+
if value == "" {
|
|
404
|
+
return false
|
|
405
|
+
}
|
|
406
|
+
for index, char := range value {
|
|
407
|
+
switch {
|
|
408
|
+
case unicode.IsLetter(char), unicode.IsDigit(char):
|
|
409
|
+
continue
|
|
410
|
+
case index != 0 && (char == '_' || char == '-'):
|
|
411
|
+
continue
|
|
412
|
+
default:
|
|
413
|
+
return false
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return true
|
|
417
|
+
}
|