@ttsc/evidence 0.28.6 → 0.30.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/lib/structures/ITtscEvidenceGraphClaimBase.d.ts +7 -0
- package/lib/structures/ITtscEvidenceGraphReferenceBase.d.ts +7 -0
- package/lib/structures/ITtscEvidenceGraphTypeScriptReference.d.ts +18 -6
- package/lib/typings/TtscEvidenceGraphTypeScriptSymbol.d.ts +11 -12
- package/native/config.go +43 -47
- package/native/declaration.go +50 -1
- package/native/default_exports.go +62 -0
- package/native/export_resolution.go +275 -0
- package/native/exports.go +62 -144
- package/native/file_link.go +370 -0
- package/native/file_link_diagnostics.go +160 -0
- package/native/fingerprint.go +33 -4
- package/native/graph.go +119 -79
- package/native/imports.go +21 -8
- package/native/inputs.go +13 -4
- package/native/loader.go +170 -23
- package/native/markdown.go +17 -11
- package/native/model.go +16 -4
- package/native/prisma.go +33 -26
- package/native/prisma_cache.go +16 -4
- package/native/rooted_typescript.go +116 -0
- package/native/severity.go +156 -0
- package/native/swagger.go +23 -16
- package/native/typescript.go +33 -14
- package/native/typescript_cache.go +5 -4
- package/native/unreadable.go +5 -6
- package/package.json +6 -6
package/native/loader.go
CHANGED
|
@@ -22,12 +22,17 @@ import (
|
|
|
22
22
|
// that symbol is precisely the one an obligation needs to name — an evidence
|
|
23
23
|
// graph exists to report the operation the frontend never called.
|
|
24
24
|
type typeScriptLoader struct {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
boundary *populationBase
|
|
26
|
+
root string
|
|
27
|
+
identityRoot string
|
|
28
|
+
program map[string]*artifactInventory
|
|
29
|
+
programIDs map[string]*artifactInventory
|
|
30
|
+
parsed map[string]*artifactInventory
|
|
31
|
+
resolved map[string]string
|
|
32
|
+
failures map[string]string
|
|
33
|
+
parseFailures map[string]string
|
|
34
|
+
identities map[string]typeScriptModuleIdentity
|
|
35
|
+
installs map[string]installedPackageLocation
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
func newTypeScriptLoader(
|
|
@@ -35,23 +40,40 @@ func newTypeScriptLoader(
|
|
|
35
40
|
program map[string]*artifactInventory,
|
|
36
41
|
) *typeScriptLoader {
|
|
37
42
|
loader := &typeScriptLoader{
|
|
38
|
-
root:
|
|
39
|
-
program:
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
root: strings.ReplaceAll(root, "\\", "/"),
|
|
44
|
+
program: map[string]*artifactInventory{},
|
|
45
|
+
programIDs: map[string]*artifactInventory{},
|
|
46
|
+
parsed: map[string]*artifactInventory{},
|
|
47
|
+
resolved: map[string]string{},
|
|
48
|
+
failures: map[string]string{},
|
|
49
|
+
parseFailures: map[string]string{},
|
|
50
|
+
identities: map[string]typeScriptModuleIdentity{},
|
|
51
|
+
installs: map[string]installedPackageLocation{},
|
|
52
|
+
}
|
|
53
|
+
loader.identityRoot, _ = physicalTypeScriptPath(filepath.FromSlash(loader.root))
|
|
54
|
+
keys := make([]string, 0, len(program))
|
|
55
|
+
for key := range program {
|
|
56
|
+
keys = append(keys, key)
|
|
57
|
+
}
|
|
58
|
+
sort.Strings(keys)
|
|
59
|
+
for _, key := range keys {
|
|
60
|
+
inventory := program[key]
|
|
46
61
|
if inventory == nil || inventory.Path == "" {
|
|
47
62
|
continue
|
|
48
63
|
}
|
|
49
64
|
location := loader.projectPath(inventory.Path)
|
|
65
|
+
identity, _ := loader.moduleIdentity(location)
|
|
66
|
+
if inventory.Address != identity && inventory.Source != nil {
|
|
67
|
+
inventory = typeScriptInventories.scan(typeScriptModuleAddress(location, identity), inventory.Source)
|
|
68
|
+
}
|
|
50
69
|
current := loader.program[location]
|
|
51
70
|
if current == nil ||
|
|
52
71
|
current.Address != current.Path && inventory.Address == inventory.Path {
|
|
53
72
|
loader.program[location] = inventory
|
|
54
73
|
}
|
|
74
|
+
if loader.programIDs[identity] == nil || location == identity {
|
|
75
|
+
loader.programIDs[identity] = inventory
|
|
76
|
+
}
|
|
55
77
|
}
|
|
56
78
|
return loader
|
|
57
79
|
}
|
|
@@ -60,24 +82,41 @@ func newTypeScriptLoader(
|
|
|
60
82
|
//
|
|
61
83
|
// The Program's copy wins when it exists so that a file under edit is read as
|
|
62
84
|
// the editor has it, not as the disk last saw it.
|
|
63
|
-
func (loader *typeScriptLoader) inventory(relative string) *artifactInventory {
|
|
85
|
+
func (loader *typeScriptLoader) inventory(relative string) (result *artifactInventory) {
|
|
64
86
|
if relative == "" {
|
|
65
87
|
return nil
|
|
66
88
|
}
|
|
67
89
|
relative = loader.projectPath(relative)
|
|
68
|
-
if
|
|
90
|
+
if !loader.withinBoundary(relative) {
|
|
91
|
+
loader.failures[relative] = "the re-export leaves the explicitly configured root"
|
|
92
|
+
return nil
|
|
93
|
+
}
|
|
94
|
+
defer func() {
|
|
95
|
+
if loader.boundary != nil && result != nil && result.Source != nil && len(result.Source.Diagnostics()) != 0 {
|
|
96
|
+
diagnostic := result.Source.Diagnostics()[0]
|
|
97
|
+
loader.failures[relative] = "TypeScript syntax error TS" + decimal(int(diagnostic.Code())) + " at line " + decimal(lineAt(result.Source.Text(), diagnostic.Pos()))
|
|
98
|
+
result = nil
|
|
99
|
+
}
|
|
100
|
+
}()
|
|
101
|
+
if inventory := loader.programInventory(relative); inventory != nil {
|
|
69
102
|
return inventory
|
|
70
103
|
}
|
|
71
104
|
if inventory, cached := loader.parsed[relative]; cached {
|
|
105
|
+
if inventory == nil && loader.parseFailures[relative] != "" {
|
|
106
|
+
loader.failures[relative] = loader.parseFailures[relative]
|
|
107
|
+
}
|
|
72
108
|
return inventory
|
|
73
109
|
}
|
|
74
110
|
loader.parsed[relative] = loader.parse(relative)
|
|
111
|
+
if loader.parsed[relative] == nil {
|
|
112
|
+
loader.parseFailures[relative] = loader.failures[relative]
|
|
113
|
+
}
|
|
75
114
|
return loader.parsed[relative]
|
|
76
115
|
}
|
|
77
116
|
|
|
78
117
|
func (loader *typeScriptLoader) parse(relative string) *artifactInventory {
|
|
79
118
|
relative = loader.projectPath(relative)
|
|
80
|
-
content, err := os.ReadFile(
|
|
119
|
+
content, err := os.ReadFile(resolveProjectPath(loader.root, relative))
|
|
81
120
|
if err != nil {
|
|
82
121
|
loader.failures[relative] = err.Error()
|
|
83
122
|
return nil
|
|
@@ -88,7 +127,7 @@ func (loader *typeScriptLoader) parse(relative string) *artifactInventory {
|
|
|
88
127
|
}
|
|
89
128
|
file := shimparser.ParseSourceFile(
|
|
90
129
|
shimast.SourceFileParseOptions{
|
|
91
|
-
FileName:
|
|
130
|
+
FileName: filepath.ToSlash(resolveProjectPath(loader.root, relative)),
|
|
92
131
|
},
|
|
93
132
|
string(content),
|
|
94
133
|
kind,
|
|
@@ -97,7 +136,8 @@ func (loader *typeScriptLoader) parse(relative string) *artifactInventory {
|
|
|
97
136
|
loader.failures[relative] = "the TypeScript parser returned no source file"
|
|
98
137
|
return nil
|
|
99
138
|
}
|
|
100
|
-
|
|
139
|
+
identity, _ := loader.moduleIdentity(relative)
|
|
140
|
+
return scanTypeScriptInventoryAt(typeScriptModuleAddress(relative, identity), file)
|
|
101
141
|
}
|
|
102
142
|
|
|
103
143
|
func (loader *typeScriptLoader) failure(relative string) string {
|
|
@@ -111,19 +151,34 @@ func (loader *typeScriptLoader) failure(relative string) string {
|
|
|
111
151
|
// without paying to scan it.
|
|
112
152
|
func (loader *typeScriptLoader) exists(relative string) bool {
|
|
113
153
|
relative = loader.projectPath(relative)
|
|
114
|
-
if loader.
|
|
154
|
+
if loader.programInventory(relative) != nil {
|
|
115
155
|
return true
|
|
116
156
|
}
|
|
117
157
|
return loader.existsOnDisk(relative)
|
|
118
158
|
}
|
|
119
159
|
|
|
160
|
+
// Authored module paths and physical identity keys are separate namespaces.
|
|
161
|
+
// Mixing them can mistake a sibling of a linked root for a different sibling
|
|
162
|
+
// of the physical project. Identity lookup still recovers its editor snapshot.
|
|
163
|
+
func (loader *typeScriptLoader) programInventory(module string) *artifactInventory {
|
|
164
|
+
if inventory := loader.program[module]; inventory != nil {
|
|
165
|
+
return inventory
|
|
166
|
+
}
|
|
167
|
+
if len(loader.programIDs) != 0 {
|
|
168
|
+
if identity, ok := loader.moduleIdentity(module); ok {
|
|
169
|
+
return loader.programIDs[identity]
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return nil
|
|
173
|
+
}
|
|
174
|
+
|
|
120
175
|
// existsOnDisk probes the filesystem for an already-normalized path.
|
|
121
176
|
//
|
|
122
177
|
// The probe is the expensive half of module resolution — one syscall per
|
|
123
178
|
// candidate, and a watch cycle resolves every re-export in the population — so
|
|
124
179
|
// callers that can answer from the Program should do that first.
|
|
125
180
|
func (loader *typeScriptLoader) existsOnDisk(relative string) bool {
|
|
126
|
-
info, err := os.Stat(
|
|
181
|
+
info, err := os.Stat(resolveProjectPath(loader.root, relative))
|
|
127
182
|
return err == nil && !info.IsDir()
|
|
128
183
|
}
|
|
129
184
|
|
|
@@ -135,6 +190,9 @@ func (loader *typeScriptLoader) resolve(from string, specifier string) string {
|
|
|
135
190
|
return cached
|
|
136
191
|
}
|
|
137
192
|
loader.resolved[key] = loader.resolveUncached(from, specifier)
|
|
193
|
+
if loader.boundary != nil && loader.resolved[key] == "" && loader.failures[from+" -> "+specifier] == "" {
|
|
194
|
+
loader.failures[from+" -> "+specifier] = "the exported module cannot be resolved"
|
|
195
|
+
}
|
|
138
196
|
return loader.resolved[key]
|
|
139
197
|
}
|
|
140
198
|
|
|
@@ -142,12 +200,19 @@ func (loader *typeScriptLoader) resolveUncached(
|
|
|
142
200
|
from string,
|
|
143
201
|
specifier string,
|
|
144
202
|
) string {
|
|
203
|
+
failure := from + " -> " + specifier
|
|
145
204
|
if strings.HasPrefix(specifier, "./") || strings.HasPrefix(specifier, "../") {
|
|
205
|
+
if loader.boundary != nil {
|
|
206
|
+
if canonical, ok := loader.moduleIdentity(from); ok {
|
|
207
|
+
from = filepath.ToSlash(resolveProjectPath(loader.identityRoot, canonical))
|
|
208
|
+
}
|
|
209
|
+
}
|
|
146
210
|
base := path.Clean(path.Join(path.Dir(from), specifier))
|
|
147
211
|
candidates := moduleCandidates(base)
|
|
148
212
|
normalized := make([]string, 0, len(candidates))
|
|
149
213
|
for _, candidate := range candidates {
|
|
150
|
-
|
|
214
|
+
module := loader.projectPath(candidate)
|
|
215
|
+
normalized = append(normalized, module)
|
|
151
216
|
}
|
|
152
217
|
// The Program answers first, and not only because it answers without a
|
|
153
218
|
// syscall. A project that emits beside its sources has both `x.js` and
|
|
@@ -155,12 +220,20 @@ func (loader *typeScriptLoader) resolveUncached(
|
|
|
155
220
|
// resolving an import to emitted JavaScript would read a module whose
|
|
156
221
|
// declarations the graph cannot address.
|
|
157
222
|
for _, candidate := range normalized {
|
|
158
|
-
if loader.
|
|
223
|
+
if loader.programInventory(candidate) != nil {
|
|
224
|
+
if !loader.withinBoundary(candidate) {
|
|
225
|
+
loader.failures[failure] = "the re-export leaves the explicitly configured root"
|
|
226
|
+
return ""
|
|
227
|
+
}
|
|
159
228
|
return candidate
|
|
160
229
|
}
|
|
161
230
|
}
|
|
162
231
|
for _, candidate := range normalized {
|
|
163
232
|
if loader.existsOnDisk(candidate) {
|
|
233
|
+
if !loader.withinBoundary(candidate) {
|
|
234
|
+
loader.failures[failure] = "the re-export leaves the explicitly configured root"
|
|
235
|
+
return ""
|
|
236
|
+
}
|
|
164
237
|
return candidate
|
|
165
238
|
}
|
|
166
239
|
}
|
|
@@ -169,9 +242,83 @@ func (loader *typeScriptLoader) resolveUncached(
|
|
|
169
242
|
if strings.HasPrefix(specifier, "/") {
|
|
170
243
|
return ""
|
|
171
244
|
}
|
|
245
|
+
if loader.boundary != nil {
|
|
246
|
+
loader.failures[failure] = "a package re-export is outside the explicitly configured root"
|
|
247
|
+
return ""
|
|
248
|
+
}
|
|
172
249
|
return loader.resolvePackage(specifier)
|
|
173
250
|
}
|
|
174
251
|
|
|
252
|
+
type typeScriptModuleIdentity struct {
|
|
253
|
+
Path string
|
|
254
|
+
Resolved bool
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Module identity follows directory links and file symlinks, while displayed
|
|
258
|
+
// paths and published module addresses retain their authored spellings.
|
|
259
|
+
// Cache once per evaluation so rooted populations share this filesystem work.
|
|
260
|
+
func (loader *typeScriptLoader) moduleIdentity(module string) (string, bool) {
|
|
261
|
+
module = loader.projectPath(module)
|
|
262
|
+
if cached, exists := loader.identities[module]; exists {
|
|
263
|
+
return cached.Path, cached.Resolved
|
|
264
|
+
}
|
|
265
|
+
absolute := resolveProjectPath(loader.root, module)
|
|
266
|
+
resolved, ok := physicalTypeScriptPath(absolute)
|
|
267
|
+
// Resolve both sides before taking a relative identity. Canonicalizing only
|
|
268
|
+
// the file makes a project junction or Windows 8.3 root leak the checkout's
|
|
269
|
+
// absolute location into every unit ID and invalidate unchanged reviews.
|
|
270
|
+
identity := projectRelativeDisplay(loader.identityRoot, resolved)
|
|
271
|
+
loader.identities[module] = typeScriptModuleIdentity{Path: identity, Resolved: ok}
|
|
272
|
+
return identity, ok
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
func physicalTypeScriptPath(absolute string) (string, bool) {
|
|
276
|
+
current := filepath.Clean(absolute)
|
|
277
|
+
// A directory link can introduce another link in an already-walked parent.
|
|
278
|
+
// Settle the complete path, with the same finite bound as directory chains.
|
|
279
|
+
for range 32 {
|
|
280
|
+
resolved, ok := resolveLinkedPath(current)
|
|
281
|
+
if !ok {
|
|
282
|
+
return resolved, false
|
|
283
|
+
}
|
|
284
|
+
resolved = expandTypeScriptPath(resolved)
|
|
285
|
+
if resolved == current {
|
|
286
|
+
return resolved, true
|
|
287
|
+
}
|
|
288
|
+
current = resolved
|
|
289
|
+
}
|
|
290
|
+
return current, false
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// An unsaved or deleted file still has an identity. Expand the deepest existing
|
|
294
|
+
// prefix so Windows short names do not reappear when EvalSymlinks cannot read
|
|
295
|
+
// the complete path, then restore the missing suffix without changing its case.
|
|
296
|
+
func expandTypeScriptPath(absolute string) string {
|
|
297
|
+
probe := absolute
|
|
298
|
+
suffix := []string{}
|
|
299
|
+
for {
|
|
300
|
+
if final, err := filepath.EvalSymlinks(probe); err == nil {
|
|
301
|
+
if !strings.EqualFold(final, probe) {
|
|
302
|
+
probe = final
|
|
303
|
+
}
|
|
304
|
+
for index := len(suffix) - 1; index >= 0; index-- {
|
|
305
|
+
probe = filepath.Join(probe, suffix[index])
|
|
306
|
+
}
|
|
307
|
+
return probe
|
|
308
|
+
}
|
|
309
|
+
parent := filepath.Dir(probe)
|
|
310
|
+
if parent == probe {
|
|
311
|
+
return absolute
|
|
312
|
+
}
|
|
313
|
+
suffix = append(suffix, filepath.Base(probe))
|
|
314
|
+
probe = parent
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
func typeScriptModuleAddress(display string, identity string) artifactAddress {
|
|
319
|
+
return artifactAddress{Base: populationBase{Default: true}, Relative: identity, Display: display, Key: identity}
|
|
320
|
+
}
|
|
321
|
+
|
|
175
322
|
// projectPath gives every Program source and module candidate one identity
|
|
176
323
|
// relative to the ttsc project. A rooted claim may name the same physical file
|
|
177
324
|
// through `../api`, while an import can arrive there through a different
|
package/native/markdown.go
CHANGED
|
@@ -23,9 +23,9 @@ var explicitAnchorPattern = regexp.MustCompile(`\s*\{#([A-Za-z0-9][A-Za-z0-9._:-
|
|
|
23
23
|
func loadMarkdownInventories(
|
|
24
24
|
root string,
|
|
25
25
|
config graphConfig,
|
|
26
|
-
) (map[string]*artifactInventory,
|
|
26
|
+
) (map[string]*artifactInventory, graphDiagnostics) {
|
|
27
27
|
inventories := map[string]*artifactInventory{}
|
|
28
|
-
problems :=
|
|
28
|
+
problems := graphDiagnostics{}
|
|
29
29
|
for _, base := range configuredBases(config, artifactMarkdown) {
|
|
30
30
|
problems = append(
|
|
31
31
|
problems,
|
|
@@ -39,16 +39,17 @@ func loadMarkdownBase(
|
|
|
39
39
|
base populationBase,
|
|
40
40
|
config graphConfig,
|
|
41
41
|
inventories map[string]*artifactInventory,
|
|
42
|
-
)
|
|
43
|
-
problems :=
|
|
42
|
+
) graphDiagnostics {
|
|
43
|
+
problems := graphDiagnostics{}
|
|
44
|
+
severity := populationSeverity(config, artifactMarkdown, base, "", "*", false)
|
|
44
45
|
if problem := baseDirectoryProblem(base, artifactMarkdown); problem != "" {
|
|
45
46
|
recordPopulationFailure(inventories, artifactMarkdown, base)
|
|
46
|
-
return
|
|
47
|
+
return problems.add(severity, problem)
|
|
47
48
|
}
|
|
48
49
|
from, resolved := resolvedBaseDirectory(base)
|
|
49
50
|
if !resolved {
|
|
50
51
|
recordPopulationFailure(inventories, artifactMarkdown, base)
|
|
51
|
-
return
|
|
52
|
+
return problems.add(severity, unresolvedBaseProblem(base, artifactMarkdown))
|
|
52
53
|
}
|
|
53
54
|
err := filepath.WalkDir(from, func(current string, entry fs.DirEntry, walkErr error) error {
|
|
54
55
|
if walkErr != nil {
|
|
@@ -82,7 +83,8 @@ func loadMarkdownBase(
|
|
|
82
83
|
)
|
|
83
84
|
if relevant {
|
|
84
85
|
recordPopulationFailure(inventories, artifactMarkdown, base)
|
|
85
|
-
|
|
86
|
+
relative, _ := relativeProjectPath(from, current)
|
|
87
|
+
problems = problems.add(populationSeverity(config, artifactMarkdown, base, relative, "*", true), problem)
|
|
86
88
|
}
|
|
87
89
|
// `WalkDir` passes a nil entry only for its root, which the guard above
|
|
88
90
|
// answers, so this error belongs to a directory whose listing failed and
|
|
@@ -105,6 +107,7 @@ func loadMarkdownBase(
|
|
|
105
107
|
if !matchesConfiguredMarkdownFile(config, base, relative) {
|
|
106
108
|
return nil
|
|
107
109
|
}
|
|
110
|
+
severity := populationSeverity(config, artifactMarkdown, base, relative, "*", false)
|
|
108
111
|
address := base.addressOf(relative)
|
|
109
112
|
content, readErr := os.ReadFile(current)
|
|
110
113
|
if readErr != nil {
|
|
@@ -113,26 +116,29 @@ func loadMarkdownBase(
|
|
|
113
116
|
Type: artifactMarkdown,
|
|
114
117
|
LoadFailed: true,
|
|
115
118
|
}
|
|
116
|
-
problems =
|
|
119
|
+
problems = problems.add(
|
|
120
|
+
severity,
|
|
121
|
+
"Evidence graph could not read Markdown file '"+address.Display+"': "+causeText(readErr)+". Fix filesystem access or exclude the file from configured globs.",
|
|
122
|
+
)
|
|
117
123
|
return nil
|
|
118
124
|
}
|
|
119
125
|
inventory, _ := scanMarkdownInventory(address, string(content))
|
|
120
126
|
inventories[address.Key] = inventory
|
|
121
127
|
for _, inventoryProblem := range inventory.Problems {
|
|
122
128
|
if selectedByMarkdownPopulation(config, base, relative, inventoryProblem.Symbol) {
|
|
123
|
-
problems =
|
|
129
|
+
problems = problems.add(populationSeverity(config, artifactMarkdown, base, relative, inventoryProblem.Symbol, false), inventoryProblem.Message)
|
|
124
130
|
}
|
|
125
131
|
}
|
|
126
132
|
// An unreadable tag is not a health question and not a symbol question
|
|
127
133
|
// either: the file loaded, its units are complete, and the tag reaches no
|
|
128
134
|
// host whichever symbol a reference selects. The walk already refuses a
|
|
129
135
|
// path no configured glob takes, so reaching here is enough to report.
|
|
130
|
-
problems =
|
|
136
|
+
problems = problems.add(severity, inventory.Unreadable...)
|
|
131
137
|
return nil
|
|
132
138
|
})
|
|
133
139
|
if err != nil {
|
|
134
140
|
recordPopulationFailure(inventories, artifactMarkdown, base)
|
|
135
|
-
problems =
|
|
141
|
+
problems = problems.add(severity, unlistableBaseProblem(base, "Markdown", err))
|
|
136
142
|
}
|
|
137
143
|
return problems
|
|
138
144
|
}
|
package/native/model.go
CHANGED
|
@@ -5,6 +5,7 @@ import (
|
|
|
5
5
|
"strings"
|
|
6
6
|
|
|
7
7
|
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
8
|
+
"github.com/samchon/ttsc/packages/lint/rule"
|
|
8
9
|
)
|
|
9
10
|
|
|
10
11
|
const graphRuleName = "evidence/graph"
|
|
@@ -57,6 +58,8 @@ type graphConfig struct {
|
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
type claimSpec struct {
|
|
61
|
+
Severity *rule.Severity
|
|
62
|
+
Level rule.Severity
|
|
60
63
|
Index int
|
|
61
64
|
Type artifactKind
|
|
62
65
|
Name string
|
|
@@ -84,10 +87,15 @@ type claimSpec struct {
|
|
|
84
87
|
}
|
|
85
88
|
|
|
86
89
|
type referenceSpec struct {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
90
|
+
Severity *rule.Severity
|
|
91
|
+
Level rule.Severity
|
|
92
|
+
Index int
|
|
93
|
+
Type artifactKind
|
|
94
|
+
Policy referencePolicy
|
|
95
|
+
Root string
|
|
96
|
+
// Rooted preserves explicit disk-loading opt-in even when '.' normalizes
|
|
97
|
+
// to the empty spelling of the project root.
|
|
98
|
+
Rooted bool
|
|
91
99
|
Base populationBase
|
|
92
100
|
Files globSet
|
|
93
101
|
Source string
|
|
@@ -328,6 +336,8 @@ func (declaration *evidenceDeclaration) valid() bool {
|
|
|
328
336
|
}
|
|
329
337
|
|
|
330
338
|
type artifactInventory struct {
|
|
339
|
+
// Source supplies declaration diagnostics and the active editor snapshot.
|
|
340
|
+
Source *shimast.SourceFile
|
|
331
341
|
// Address is the population-relative identity used for units and
|
|
332
342
|
// declarations. It differs from Path when a configured root moves the
|
|
333
343
|
// address space while a diagnostic keeps naming the file the way a reader
|
|
@@ -444,6 +454,8 @@ type claimState struct {
|
|
|
444
454
|
}
|
|
445
455
|
|
|
446
456
|
type referenceState struct {
|
|
457
|
+
// Code resolves finite accessor paths independently of population traversal.
|
|
458
|
+
Code *typeScriptExportResolver
|
|
447
459
|
Spec referenceSpec
|
|
448
460
|
Paths []string
|
|
449
461
|
Units []*evidenceUnit
|
package/native/prisma.go
CHANGED
|
@@ -12,6 +12,8 @@ import (
|
|
|
12
12
|
"sort"
|
|
13
13
|
"strings"
|
|
14
14
|
"time"
|
|
15
|
+
|
|
16
|
+
"github.com/samchon/ttsc/packages/lint/rule"
|
|
15
17
|
)
|
|
16
18
|
|
|
17
19
|
const prismaBridgeTimeout = 60 * time.Second
|
|
@@ -127,7 +129,7 @@ type prismaField struct {
|
|
|
127
129
|
func loadPrismaInventories(
|
|
128
130
|
root string,
|
|
129
131
|
config graphConfig,
|
|
130
|
-
) (map[string]*artifactInventory,
|
|
132
|
+
) (map[string]*artifactInventory, graphDiagnostics) {
|
|
131
133
|
// A graph that names no Prisma glob never pays for one. The walk below is
|
|
132
134
|
// cheap on such a project — every directory fails the descendant test and is
|
|
133
135
|
// skipped at once — but "cheap" is still a full listing of the project root
|
|
@@ -160,27 +162,28 @@ func loadPrismaInventories(
|
|
|
160
162
|
// ten-model schema and a two-hundred-model one pay nearly the same. A
|
|
161
163
|
// resident host repeats this every cycle, so an unchanged schema would
|
|
162
164
|
// otherwise be re-parsed on every TypeScript keystroke that rebuilds.
|
|
165
|
+
severity := prismaSetSeverity(config, inventories)
|
|
163
166
|
digest := prismaContentDigest(root, set.Sources)
|
|
164
167
|
if outcome, hit := prismaSchemas.lookup(digest); hit {
|
|
165
168
|
return inventories, append(
|
|
166
169
|
problems,
|
|
167
|
-
prismaUnitsFromOutcome(root, set, inventories, outcome)...,
|
|
170
|
+
prismaUnitsFromOutcome(root, set, inventories, outcome, config)...,
|
|
168
171
|
)
|
|
169
172
|
}
|
|
170
173
|
|
|
171
174
|
result, err := normalizePrismaSet(root, set.Sources)
|
|
172
175
|
if err != nil {
|
|
173
176
|
message := "Evidence graph could not run its Prisma schema loader: " + causeText(err) + ". Prisma references require Node.js and a resolvable @prisma/prisma-schema-wasm."
|
|
174
|
-
return inventories,
|
|
177
|
+
return inventories, problems.add(severity, failPrismaSet(inventories, set, message))
|
|
175
178
|
}
|
|
176
179
|
|
|
177
180
|
outcome, problem := prismaOutcomeOf(result)
|
|
178
181
|
if problem != "" {
|
|
179
|
-
return inventories,
|
|
182
|
+
return inventories, problems.add(severity, failPrismaSet(inventories, set, problem))
|
|
180
183
|
}
|
|
181
184
|
problems = append(
|
|
182
185
|
problems,
|
|
183
|
-
prismaUnitsFromOutcome(root, set, inventories, outcome)...,
|
|
186
|
+
prismaUnitsFromOutcome(root, set, inventories, outcome, config)...,
|
|
184
187
|
)
|
|
185
188
|
rememberPrismaSchema(outcome.digest, outcome)
|
|
186
189
|
return inventories, problems
|
|
@@ -223,27 +226,28 @@ func prismaOutcomeOf(result prismaNormalizationResult) (prismaSetOutcome, string
|
|
|
223
226
|
// Order is the digest's, so it must not depend on filesystem enumeration.
|
|
224
227
|
func configuredPrismaAddresses(
|
|
225
228
|
config graphConfig,
|
|
226
|
-
) ([]artifactAddress,
|
|
229
|
+
) ([]artifactAddress, graphDiagnostics) {
|
|
227
230
|
addresses, _, problems := configuredPrismaAddressesWithHealth(config)
|
|
228
231
|
return addresses, problems
|
|
229
232
|
}
|
|
230
233
|
|
|
231
234
|
func configuredPrismaAddressesWithHealth(
|
|
232
235
|
config graphConfig,
|
|
233
|
-
) ([]artifactAddress, []populationBase,
|
|
236
|
+
) ([]artifactAddress, []populationBase, graphDiagnostics) {
|
|
234
237
|
addresses := []artifactAddress{}
|
|
235
238
|
failedBases := []populationBase{}
|
|
236
|
-
problems :=
|
|
239
|
+
problems := graphDiagnostics{}
|
|
237
240
|
for _, base := range configuredBases(config, artifactPrisma) {
|
|
241
|
+
severity := populationSeverity(config, artifactPrisma, base, "", "*", false)
|
|
238
242
|
if problem := baseDirectoryProblem(base, artifactPrisma); problem != "" {
|
|
239
|
-
problems =
|
|
243
|
+
problems = problems.add(severity, problem)
|
|
240
244
|
failedBases = append(failedBases, base)
|
|
241
245
|
continue
|
|
242
246
|
}
|
|
243
247
|
baseFailed := false
|
|
244
248
|
from, resolved := resolvedBaseDirectory(base)
|
|
245
249
|
if !resolved {
|
|
246
|
-
problems =
|
|
250
|
+
problems = problems.add(severity, unresolvedBaseProblem(base, artifactPrisma))
|
|
247
251
|
failedBases = append(failedBases, base)
|
|
248
252
|
continue
|
|
249
253
|
}
|
|
@@ -272,7 +276,8 @@ func configuredPrismaAddressesWithHealth(
|
|
|
272
276
|
)
|
|
273
277
|
if relevant {
|
|
274
278
|
baseFailed = true
|
|
275
|
-
|
|
279
|
+
relative, _ := relativeProjectPath(from, current)
|
|
280
|
+
problems = problems.add(populationSeverity(config, artifactPrisma, base, relative, "*", true), problem)
|
|
276
281
|
}
|
|
277
282
|
return filepath.SkipDir
|
|
278
283
|
}
|
|
@@ -295,7 +300,7 @@ func configuredPrismaAddressesWithHealth(
|
|
|
295
300
|
})
|
|
296
301
|
if err != nil {
|
|
297
302
|
baseFailed = true
|
|
298
|
-
problems =
|
|
303
|
+
problems = problems.add(severity, unlistableBaseProblem(base, "Prisma", err))
|
|
299
304
|
}
|
|
300
305
|
if baseFailed {
|
|
301
306
|
failedBases = append(failedBases, base)
|
|
@@ -647,15 +652,17 @@ func prismaDeclarationsFromComments(
|
|
|
647
652
|
comments []prismaCommentRun,
|
|
648
653
|
hosts map[string]*evidenceUnit,
|
|
649
654
|
inventories map[string][]*artifactInventory,
|
|
650
|
-
|
|
651
|
-
|
|
655
|
+
levels map[string]rule.Severity,
|
|
656
|
+
) graphDiagnostics {
|
|
657
|
+
problems := graphDiagnostics{}
|
|
652
658
|
sequence := 0
|
|
653
659
|
for _, run := range comments {
|
|
660
|
+
severity := levels[run.Path]
|
|
654
661
|
location := run.Path + ":" + decimal(run.Line)
|
|
655
662
|
if run.Form != prismaDocComment {
|
|
656
663
|
if prismaCommentCarriesTag(run.Body) {
|
|
657
|
-
problems =
|
|
658
|
-
|
|
664
|
+
problems = problems.add(
|
|
665
|
+
severity,
|
|
659
666
|
"Evidence tag at "+location+" sits in a '//' line comment, which Prisma discards rather than attaching to the declaration below it. Write the citation on a '///' or '/* */' documentation comment directly above the model, column, or relation it grounds.",
|
|
660
667
|
)
|
|
661
668
|
}
|
|
@@ -667,8 +674,8 @@ func prismaDeclarationsFromComments(
|
|
|
667
674
|
continue
|
|
668
675
|
}
|
|
669
676
|
for _, offset := range prismaBuriedTagLines(run.Body) {
|
|
670
|
-
problems =
|
|
671
|
-
|
|
677
|
+
problems = problems.add(
|
|
678
|
+
severity,
|
|
672
679
|
"Evidence tag at "+run.Path+":"+decimal(run.Line+offset)+" is buried behind an extra slash. Prisma reads a fourth slash as content, so the tag no longer opens its documentation line and nothing resolves it. Write exactly three slashes.",
|
|
673
680
|
)
|
|
674
681
|
}
|
|
@@ -676,8 +683,8 @@ func prismaDeclarationsFromComments(
|
|
|
676
683
|
sequence++
|
|
677
684
|
line := run.Line + parsed.LineOffset
|
|
678
685
|
if parsed.Tag == tagEvidence {
|
|
679
|
-
problems =
|
|
680
|
-
|
|
686
|
+
problems = problems.add(
|
|
687
|
+
severity,
|
|
681
688
|
"@evidence at "+run.Path+":"+decimal(line)+" is on a file-level Prisma exclusion carrier. Move ownership evidence directly above the selected model, column, or relation it grounds; only @evidenceExclude may be unattached at file level.",
|
|
682
689
|
)
|
|
683
690
|
continue
|
|
@@ -724,8 +731,8 @@ func prismaDeclarationsFromComments(
|
|
|
724
731
|
}
|
|
725
732
|
if run.Key == "" {
|
|
726
733
|
if prismaCommentCarriesTag(run.Body) {
|
|
727
|
-
problems =
|
|
728
|
-
|
|
734
|
+
problems = problems.add(
|
|
735
|
+
severity,
|
|
729
736
|
"Evidence tag at "+location+" documents no declaration. Prisma attaches a '///' comment to the declaration that immediately follows it, so a blank line before a top-level block, a block attribute, or a closing brace leaves the comment documenting nothing. Move the citation directly above the model, column, or relation it grounds.",
|
|
730
737
|
)
|
|
731
738
|
}
|
|
@@ -734,8 +741,8 @@ func prismaDeclarationsFromComments(
|
|
|
734
741
|
host := hosts[run.Key]
|
|
735
742
|
if host == nil {
|
|
736
743
|
if prismaCommentCarriesTag(run.Body) {
|
|
737
|
-
problems =
|
|
738
|
-
|
|
744
|
+
problems = problems.add(
|
|
745
|
+
severity,
|
|
739
746
|
"Evidence tag at "+location+" documents '"+run.Key+"', which is not a model, column, or relation. A model or a view and their members host an evidence citation; an enum, a composite type, and a datasource or generator setting do not.",
|
|
740
747
|
)
|
|
741
748
|
}
|
|
@@ -746,8 +753,8 @@ func prismaDeclarationsFromComments(
|
|
|
746
753
|
continue
|
|
747
754
|
}
|
|
748
755
|
for _, offset := range prismaBuriedTagLines(run.Body) {
|
|
749
|
-
problems =
|
|
750
|
-
|
|
756
|
+
problems = problems.add(
|
|
757
|
+
severity,
|
|
751
758
|
"Evidence tag at "+run.Path+":"+decimal(run.Line+offset)+" is buried behind an extra slash. Prisma reads a fourth slash as content, so the tag no longer opens its documentation line and nothing resolves it. Write exactly three slashes.",
|
|
752
759
|
)
|
|
753
760
|
}
|
package/native/prisma_cache.go
CHANGED
|
@@ -6,6 +6,8 @@ import (
|
|
|
6
6
|
"os"
|
|
7
7
|
"strings"
|
|
8
8
|
"sync"
|
|
9
|
+
|
|
10
|
+
"github.com/samchon/ttsc/packages/lint/rule"
|
|
9
11
|
)
|
|
10
12
|
|
|
11
13
|
// prismaCacheLimit bounds the cache so a resident host cannot grow without end.
|
|
@@ -176,13 +178,14 @@ func prismaUnitsFromOutcome(
|
|
|
176
178
|
set prismaSourceSet,
|
|
177
179
|
inventories map[string]*artifactInventory,
|
|
178
180
|
outcome prismaSetOutcome,
|
|
179
|
-
|
|
181
|
+
config graphConfig,
|
|
182
|
+
) graphDiagnostics {
|
|
180
183
|
if outcome.Rejected {
|
|
181
|
-
return
|
|
184
|
+
return graphDiagnostics{}.add(prismaSetSeverity(config, inventories), failPrismaSet(
|
|
182
185
|
inventories,
|
|
183
186
|
set,
|
|
184
187
|
prismaNormalizationFailure(outcome.Problem),
|
|
185
|
-
)
|
|
188
|
+
))
|
|
186
189
|
}
|
|
187
190
|
locations, comments := locatePrismaDeclarations(root, set.Sources)
|
|
188
191
|
fallback := ""
|
|
@@ -230,7 +233,16 @@ func prismaUnitsFromOutcome(
|
|
|
230
233
|
for _, inventory := range inventories {
|
|
231
234
|
sortUnits(inventory.Units)
|
|
232
235
|
}
|
|
233
|
-
|
|
236
|
+
levels := map[string]rule.Severity{}
|
|
237
|
+
for key, inventory := range inventories {
|
|
238
|
+
levels[inventory.Path] = max(levels[inventory.Path], inventorySeverity(config, artifactPrisma, key, "*"))
|
|
239
|
+
}
|
|
240
|
+
for _, source := range set.Sources {
|
|
241
|
+
for _, spelling := range set.Spellings[source] {
|
|
242
|
+
levels[source] = max(levels[source], levels[spelling])
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return prismaDeclarationsFromComments(comments, hosts, indexed, levels)
|
|
234
246
|
}
|
|
235
247
|
|
|
236
248
|
// joinPrismaIdentity renders a unit's identity the way the locator keys one.
|