@ttsc/evidence 0.26.2 → 0.28.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/README.md +82 -169
- package/lib/structures/ITtscEvidenceGraphClaimBase.d.ts +15 -5
- package/lib/structures/ITtscEvidenceGraphMarkdownReference.d.ts +70 -6
- package/lib/structures/ITtscEvidenceGraphPrismaClaim.d.ts +5 -0
- package/lib/structures/ITtscEvidenceGraphPrismaReference.d.ts +16 -5
- package/lib/structures/ITtscEvidenceGraphReferenceBase.d.ts +8 -0
- package/native/base.go +517 -22
- package/native/config.go +114 -5
- package/native/graph.go +332 -70
- package/native/graph_nodes.go +121 -0
- package/native/loader.go +30 -7
- package/native/markdown.go +91 -28
- package/native/model.go +49 -16
- package/native/prisma.go +167 -48
- package/native/prisma_cache.go +40 -16
- package/native/swagger.go +1 -1
- package/native/swagger_cache.go +1 -1
- package/native/typescript.go +120 -9
- package/package.json +5 -5
package/native/base.go
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
package evidence
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
+
"errors"
|
|
5
|
+
"io/fs"
|
|
4
6
|
"os"
|
|
5
7
|
"path"
|
|
6
8
|
"path/filepath"
|
|
@@ -29,10 +31,22 @@ type populationBase struct {
|
|
|
29
31
|
// Absolute is the canonical directory, and the identity two populations are
|
|
30
32
|
// judged the same base by.
|
|
31
33
|
Absolute string
|
|
32
|
-
//
|
|
34
|
+
// Declared is the author's own spelling of this base, empty for the default
|
|
35
|
+
// base. It sits beside the derived Display because a message that names a
|
|
36
|
+
// configuration property and a message that names a location want different
|
|
37
|
+
// spellings; `populationRootLabel` owns that split and the reasoning for it.
|
|
38
|
+
//
|
|
39
|
+
// It arrives already normalized and is stored untouched. `normalizeRootPath`
|
|
40
|
+
// owns that step, refusing the two forms a root may not take and reducing the
|
|
41
|
+
// rest to one slash-separated spelling before a project identity exists to
|
|
42
|
+
// resolve against — so a second normalization here would be a branch no
|
|
43
|
+
// configuration can reach.
|
|
44
|
+
Declared string
|
|
45
|
+
// Display is what a location names: project-relative, ascending with `..`
|
|
33
46
|
// when the base sits above the project, and absolute only when no relative
|
|
34
|
-
// spelling exists
|
|
35
|
-
//
|
|
47
|
+
// spelling exists, which on Windows is another volume or a UNC share against a
|
|
48
|
+
// drive-lettered project. Empty for the default base, whose files are already
|
|
49
|
+
// named by their project-relative path.
|
|
36
50
|
Display string
|
|
37
51
|
// Default marks the base a population takes when it declares no root. Its
|
|
38
52
|
// addresses stay plain project-relative paths, which is what leaves every
|
|
@@ -64,10 +78,20 @@ func (base populationBase) addressOf(relative string) artifactAddress {
|
|
|
64
78
|
}
|
|
65
79
|
|
|
66
80
|
// display spells a base-relative path the way a reader must open it.
|
|
81
|
+
//
|
|
82
|
+
// The separator is added only where one is missing. A base with no relative
|
|
83
|
+
// spelling, which on Windows is another volume or a UNC share against a
|
|
84
|
+
// drive-lettered project, carries its absolute path as its Display, and a drive
|
|
85
|
+
// root is the one directory whose own separator is part of it: joining `D:/` to
|
|
86
|
+
// a path without this would print `D://requirements`, in every file location as
|
|
87
|
+
// well as in the two walk messages.
|
|
67
88
|
func (base populationBase) display(relative string) string {
|
|
68
89
|
if base.Display == "" {
|
|
69
90
|
return relative
|
|
70
91
|
}
|
|
92
|
+
if strings.HasSuffix(base.Display, "/") {
|
|
93
|
+
return base.Display + relative
|
|
94
|
+
}
|
|
71
95
|
return base.Display + "/" + relative
|
|
72
96
|
}
|
|
73
97
|
|
|
@@ -127,13 +151,16 @@ func resolveProjectPath(root string, relative string) string {
|
|
|
127
151
|
// A declared root that resolves back onto the project root is the default base
|
|
128
152
|
// rather than a second one spelled differently, so `root: "."` and an omitted
|
|
129
153
|
// root produce one population instead of two that address the same files
|
|
130
|
-
// differently.
|
|
154
|
+
// differently. That collapse is also why only a non-default base carries a
|
|
155
|
+
// declared spelling: the base every un-rooted population shares names no
|
|
156
|
+
// configuration property, and giving it one would be a second identity for the
|
|
157
|
+
// one thing this branch exists to keep single.
|
|
131
158
|
func resolvePopulationBase(root string, declared string) populationBase {
|
|
132
159
|
if declared == "" {
|
|
133
160
|
return populationBase{Absolute: root, Default: true}
|
|
134
161
|
}
|
|
135
162
|
absolute := filepath.FromSlash(declared)
|
|
136
|
-
if !
|
|
163
|
+
if !declaredRootIsAbsolute(declared) {
|
|
137
164
|
absolute = filepath.Join(root, absolute)
|
|
138
165
|
}
|
|
139
166
|
absolute = filepath.Clean(absolute)
|
|
@@ -142,16 +169,41 @@ func resolvePopulationBase(root string, declared string) populationBase {
|
|
|
142
169
|
}
|
|
143
170
|
return populationBase{
|
|
144
171
|
Absolute: absolute,
|
|
172
|
+
Declared: declared,
|
|
145
173
|
Display: projectRelativeDisplay(root, absolute),
|
|
146
174
|
}
|
|
147
175
|
}
|
|
148
176
|
|
|
177
|
+
// declaredRootIsAbsolute reports whether a declared root names its own location
|
|
178
|
+
// rather than one below the ttsc project root.
|
|
179
|
+
//
|
|
180
|
+
// One predicate answers this for the resolution and for every message that
|
|
181
|
+
// describes it, so the two cannot disagree. A sentence telling an author that
|
|
182
|
+
// their root resolves against the project root, printed over a path the
|
|
183
|
+
// resolution never joined to anything, is the false clause this diagnostic
|
|
184
|
+
// exists to remove — and it would return the moment a message re-derived the
|
|
185
|
+
// question with a test of its own.
|
|
186
|
+
//
|
|
187
|
+
// Windows answers it the way `filepath.Join` does: a rooted path carrying no
|
|
188
|
+
// volume, such as `/srv/contracts`, is relative there and is joined to the
|
|
189
|
+
// project, so a message reading this predicate keeps telling the truth on that
|
|
190
|
+
// platform while one reading the spelling would not.
|
|
191
|
+
func declaredRootIsAbsolute(declared string) bool {
|
|
192
|
+
return filepath.IsAbs(filepath.FromSlash(declared))
|
|
193
|
+
}
|
|
194
|
+
|
|
149
195
|
// projectRelativeDisplay spells a directory the way the project sees it.
|
|
150
196
|
//
|
|
151
197
|
// A relative spelling is preferred even when it ascends, because every other
|
|
152
|
-
//
|
|
153
|
-
// each other. An absolute path appears only when no relative one exists
|
|
154
|
-
// different Windows volume
|
|
198
|
+
// location this rule prints is project-relative and a reader compares them
|
|
199
|
+
// against each other. An absolute path appears only when no relative one exists
|
|
200
|
+
// — a different Windows volume, or a UNC share against a drive-letter project —
|
|
201
|
+
// where the alternative would be no spelling at all.
|
|
202
|
+
//
|
|
203
|
+
// The `root` property a diagnostic asks an author to correct is the one printed
|
|
204
|
+
// path this no longer decides. `populationRootLabel` spells that as the
|
|
205
|
+
// configuration does, because it is a different reader doing a different thing:
|
|
206
|
+
// editing a file rather than opening one.
|
|
155
207
|
func projectRelativeDisplay(root string, absolute string) string {
|
|
156
208
|
relative, err := filepath.Rel(root, absolute)
|
|
157
209
|
if err != nil {
|
|
@@ -187,6 +239,14 @@ func resolveGraphBases(root string, config *graphConfig) {
|
|
|
187
239
|
// Order is fixed rather than incidental because a loader walks these in
|
|
188
240
|
// sequence and its diagnostics are reported in the order they were produced. A
|
|
189
241
|
// map iteration here would make an unreadable root's message move between runs.
|
|
242
|
+
//
|
|
243
|
+
// Two populations that reach one directory through different spellings are one
|
|
244
|
+
// base, and the surviving spelling is the first one in configuration order,
|
|
245
|
+
// because that is the order this loop admits them in and the `seen` map only
|
|
246
|
+
// answers membership. That decides only the loader-level root messages, which
|
|
247
|
+
// belong to the directory rather than to either declaration. Every per-
|
|
248
|
+
// population diagnostic reads that population's own base and therefore keeps
|
|
249
|
+
// naming the spelling its own author wrote.
|
|
190
250
|
func configuredBases(config graphConfig, kind artifactKind) []populationBase {
|
|
191
251
|
bases := []populationBase{}
|
|
192
252
|
seen := map[string]bool{}
|
|
@@ -213,26 +273,52 @@ func configuredBases(config graphConfig, kind artifactKind) []populationBase {
|
|
|
213
273
|
return bases
|
|
214
274
|
}
|
|
215
275
|
|
|
216
|
-
// populationRootLabel names
|
|
217
|
-
//
|
|
276
|
+
// populationRootLabel names the configuration property a reader has to edit,
|
|
277
|
+
// spelled the way they wrote it.
|
|
278
|
+
//
|
|
279
|
+
// This is the one place a declared spelling is preferred over the derived one,
|
|
280
|
+
// and the split is between two questions rather than two formats. A message
|
|
281
|
+
// naming a *location* spells it the way the reader opens it, which is what
|
|
282
|
+
// `display` does, and which is project-relative wherever a relative spelling
|
|
283
|
+
// exists. A
|
|
284
|
+
// message naming a *configuration property* spells it the way the configuration
|
|
285
|
+
// does, because the author's first move is to search `lint.config.ts` for it —
|
|
286
|
+
// and the derived spelling of an absolute root is an ascending path that file
|
|
287
|
+
// does not contain.
|
|
288
|
+
//
|
|
289
|
+
// The absolute fallback belongs to the default base alone, which is the only
|
|
290
|
+
// base with no declared spelling, and the project root is then the only thing
|
|
291
|
+
// left to name. Of the four callers, two reach it: `unlistableBaseProblem` and
|
|
292
|
+
// `unresolvedBaseProblem`, both of which a base with no declared root can arrive
|
|
293
|
+
// at. `describePopulation` returns on the default base before it names anything,
|
|
294
|
+
// and `describeBaseDirectoryProblem` is only ever entered through
|
|
295
|
+
// `baseDirectoryProblem`, which does the same. Neither of the two that reach it
|
|
296
|
+
// asks for an edit to a property that is not there, which is what makes the
|
|
297
|
+
// spelling usable where no property exists.
|
|
218
298
|
func populationRootLabel(base populationBase) string {
|
|
219
|
-
if base.
|
|
299
|
+
if base.Declared == "" {
|
|
220
300
|
return filepath.ToSlash(base.Absolute)
|
|
221
301
|
}
|
|
222
|
-
return base.
|
|
302
|
+
return base.Declared
|
|
223
303
|
}
|
|
224
304
|
|
|
225
|
-
//
|
|
226
|
-
//
|
|
305
|
+
// baseDirectoryProblem reports a declared root that is not a usable directory.
|
|
306
|
+
//
|
|
307
|
+
// The observation and the sentence are separate functions because the sentence
|
|
308
|
+
// branches on the artifact kind as well as on what the stat found, while the
|
|
309
|
+
// observation is one call, and because only the sentence can be put in front of
|
|
310
|
+
// a reader without a filesystem in the state it describes.
|
|
311
|
+
//
|
|
312
|
+
// The name says neither "missing" nor "read". Not missing, because the path may
|
|
313
|
+
// be occupied by a file or hidden behind a parent this process may not enter,
|
|
314
|
+
// and both of those are present. Not read, because the third of its three
|
|
315
|
+
// callers materializes from the Program and reads nothing while asking the same
|
|
316
|
+
// question the two walkers ask.
|
|
227
317
|
//
|
|
228
318
|
// The default base is excluded because `Check` already validated the project
|
|
229
319
|
// root, and its diagnostic names the ttsc project identity as the repair rather
|
|
230
320
|
// than a configuration property that does not exist there.
|
|
231
|
-
|
|
232
|
-
// Both spellings appear. The relative one is the property the author has to
|
|
233
|
-
// edit, and the absolute one is where that property actually landed — which is
|
|
234
|
-
// the whole question the moment a root ascends out of the project.
|
|
235
|
-
func unreadableBaseProblem(base populationBase, kind artifactKind) string {
|
|
321
|
+
func baseDirectoryProblem(base populationBase, kind artifactKind) string {
|
|
236
322
|
if base.Default {
|
|
237
323
|
return ""
|
|
238
324
|
}
|
|
@@ -240,9 +326,418 @@ func unreadableBaseProblem(base populationBase, kind artifactKind) string {
|
|
|
240
326
|
if err == nil && info.IsDir() {
|
|
241
327
|
return ""
|
|
242
328
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
329
|
+
// A link with no target is the one state where the two calls disagree about
|
|
330
|
+
// whether anything is there: `Stat` follows it and reports the target as
|
|
331
|
+
// absent, while `Lstat` finds the link itself. Told to create a directory over
|
|
332
|
+
// one, the author gets `EEXIST` and reads the same sentence again, which is
|
|
333
|
+
// the repair this predicate exists to avoid.
|
|
334
|
+
//
|
|
335
|
+
// Only an absent answer is asked the second question. A loop, a parent that
|
|
336
|
+
// denies traversal, a name the filesystem refuses, and a path too long each
|
|
337
|
+
// already say something the rule cannot improve on, and promoting one here
|
|
338
|
+
// would trade the operating system's own sentence for an assertion that a
|
|
339
|
+
// non-directory is in the way, which is the guess the third state exists to
|
|
340
|
+
// refuse. The second call is made only on the absent path, so an ordinary
|
|
341
|
+
// root still costs one stat.
|
|
342
|
+
//
|
|
343
|
+
// `fs.ErrNotExist` is the whole of that test, and Windows answers it for more
|
|
344
|
+
// than an absent path: `ERROR_BAD_NETPATH` maps to it, so a UNC root whose
|
|
345
|
+
// server is unreachable reads as missing and is told to create a directory it
|
|
346
|
+
// cannot create. That is the second concession of the same shape as a path
|
|
347
|
+
// whose parent is a file, and it is left for the same reason: separating it
|
|
348
|
+
// means reading errno numbers per platform inside a file whose whole subject
|
|
349
|
+
// is that a path means the same thing on both.
|
|
350
|
+
occupied := err == nil
|
|
351
|
+
if errors.Is(err, fs.ErrNotExist) {
|
|
352
|
+
if _, linkErr := os.Lstat(base.Absolute); linkErr == nil {
|
|
353
|
+
occupied = true
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return describeBaseDirectoryProblem(base, kind, occupied, err)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// describeBaseDirectoryProblem says what a declared root turned out to be, and
|
|
360
|
+
// what to do about it.
|
|
361
|
+
//
|
|
362
|
+
// Three states reach it, and each one owns a repair the other two cannot be
|
|
363
|
+
// given. `occupied` says something that is not a directory is at the path, and
|
|
364
|
+
// `cause` is the stat's error when it had one. They are not exclusive: a link
|
|
365
|
+
// with no target occupies the path and fails the stat at once, and it is
|
|
366
|
+
// `occupied` that decides the sentence, because what the author has to do is
|
|
367
|
+
// replace what is there. `cause` is read only where `occupied` is false, which
|
|
368
|
+
// is the one shape the caller never produces without an error.
|
|
369
|
+
//
|
|
370
|
+
// `occupied` means the repair has to name what is in the way: told to "add that
|
|
371
|
+
// directory" over a file, an author follows the instruction, watches it fail,
|
|
372
|
+
// and reads the same sentence again. An absent path is created. A stat that
|
|
373
|
+
// failed for any other reason is the state where the rule does not know which
|
|
374
|
+
// of those two it is looking at, and it must not guess, because "create that
|
|
375
|
+
// directory" over a directory an unreadable parent is hiding is the same
|
|
376
|
+
// unfollowable repair a second time.
|
|
377
|
+
//
|
|
378
|
+
// The third repair therefore names no cause of its own. A permission is only
|
|
379
|
+
// the likeliest reason a stat fails without saying the path is absent; a name
|
|
380
|
+
// the filesystem refuses to spell, a path too long, and a link loop arrive the
|
|
381
|
+
// same way, and "make that path reachable" would be a guess in each of them.
|
|
382
|
+
// The operating system already wrote the reason one clause earlier.
|
|
383
|
+
//
|
|
384
|
+
// `fs.ErrNotExist` is what separates the second from the third, and the split
|
|
385
|
+
// is not identical on both platforms. A path whose parent is a file answers
|
|
386
|
+
// `ENOTDIR` on POSIX and a not-found error on Windows, so POSIX reads the
|
|
387
|
+
// filesystem's own reason while Windows offers "create that directory" over a
|
|
388
|
+
// path `mkdir -p` cannot create. Both lead clauses are true; that one repair
|
|
389
|
+
// clause is not followable, and it is left because separating it needs a stat
|
|
390
|
+
// of every ancestor on a path this rule otherwise touches once. "Correct the
|
|
391
|
+
// 'root' property" is offered first in both, and it is the repair that works.
|
|
392
|
+
//
|
|
393
|
+
// The Markdown and Prisma messages may still lead with "could not read",
|
|
394
|
+
// because the walk each of those callers is about to run is the read the root
|
|
395
|
+
// exists to serve, and it is skipped only because the root is not there.
|
|
396
|
+
//
|
|
397
|
+
// Both spellings appear, and only while they differ. The declared one is the
|
|
398
|
+
// property the author has to edit, and the resolved one is where that property
|
|
399
|
+
// actually landed, which is the whole question the moment a root ascends out of
|
|
400
|
+
// the project. Restating a path the declaration already names would offer it as
|
|
401
|
+
// an explanation of itself, which is why the test is the difference and not
|
|
402
|
+
// whether the root was absolute: a UNC spelling on POSIX is absolute and still
|
|
403
|
+
// differs, because `filepath.Clean` collapses its leading slashes there while
|
|
404
|
+
// Windows keeps the volume.
|
|
405
|
+
//
|
|
406
|
+
// The clause about resolution goes with it, for the stronger reason that it is
|
|
407
|
+
// false there. `resolvePopulationBase` joins the project root into a relative
|
|
408
|
+
// declared root and into nothing else, so "it resolves against the ttsc project
|
|
409
|
+
// root" describes an act that did not happen whenever the root was absolute.
|
|
410
|
+
// `declaredRootIsAbsolute` is what both the resolution and this sentence read,
|
|
411
|
+
// so the sentence cannot outlive the act it reports.
|
|
412
|
+
//
|
|
413
|
+
// TypeScript is told apart in the verb and in the repair clause. Its root
|
|
414
|
+
// re-bases addressing over sources the Program already holds and never scans a
|
|
415
|
+
// directory, so a message about reading one would name an access this artifact
|
|
416
|
+
// kind never attempts.
|
|
417
|
+
//
|
|
418
|
+
// The verb also has to stay clear of the path sense beside it. "Resolves to"
|
|
419
|
+
// and "resolves against" are both composition, and a lead clause saying the
|
|
420
|
+
// root could not be resolved would make one sentence carry that sense and its
|
|
421
|
+
// opposite, in the one diagnostic whose whole job is to stop a misreading.
|
|
422
|
+
//
|
|
423
|
+
// Keep the two facts of the repair clause apart. The root spelling resolves
|
|
424
|
+
// against the project root; the sources re-base onto the root. Merging them
|
|
425
|
+
// into "re-bases Program sources against the ttsc project root" states the
|
|
426
|
+
// reverse of what the property does, and reads as though declaring it changed
|
|
427
|
+
// nothing, which is the one conclusion an author must not draw here.
|
|
428
|
+
//
|
|
429
|
+
// Every repair clause that ends in a directory takes two steps, and that part
|
|
430
|
+
// is not about TypeScript at all. This stat is satisfied by an empty directory,
|
|
431
|
+
// so creating one silences the diagnostic and leaves the population exactly as
|
|
432
|
+
// empty, and for a claim that is worse than the diagnostic was, because an
|
|
433
|
+
// empty healthy claim deactivates without a word. Those branches therefore ask
|
|
434
|
+
// for what the directory must hold, and the split is disk against Program
|
|
435
|
+
// rather than one noun against another: the walkers want the sources on disk,
|
|
436
|
+
// and TypeScript wants them in the Program.
|
|
437
|
+
func describeBaseDirectoryProblem(
|
|
438
|
+
base populationBase,
|
|
439
|
+
kind artifactKind,
|
|
440
|
+
occupied bool,
|
|
441
|
+
cause error,
|
|
442
|
+
) string {
|
|
443
|
+
unexaminable := !occupied && !errors.Is(cause, fs.ErrNotExist)
|
|
444
|
+
label := populationRootLabel(base)
|
|
445
|
+
// Two questions, two tests, and they are not the same one. Restating the
|
|
446
|
+
// resolved path tells a reader something only where it differs from the label,
|
|
447
|
+
// which almost every absolute declared root fails and a UNC spelling on POSIX
|
|
448
|
+
// passes, because `filepath.Clean` collapses its leading slashes while the
|
|
449
|
+
// declaration keeps them. Whether the project root was composed into the
|
|
450
|
+
// spelling at all is a different question, it is what gates the clause that
|
|
451
|
+
// says so, and only `declaredRootIsAbsolute` answers it.
|
|
452
|
+
restate := label != filepath.ToSlash(base.Absolute)
|
|
453
|
+
resolved := !declaredRootIsAbsolute(base.Declared)
|
|
454
|
+
if unexaminable {
|
|
455
|
+
message := "Evidence graph could not examine the " + string(kind) + " root '" + label + "'"
|
|
456
|
+
if restate {
|
|
457
|
+
message += ", which resolves to '" + filepath.ToSlash(base.Absolute) + "'"
|
|
458
|
+
}
|
|
459
|
+
message += ": " + causeText(cause) + ". Correct the 'root' property, or clear the condition the filesystem reported"
|
|
460
|
+
if resolved {
|
|
461
|
+
message += "; it resolves against the ttsc project root"
|
|
462
|
+
}
|
|
463
|
+
if kind != artifactTypeScript {
|
|
464
|
+
return message + "."
|
|
465
|
+
}
|
|
466
|
+
return message + ". A " + string(kind) +
|
|
467
|
+
" root is checked by this stat alone: it re-bases Program sources onto itself rather than scanning the filesystem."
|
|
468
|
+
}
|
|
469
|
+
message := "Evidence graph could not read the " + string(kind) + " root '" + label + "'"
|
|
470
|
+
if kind == artifactTypeScript {
|
|
471
|
+
message = "Evidence graph found no directory at the " + string(kind) + " root '" + label + "'"
|
|
472
|
+
}
|
|
473
|
+
if restate {
|
|
474
|
+
message += ", which resolves to '" + filepath.ToSlash(base.Absolute) + "'"
|
|
475
|
+
}
|
|
476
|
+
if occupied {
|
|
477
|
+
message += ", because that path is not a directory"
|
|
478
|
+
}
|
|
479
|
+
message += ". Correct the 'root' property, or "
|
|
480
|
+
switch {
|
|
481
|
+
case kind == artifactTypeScript && occupied:
|
|
482
|
+
message += "replace that path with a directory and make its sources part of the tsconfig Program"
|
|
483
|
+
case kind == artifactTypeScript:
|
|
484
|
+
message += "add that directory and make its sources part of the tsconfig Program"
|
|
485
|
+
case occupied:
|
|
486
|
+
message += "replace that path with a directory and the " + string(kind) + " sources it should hold"
|
|
487
|
+
default:
|
|
488
|
+
message += "create that directory and the " + string(kind) + " sources it should hold"
|
|
489
|
+
}
|
|
490
|
+
message += "; "
|
|
491
|
+
if resolved {
|
|
492
|
+
message += "it resolves against the ttsc project root, and "
|
|
493
|
+
}
|
|
494
|
+
if kind == artifactTypeScript {
|
|
495
|
+
return message + "a " + string(kind) +
|
|
496
|
+
" root re-bases Program sources onto itself rather than scanning the filesystem."
|
|
497
|
+
}
|
|
498
|
+
return message + "an empty directory leaves the population just as empty."
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// resolvedBaseDirectory is the directory a base's files sit in, and reports
|
|
502
|
+
// whether the links naming it end at one.
|
|
503
|
+
//
|
|
504
|
+
// A declared root may be a link, and `os.Stat` accepts one as a directory when
|
|
505
|
+
// the gate reads it, so a consumer that then compares paths against the base has
|
|
506
|
+
// to compare against what the link names. The filesystem never needs this: it
|
|
507
|
+
// opens a path through any link transparently. A comparison does, because the
|
|
508
|
+
// two sides are strings and only one of them was spelled by this rule.
|
|
509
|
+
//
|
|
510
|
+
// Every component is resolved, not only the leaf. A link on an ancestor is
|
|
511
|
+
// exactly what `os.Lstat` of the leaf cannot see, and it is the shape a package
|
|
512
|
+
// manager installs: the workspace dependency is the link and the root an author
|
|
513
|
+
// declares is a directory inside it. Resolving only the leaf left that silent,
|
|
514
|
+
// which is what #1269 recorded.
|
|
515
|
+
//
|
|
516
|
+
// The answer is verified rather than trusted, at every component and not only
|
|
517
|
+
// at the last. `resolveLinkedDirectory` gives up after a fixed number of hops
|
|
518
|
+
// and returns the link it stopped on, while the stat in `baseDirectoryProblem`
|
|
519
|
+
// follows further on Linux and on Windows, so a long enough chain passes the
|
|
520
|
+
// gate and leaves whoever trusted the resolver holding a link. Asking only the
|
|
521
|
+
// leaf would miss it on an ancestor for the same reason `os.Lstat` does:
|
|
522
|
+
// traversal through a link is transparent, so the remaining components still
|
|
523
|
+
// name a real directory and the last stat says so. Darwin and the BSDs stop at
|
|
524
|
+
// the same number of hops, so for a declared root the gate answers first and
|
|
525
|
+
// the refusal is unreachable there. The default base is not gated at all,
|
|
526
|
+
// because `baseDirectoryProblem` returns on it without stat'ing anything, so a
|
|
527
|
+
// walker reaches this refusal for the project root on every platform.
|
|
528
|
+
//
|
|
529
|
+
// A base that is not a link costs one `os.Lstat` per path component plus one
|
|
530
|
+
// for this answer, where resolving the leaf alone cost two. It is paid per call
|
|
531
|
+
// rather than per file, and the call count is the graph's own shape rather than
|
|
532
|
+
// this function's: claim-side populations are materialized before the whole
|
|
533
|
+
// configuration, so a base is asked once for each load it appears in. That is
|
|
534
|
+
// at most twice for a Markdown or Prisma claim base and once for its reference
|
|
535
|
+
// bases, and twice those numbers for TypeScript, whose gate asks beside the
|
|
536
|
+
// pass that builds its match table and whose governance pass builds one more
|
|
537
|
+
// from the configuration as declared. All of it stays off the loop this feeds,
|
|
538
|
+
// which is every source file of the project.
|
|
539
|
+
func resolvedBaseDirectory(base populationBase) (string, bool) {
|
|
540
|
+
from, resolved := resolveLinkedPath(base.Absolute)
|
|
541
|
+
if !resolved {
|
|
542
|
+
return from, false
|
|
543
|
+
}
|
|
544
|
+
info, err := os.Lstat(from)
|
|
545
|
+
return from, err == nil && info.IsDir()
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// resolveLinkedPath resolves a link at any component of an absolute path.
|
|
549
|
+
//
|
|
550
|
+
// `filepath.EvalSymlinks` does this in one call and cannot be used, for the
|
|
551
|
+
// reason `resolveLinkedDirectory` exists at all: it returns a Windows junction
|
|
552
|
+
// unchanged, which is the link a package manager creates there. So the walk is
|
|
553
|
+
// by hand, and it asks the same resolver at every prefix.
|
|
554
|
+
//
|
|
555
|
+
// The volume is taken off first and never split. A Windows drive root and a UNC
|
|
556
|
+
// share are one component whose separator is part of them, and joining their
|
|
557
|
+
// pieces back would name a different location, which is the same hazard
|
|
558
|
+
// `normalizeRootPath` hand-cleans for a declared spelling. A base that is the
|
|
559
|
+
// share itself has nothing left to walk, and it returns unchanged rather than
|
|
560
|
+
// gaining the separator this walk starts from: a spelling that differs from
|
|
561
|
+
// the base by one character resolves to the same directory and compares as
|
|
562
|
+
// though it did not, so every consumer would pay the second spelling forever
|
|
563
|
+
// on a path with no link in it.
|
|
564
|
+
//
|
|
565
|
+
// A component whose chain outran the resolver ends the walk. Joining the rest
|
|
566
|
+
// onto it would produce a path the filesystem opens and every string
|
|
567
|
+
// comparison misses, which is #1269 exactly, one component further up than the
|
|
568
|
+
// case that opened it.
|
|
569
|
+
func resolveLinkedPath(absolute string) (string, bool) {
|
|
570
|
+
volume := filepath.VolumeName(absolute)
|
|
571
|
+
rest := absolute[len(volume):]
|
|
572
|
+
current := volume + string(filepath.Separator)
|
|
573
|
+
walked := false
|
|
574
|
+
for _, segment := range strings.Split(filepath.ToSlash(rest), "/") {
|
|
575
|
+
if segment == "" {
|
|
576
|
+
continue
|
|
577
|
+
}
|
|
578
|
+
walked = true
|
|
579
|
+
resolved, settled := resolveLinkedDirectory(filepath.Join(current, segment))
|
|
580
|
+
if !settled {
|
|
581
|
+
return filepath.FromSlash(resolved), false
|
|
582
|
+
}
|
|
583
|
+
current = resolved
|
|
584
|
+
}
|
|
585
|
+
if !walked {
|
|
586
|
+
return filepath.Clean(absolute), true
|
|
587
|
+
}
|
|
588
|
+
return filepath.FromSlash(current), true
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// unresolvedBaseProblem reports a base whose links this rule stops following
|
|
592
|
+
// before they reach a directory.
|
|
593
|
+
//
|
|
594
|
+
// The chain may be the root itself or any directory above it, so the sentence
|
|
595
|
+
// says the path passes through one rather than that it is one. Naming the
|
|
596
|
+
// component would be more precise and is deliberately not done: this message
|
|
597
|
+
// already carries the declared spelling and, where they differ, the path it
|
|
598
|
+
// resolves to, and a third path would cost more to read than the component
|
|
599
|
+
// buys — the repair is the same wherever on the path the chain sits.
|
|
600
|
+
//
|
|
601
|
+
// Every artifact kind gets this sentence, because the failure is the resolver's
|
|
602
|
+
// bound rather than anything a walk or a Program does, and the kind appears only
|
|
603
|
+
// as the noun. The verb stays clear of "resolve" for the reason
|
|
604
|
+
// `describeBaseDirectoryProblem` records: that word is composition everywhere
|
|
605
|
+
// else in these messages, and a lead clause borrowing it for failure would make
|
|
606
|
+
// one sentence carry both senses.
|
|
607
|
+
//
|
|
608
|
+
// The default base is named and repaired differently, because it declared no
|
|
609
|
+
// `root` and telling its author to correct that property sends them looking for
|
|
610
|
+
// a line their configuration does not contain. It is the ttsc project root, so
|
|
611
|
+
// it is named as one and the repair is the invocation.
|
|
612
|
+
func unresolvedBaseProblem(base populationBase, kind artifactKind) string {
|
|
613
|
+
label := populationRootLabel(base)
|
|
614
|
+
message := "Evidence graph found no directory at the end of the "
|
|
615
|
+
if base.Default {
|
|
616
|
+
message += "ttsc project root '" + label + "'"
|
|
617
|
+
} else {
|
|
618
|
+
message += string(kind) + " root '" + label + "'"
|
|
619
|
+
}
|
|
620
|
+
// The resolved path is restated only when it differs from the label, which is
|
|
621
|
+
// the whole question: an absolute declared root and the default base both name
|
|
622
|
+
// the path they landed on, while a UNC spelling on POSIX does not, because
|
|
623
|
+
// `filepath.Clean` collapses its leading slashes and the two genuinely differ.
|
|
624
|
+
// `describeBaseDirectoryProblem` makes the same test for the same reason.
|
|
625
|
+
if resolved := filepath.ToSlash(base.Absolute); label != resolved {
|
|
626
|
+
message += ", which resolves to '" + resolved + "'"
|
|
627
|
+
}
|
|
628
|
+
message += ". That path passes through a chain of links longer than this rule follows. "
|
|
629
|
+
if base.Default {
|
|
630
|
+
return message + "Run ttsc against the directory those links end at."
|
|
631
|
+
}
|
|
632
|
+
return message + "Correct the 'root' property to name the directory those links end at."
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// causeText spells a reason for a sentence that owns its own terminator.
|
|
636
|
+
//
|
|
637
|
+
// Windows ends its messages with a period and POSIX does not, so a rule that
|
|
638
|
+
// appends one prints "denied.. Correct the" on the one platform and "denied.
|
|
639
|
+
// Correct the" on the other, over the same failure. The terminator belongs to
|
|
640
|
+
// the sentence this rule writes, and the reason belongs to whoever wrote it, so
|
|
641
|
+
// only the punctuation is taken.
|
|
642
|
+
//
|
|
643
|
+
// The reason may come from the filesystem, a subprocess, or this rule's own
|
|
644
|
+
// inner validation. Only the trim happens here; the sentence supplies its
|
|
645
|
+
// terminator, which is what lets a quoted glob error that writes none end with
|
|
646
|
+
// one instead of ending bare.
|
|
647
|
+
func causeText(cause error) string {
|
|
648
|
+
return causeReason(cause.Error())
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// causeReason is the same rule for a failure already reduced to text. A package
|
|
652
|
+
// entry read, a package walk, and a package source read take a string from the
|
|
653
|
+
// loader, and the Prisma and Swagger bridges hand back a parser's own sentence,
|
|
654
|
+
// so those five sites reach the rule this way rather than through an error.
|
|
655
|
+
func causeReason(text string) string {
|
|
656
|
+
return strings.TrimSuffix(text, ".")
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// unlistableBaseProblem reports a population whose own base could not be
|
|
660
|
+
// listed.
|
|
661
|
+
//
|
|
662
|
+
// This is a different finding from the entry-level one below, and the
|
|
663
|
+
// difference is the whole population. An entry the walk could not read costs
|
|
664
|
+
// the units in it; a base the walk could not list costs every unit there is,
|
|
665
|
+
// and reporting it through the per-entry line would name the base as though it
|
|
666
|
+
// were one file inside itself. Both walkers reach it through the error their
|
|
667
|
+
// callback returns for that one path, which is what ends the walk and brings
|
|
668
|
+
// the failure back here.
|
|
669
|
+
//
|
|
670
|
+
// The base is named by its configuration spelling rather than by a location,
|
|
671
|
+
// because that is the property an author edits, and because a base with no
|
|
672
|
+
// declared root has no other name than where it is.
|
|
673
|
+
//
|
|
674
|
+
// One cause reaches this: a listing the filesystem refused. A chain of links the
|
|
675
|
+
// resolver stops following used to arrive here as well and has its own sentence
|
|
676
|
+
// now, because that failure is the resolver's bound and this repair is about
|
|
677
|
+
// access.
|
|
678
|
+
func unlistableBaseProblem(
|
|
679
|
+
base populationBase,
|
|
680
|
+
sources string,
|
|
681
|
+
cause error,
|
|
682
|
+
) string {
|
|
683
|
+
return "Evidence graph could not walk " + sources + " root '" + populationRootLabel(base) +
|
|
684
|
+
"': " + causeText(cause) + ". Make that root a directory this process can list, so its configured " +
|
|
685
|
+
sources + " sources can be indexed."
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// unreadableEntryProblem decides whether a walk failure inside a population
|
|
689
|
+
// belongs to it, and spells it if it does.
|
|
690
|
+
//
|
|
691
|
+
// The Markdown and Prisma walkers share it because they are otherwise one
|
|
692
|
+
// decision written twice, and its two halves fail differently. Reporting a path
|
|
693
|
+
// no configured glob reaches turns an unrelated permission on an unrelated
|
|
694
|
+
// directory into a build error; printing the path the walker was handed spells
|
|
695
|
+
// it the way the filesystem API does rather than the way the rest of the loader
|
|
696
|
+
// spells every path beside it.
|
|
697
|
+
//
|
|
698
|
+
// `reads` is the population's own membership question, which is the only part
|
|
699
|
+
// that differs by artifact kind. `from` is the directory the walk started at,
|
|
700
|
+
// which is the base unless the base is a link, and it is what a callback path
|
|
701
|
+
// is measured against; the spelling still comes from the base, so a reader sees
|
|
702
|
+
// the root they declared either way. The base itself never arrives here: it is
|
|
703
|
+
// not one entry among many, and each walker answers it before this is asked.
|
|
704
|
+
func unreadableEntryProblem(
|
|
705
|
+
base populationBase,
|
|
706
|
+
from string,
|
|
707
|
+
sources string,
|
|
708
|
+
current string,
|
|
709
|
+
cause error,
|
|
710
|
+
reads func(relative string) bool,
|
|
711
|
+
) (string, bool) {
|
|
712
|
+
relative, ok := relativeProjectPath(from, current)
|
|
713
|
+
if !ok || !reads(relative) {
|
|
714
|
+
return "", false
|
|
715
|
+
}
|
|
716
|
+
return unreadableWalkEntryProblem(base, relative, sources, cause), true
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// unreadableWalkEntryProblem names one path a population walk could not
|
|
720
|
+
// inspect.
|
|
721
|
+
//
|
|
722
|
+
// The path a `filepath.WalkDir` callback receives is the OS-native absolute
|
|
723
|
+
// one, and printing it as it arrives made this the only line in either walker
|
|
724
|
+
// spelled with backslashes on Windows, against globs, a root, and file
|
|
725
|
+
// locations that all pass through `filepath.ToSlash`. A reader compares them
|
|
726
|
+
// against each other, so the walker composes the base-relative path it already
|
|
727
|
+
// holds through the same `display` a loaded file's own message uses.
|
|
728
|
+
//
|
|
729
|
+
// The cause is passed through as the filesystem wrote it. It may embed an
|
|
730
|
+
// OS-native absolute path of its own, and rewriting a sentence this rule did
|
|
731
|
+
// not author is a different claim than spelling its own paths one way.
|
|
732
|
+
func unreadableWalkEntryProblem(
|
|
733
|
+
base populationBase,
|
|
734
|
+
relative string,
|
|
735
|
+
sources string,
|
|
736
|
+
cause error,
|
|
737
|
+
) string {
|
|
738
|
+
return "Evidence graph could not inspect '" + base.display(relative) +
|
|
739
|
+
"': " + causeText(cause) + ". Fix filesystem access so configured " + sources +
|
|
740
|
+
" sources can be indexed."
|
|
246
741
|
}
|
|
247
742
|
|
|
248
743
|
// normalizeRootPath validates a declared root without resolving it.
|