@ttsc/evidence 0.30.4 → 0.31.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.
Files changed (66) hide show
  1. package/README.md +14 -6
  2. package/lib/index.d.ts +2 -3
  3. package/lib/index.js +2 -3
  4. package/lib/internal/canonicalDigest.js +12 -6
  5. package/lib/internal/loadPrismaModels.js +52 -7
  6. package/lib/internal/loadSwaggerOperations.js +253 -59
  7. package/lib/internal/normalizeSwaggerDocument.js +320 -2
  8. package/lib/structures/ITtscEvidenceDocumentedConfig.d.ts +16 -8
  9. package/lib/structures/ITtscEvidenceGraphClaim.d.ts +6 -0
  10. package/lib/structures/ITtscEvidenceGraphClaimBase.d.ts +8 -2
  11. package/lib/structures/ITtscEvidenceGraphConfig.d.ts +6 -0
  12. package/lib/structures/ITtscEvidenceGraphMarkdownClaim.d.ts +7 -1
  13. package/lib/structures/ITtscEvidenceGraphMarkdownReference.d.ts +7 -1
  14. package/lib/structures/ITtscEvidenceGraphPrismaClaim.d.ts +7 -1
  15. package/lib/structures/ITtscEvidenceGraphPrismaReference.d.ts +7 -1
  16. package/lib/structures/ITtscEvidenceGraphReference.d.ts +10 -4
  17. package/lib/structures/ITtscEvidenceGraphReferenceBase.d.ts +6 -0
  18. package/lib/structures/ITtscEvidenceGraphSwaggerReference.d.ts +6 -0
  19. package/lib/structures/ITtscEvidenceGraphTypeScriptClaim.d.ts +7 -1
  20. package/lib/structures/ITtscEvidenceGraphTypeScriptReference.d.ts +11 -3
  21. package/lib/structures/ITtscEvidenceRules.d.ts +93 -0
  22. package/lib/structures/TtscEvidenceGraphMarkdownSymbol.d.ts +39 -0
  23. package/lib/{typings → structures}/TtscEvidenceGraphPrismaSymbol.d.ts +6 -0
  24. package/lib/{typings → structures}/TtscEvidenceGraphTypeScriptSymbol.d.ts +6 -0
  25. package/lib/structures/index.d.ts +4 -0
  26. package/lib/structures/index.js +4 -0
  27. package/native/base.go +25 -23
  28. package/native/bridge_trace.go +348 -0
  29. package/native/comments.go +1 -1
  30. package/native/config.go +6 -3
  31. package/native/declaration.go +17 -43
  32. package/native/documentation_regions.go +214 -0
  33. package/native/documented.go +5 -4
  34. package/native/exports.go +1 -1
  35. package/native/fingerprint.go +2 -8
  36. package/native/glob.go +4 -1
  37. package/native/graph.go +59 -23
  38. package/native/graph_nodes.go +2 -1
  39. package/native/hints.go +22 -18
  40. package/native/imports.go +7 -6
  41. package/native/input_reader.go +83 -0
  42. package/native/inputs.go +13 -12
  43. package/native/loader.go +36 -24
  44. package/native/markdown.go +587 -177
  45. package/native/markdown_capture.go +223 -0
  46. package/native/model.go +2 -1
  47. package/native/prisma.go +110 -25
  48. package/native/prisma_cache.go +12 -8
  49. package/native/prisma_locator.go +70 -33
  50. package/native/review.go +8 -7
  51. package/native/rooted_typescript.go +3 -4
  52. package/native/swagger.go +123 -15
  53. package/native/swagger_cache.go +16 -12
  54. package/native/todo.go +12 -0
  55. package/native/typescript.go +129 -25
  56. package/package.json +9 -7
  57. package/lib/typings/ITtscLintContributorRules.d.ts +0 -57
  58. package/lib/typings/ITtscLintRuleOptionsMap.d.ts +0 -20
  59. package/lib/typings/ITtscLintRuleOptionsMap.js +0 -2
  60. package/lib/typings/TtscEvidenceGraphMarkdownSymbol.d.ts +0 -32
  61. package/lib/typings/index.d.ts +0 -5
  62. package/lib/typings/index.js +0 -21
  63. /package/lib/{typings/ITtscLintContributorRules.js → structures/ITtscEvidenceRules.js} +0 -0
  64. /package/lib/{typings → structures}/TtscEvidenceGraphMarkdownSymbol.js +0 -0
  65. /package/lib/{typings → structures}/TtscEvidenceGraphPrismaSymbol.js +0 -0
  66. /package/lib/{typings → structures}/TtscEvidenceGraphTypeScriptSymbol.js +0 -0
package/native/hints.go CHANGED
@@ -10,17 +10,21 @@ import (
10
10
  // graphCorpus is what a passing Check publishes for Hints to project.
11
11
  //
12
12
  // It carries the inventories Check already built rather than the paths to
13
- // rebuild them from. The graph loads Markdown and Swagger to do its own job, so
14
- // handing that result forward costs a build nothing; reloading inside Hints
15
- // would read every source a second time to answer a question the rule had just
16
- // finished answering.
13
+ // rebuild them from. The graph loads Markdown, Prisma and Swagger to do its own
14
+ // job, so handing that result forward costs a build nothing; reloading inside
15
+ // Hints would read every source a second time to answer a question the rule had
16
+ // just finished answering.
17
17
  //
18
18
  // What the editor path does pay is Check itself. A hints request runs a fresh
19
- // project cycle (`linthost/hints.go:74`), so every source the graph reads is
20
- // read again there — including an HTTP(S) Swagger reference, which is fetched.
21
- // That cost is inherent to answering from a current index rather than a stale
22
- // one, and it is bounded the same way a build is: a source that fails to load
23
- // is reported by Check, which fails the rule, which withdraws the corpus.
19
+ // project cycle (`linthost.collectProjectHints` over `runProjectCycle`), so
20
+ // every source the graph reads is read again there. Local Swagger documents are
21
+ // validated by content, and successful HTTP(S) Swagger documents are reused for
22
+ // the process lifetime while failed fetches are retried. A source that fails to
23
+ // load is reported by Check, which fails the rule, which withdraws the corpus.
24
+ // Markdown activation and evaluation share the first observation of each
25
+ // addressed document within that Check. Hints reflects that captured content;
26
+ // an edit after consumption is observed by a later Check, not by projecting
27
+ // this corpus. Reusable downstream delivery still validates consumed proofs.
24
28
  type graphCorpus struct {
25
29
  Config graphConfig
26
30
  Markdown map[string]*artifactInventory
@@ -70,14 +74,13 @@ func (gate *programDiagnosticGate) first(key string) bool {
70
74
  // here can be computed per keystroke.
71
75
  //
72
76
  // The corpus is available only while the graph passes, and that is a host gate
73
- // rather than a choice made here. `linthost/hints.go:147-149` skips a rule whose
74
- // snapshot is not `ProjectRulePassed` or whose state is nil, and
75
- // `projectReporter.Report` sets `failed = true` unconditionally
76
- // (`linthost/project_engine.go:68-77`). So the cycle that reports an unmet
77
- // obligation is the cycle that withdraws the completions — which is the cycle an
78
- // author is most likely to be writing a citation in. Nothing in this package can
79
- // widen that; it widens upstream, by letting a project rule publish state it
80
- // reported against.
77
+ // rather than a choice made here. `linthost.collectProjectHints` skips a rule
78
+ // whose snapshot is not `ProjectRulePassed` or whose state is nil, and
79
+ // `projectReporter.ReportSeverity` marks the rule failed on every finding that
80
+ // is not severity off. So the cycle that reports an unmet obligation is the
81
+ // cycle that withdraws the completions, which is the cycle an author is most
82
+ // likely to be writing a citation in. Nothing in this package can widen that; it
83
+ // widens upstream, by letting a project rule publish state it reported against.
81
84
  func (graphRule) Hints(ctx *rule.HintContext) []rule.Hint {
82
85
  if ctx == nil {
83
86
  return nil
@@ -175,7 +178,8 @@ func selectsTypeScriptReference(config graphConfig, exclusion bool) bool {
175
178
  // author cannot supply from memory. A heading's generated anchor is neither
176
179
  // visible in the project tree nor guessable from its text, so selected headings
177
180
  // come first; a Markdown file path is typed as easily as it is read, so file
178
- // targets follow; Swagger operations come last, being both few and mechanical.
181
+ // targets follow; Swagger operations come next, being both few and mechanical,
182
+ // and Prisma models and their fields come last.
179
183
  //
180
184
  // Order therefore serves the author who is still browsing, and the filter
181
185
  // serves the one who is not. No hint sets Label, so the editor lists and
package/native/imports.go CHANGED
@@ -2,7 +2,6 @@ package evidence
2
2
 
3
3
  import (
4
4
  "encoding/json"
5
- "os"
6
5
  "path"
7
6
  "strings"
8
7
 
@@ -109,7 +108,8 @@ func stringLiteralText(node *shimast.Node) string {
109
108
  return node.Text()
110
109
  }
111
110
 
112
- // typeScriptModuleExtensions are tried in the order TypeScript itself prefers.
111
+ // typeScriptModuleExtensions are the suffixes an extensionless specifier may
112
+ // take, a source file before the declaration file that would describe it.
113
113
  var typeScriptModuleExtensions = []string{
114
114
  ".ts",
115
115
  ".tsx",
@@ -120,8 +120,6 @@ var typeScriptModuleExtensions = []string{
120
120
  ".d.cts",
121
121
  }
122
122
 
123
- // splitPackageSpecifier separates a package name from a deep-import subpath,
124
- // keeping the leading segment of a scoped name attached to its scope.
125
123
  // moduleCandidates lists the files a specifier may denote, most specific first.
126
124
  //
127
125
  // The `.js` rewrite is not a convenience: under `nodenext` a TypeScript source
@@ -154,6 +152,8 @@ func moduleCandidates(base string) []string {
154
152
  return candidates
155
153
  }
156
154
 
155
+ // splitPackageSpecifier separates a package name from a deep-import subpath,
156
+ // keeping the leading segment of a scoped name attached to its scope.
157
157
  func splitPackageSpecifier(specifier string) (string, string) {
158
158
  segments := strings.Split(specifier, "/")
159
159
  if len(segments) == 0 || segments[0] == "" {
@@ -174,8 +174,9 @@ func splitPackageSpecifier(specifier string) (string, string) {
174
174
  return name, subpath
175
175
  }
176
176
 
177
- func readPackageManifest(root string, relative string) map[string]json.RawMessage {
178
- content, err := os.ReadFile(path.Join(strings.ReplaceAll(root, "\\", "/"), relative))
177
+ func readPackageManifest(root string, relative string, readers ...evidenceInputReader) map[string]json.RawMessage {
178
+ inputs := inputReader(readers)
179
+ content, err := inputs.ReadFile(path.Join(strings.ReplaceAll(root, "\\", "/"), relative))
179
180
  if err != nil {
180
181
  return nil
181
182
  }
@@ -0,0 +1,83 @@
1
+ package evidence
2
+
3
+ import (
4
+ "io/fs"
5
+ "os"
6
+ "path/filepath"
7
+
8
+ "github.com/samchon/ttsc/packages/lint/rule"
9
+ )
10
+
11
+ // evidenceInputReader delegates only the operations an inventory or locator
12
+ // actually performs. A nil host reader preserves the original OS operations
13
+ // for manual contexts and native unit callers. It never preloads a population.
14
+ type evidenceInputReader struct{ host rule.ProjectInputReader }
15
+
16
+ // ReadFile routes actual raw consumption through the generation reader when
17
+ // supplied. Nil manual contexts keep os.ReadFile's bytes and error semantics.
18
+ func (r evidenceInputReader) ReadFile(name string) ([]byte, error) {
19
+ if r.host != nil {
20
+ return r.host.ReadFile(name)
21
+ }
22
+ return os.ReadFile(name)
23
+ }
24
+
25
+ // Stat delegates target metadata without changing native link following.
26
+ func (r evidenceInputReader) Stat(name string) (os.FileInfo, error) {
27
+ if r.host != nil {
28
+ return r.host.Stat(name)
29
+ }
30
+ return os.Stat(name)
31
+ }
32
+
33
+ // Lstat keeps entry metadata distinct from the followed target; an observing
34
+ // host owns withdrawal when its protocol cannot represent that native entry.
35
+ func (r evidenceInputReader) Lstat(name string) (os.FileInfo, error) {
36
+ if r.host != nil {
37
+ return r.host.Lstat(name)
38
+ }
39
+ return os.Lstat(name)
40
+ }
41
+
42
+ // Readlink preserves native target text and errors instead of resolving it.
43
+ func (r evidenceInputReader) Readlink(name string) (string, error) {
44
+ if r.host != nil {
45
+ return r.host.Readlink(name)
46
+ }
47
+ return os.Readlink(name)
48
+ }
49
+
50
+ // WalkDir delegates callback ordering and SkipDir/SkipAll behavior. Neither
51
+ // branch preloads unvisited populations or retains a separate walk cache.
52
+ func (r evidenceInputReader) WalkDir(name string, visit fs.WalkDirFunc) error {
53
+ if r.host != nil {
54
+ return r.host.WalkDir(name, visit)
55
+ }
56
+ return filepath.WalkDir(name, visit)
57
+ }
58
+
59
+ // Unavailable propagates unsupported external consumption to the observing
60
+ // host; a nil manual context has no reusable input authority to withdraw.
61
+ func (r evidenceInputReader) Unavailable() {
62
+ if r.host != nil {
63
+ r.host.Unavailable()
64
+ }
65
+ }
66
+
67
+ // inputReader preserves optional variadic-call compatibility: only the first
68
+ // supplied reader belongs to this operation, otherwise native calls remain.
69
+ func inputReader(readers []evidenceInputReader) evidenceInputReader {
70
+ if len(readers) != 0 {
71
+ return readers[0]
72
+ }
73
+ return evidenceInputReader{}
74
+ }
75
+
76
+ // EvalSymlinks preserves native resolution and returned spelling; observation
77
+ // coordinates and unresolved-proof refusal belong to the supplied host.
78
+ func (r evidenceInputReader) EvalSymlinks(name string) (string, error) {
79
+ if r.host != nil {
80
+ return r.host.EvalSymlinks(name)
81
+ }
82
+ return filepath.EvalSymlinks(name)
83
+ }
package/native/inputs.go CHANGED
@@ -9,12 +9,11 @@ import (
9
9
  // ProjectInputs declares the files this rule reads from outside the Program.
10
10
  //
11
11
  // Markdown, Prisma, and Swagger evidence never enters the TypeScript Program,
12
- // so until `@ttsc/lint@0.22.0` shipped this contract the host had no way to
13
- // learn that the graph depended on them. The resulting asymmetry was invisible
14
- // rather than merely inconvenient: a developer editing code saw fresh
15
- // diagnostics, because the TypeScript event drove a cycle that reloaded the
16
- // documents along with it, while a developer editing only a spec section or a
17
- // schema saw the citation that had just gone stale keep reporting green.
12
+ // so without this declaration the host cannot learn that the graph depends on
13
+ // them. Editing code drives a project cycle that reloads the documents along
14
+ // with it, but editing only a spec section or a schema would raise no event the
15
+ // host associates with this rule, and the citation that had just gone stale
16
+ // would keep reporting green.
18
17
  //
19
18
  // The host calls this after resolving options and project identity but before
20
19
  // loading a Program, so nothing here may read `ctx.Sources`, and nothing here
@@ -84,8 +83,8 @@ func graphProjectInputs(config graphConfig) []rule.ProjectInput {
84
83
  //
85
84
  // The root is prefixed as the author wrote it rather than resolved here. The
86
85
  // host anchors a relative pattern against the same physical project root this
87
- // rule uses (`linthost/project_inputs.go:151-171`), and it accepts one that
88
- // ascends or is absolute — so `../../docs/**/*.md` and `C:/shared/docs/**/*.md`
86
+ // rule uses (`normalizeProjectInput` in `linthost/project_inputs.go`), and it
87
+ // accepts one that ascends or is absolute — so `../../docs/**/*.md` and `C:/shared/docs/**/*.md`
89
88
  // both arrive at the directory `Check` will read. Resolving it here would
90
89
  // duplicate that arithmetic and make this contract depend on a project identity
91
90
  // it is supposed to be able to answer without.
@@ -110,14 +109,16 @@ func globInputs(root string, globs globSet) []rule.ProjectInput {
110
109
  // localSwaggerInputs publishes a Swagger reference only when it names a file.
111
110
  //
112
111
  // Withholding the URL form is not a shortcut. The host rejects a remote pattern
113
- // outright (`linthost/project_inputs.go:159-161`), and one rejected input
114
- // discards the whole snapshot for every project rule in the run
115
- // (`linthost/project_inputs.go:113-135`) — so declaring a URL here would take
112
+ // outright (`normalizeProjectInput` in `linthost/project_inputs.go`), and one
113
+ // rejected input discards the whole snapshot for every project rule in the run
114
+ // (`collectProjectInputs` returns the joined error and an empty snapshot) — so declaring a URL here would take
116
115
  // this graph's Markdown globs down with it and restore the exact blindness the
117
116
  // contract removes.
118
117
  //
119
118
  // The boundary is also real rather than imposed: a URL has no filesystem event
120
- // to observe, so its freshness is per-evaluation and belongs to the loader.
119
+ // to observe. The loader reuses a successful fetch until its process ends and
120
+ // retries failed fetches on later evaluations; restarting the session refreshes
121
+ // a remote document that changed while the process remained alive.
121
122
  //
122
123
  // A local path is published exactly as it normalized, including one that
123
124
  // ascends out of the project or names an absolute location. The host resolves
package/native/loader.go CHANGED
@@ -3,7 +3,6 @@ package evidence
3
3
  import (
4
4
  "encoding/json"
5
5
  "io/fs"
6
- "os"
7
6
  "path"
8
7
  "path/filepath"
9
8
  "sort"
@@ -22,6 +21,7 @@ import (
22
21
  // that symbol is precisely the one an obligation needs to name — an evidence
23
22
  // graph exists to report the operation the frontend never called.
24
23
  type typeScriptLoader struct {
24
+ inputs evidenceInputReader
25
25
  boundary *populationBase
26
26
  root string
27
27
  identityRoot string
@@ -38,8 +38,10 @@ type typeScriptLoader struct {
38
38
  func newTypeScriptLoader(
39
39
  root string,
40
40
  program map[string]*artifactInventory,
41
+ readers ...evidenceInputReader,
41
42
  ) *typeScriptLoader {
42
43
  loader := &typeScriptLoader{
44
+ inputs: inputReader(readers),
43
45
  root: strings.ReplaceAll(root, "\\", "/"),
44
46
  program: map[string]*artifactInventory{},
45
47
  programIDs: map[string]*artifactInventory{},
@@ -50,7 +52,7 @@ func newTypeScriptLoader(
50
52
  identities: map[string]typeScriptModuleIdentity{},
51
53
  installs: map[string]installedPackageLocation{},
52
54
  }
53
- loader.identityRoot, _ = physicalTypeScriptPath(filepath.FromSlash(loader.root))
55
+ loader.identityRoot, _ = physicalTypeScriptPath(filepath.FromSlash(loader.root), loader.inputs)
54
56
  keys := make([]string, 0, len(program))
55
57
  for key := range program {
56
58
  keys = append(keys, key)
@@ -116,7 +118,7 @@ func (loader *typeScriptLoader) inventory(relative string) (result *artifactInve
116
118
 
117
119
  func (loader *typeScriptLoader) parse(relative string) *artifactInventory {
118
120
  relative = loader.projectPath(relative)
119
- content, err := os.ReadFile(resolveProjectPath(loader.root, relative))
121
+ content, err := loader.inputs.ReadFile(resolveProjectPath(loader.root, relative))
120
122
  if err != nil {
121
123
  loader.failures[relative] = err.Error()
122
124
  return nil
@@ -178,7 +180,7 @@ func (loader *typeScriptLoader) programInventory(module string) *artifactInvento
178
180
  // candidate, and a watch cycle resolves every re-export in the population — so
179
181
  // callers that can answer from the Program should do that first.
180
182
  func (loader *typeScriptLoader) existsOnDisk(relative string) bool {
181
- info, err := os.Stat(resolveProjectPath(loader.root, relative))
183
+ info, err := loader.inputs.Stat(resolveProjectPath(loader.root, relative))
182
184
  return err == nil && !info.IsDir()
183
185
  }
184
186
 
@@ -215,10 +217,10 @@ func (loader *typeScriptLoader) resolveUncached(
215
217
  normalized = append(normalized, module)
216
218
  }
217
219
  // The Program answers first, and not only because it answers without a
218
- // syscall. A project that emits beside its sources has both `x.js` and
219
- // `x.ts` on disk, and `x.js` is written earlier in the candidate order;
220
- // resolving an import to emitted JavaScript would read a module whose
221
- // declarations the graph cannot address.
220
+ // syscall. A module under edit has to be read as the editor has it rather
221
+ // than as the disk last saw it, which is the rule `inventory` states for a
222
+ // file already named. The candidates never include emitted JavaScript, so
223
+ // an emit beside its sources cannot win here.
222
224
  for _, candidate := range normalized {
223
225
  if loader.programInventory(candidate) != nil {
224
226
  if !loader.withinBoundary(candidate) {
@@ -263,7 +265,7 @@ func (loader *typeScriptLoader) moduleIdentity(module string) (string, bool) {
263
265
  return cached.Path, cached.Resolved
264
266
  }
265
267
  absolute := resolveProjectPath(loader.root, module)
266
- resolved, ok := physicalTypeScriptPath(absolute)
268
+ resolved, ok := physicalTypeScriptPath(absolute, loader.inputs)
267
269
  // Resolve both sides before taking a relative identity. Canonicalizing only
268
270
  // the file makes a project junction or Windows 8.3 root leak the checkout's
269
271
  // absolute location into every unit ID and invalidate unchanged reviews.
@@ -272,16 +274,17 @@ func (loader *typeScriptLoader) moduleIdentity(module string) (string, bool) {
272
274
  return identity, ok
273
275
  }
274
276
 
275
- func physicalTypeScriptPath(absolute string) (string, bool) {
277
+ func physicalTypeScriptPath(absolute string, readers ...evidenceInputReader) (string, bool) {
278
+ inputs := inputReader(readers)
276
279
  current := filepath.Clean(absolute)
277
280
  // A directory link can introduce another link in an already-walked parent.
278
281
  // Settle the complete path, with the same finite bound as directory chains.
279
282
  for range 32 {
280
- resolved, ok := resolveLinkedPath(current)
283
+ resolved, ok := resolveLinkedPath(current, inputs)
281
284
  if !ok {
282
285
  return resolved, false
283
286
  }
284
- resolved = expandTypeScriptPath(resolved)
287
+ resolved = expandTypeScriptPath(resolved, inputs)
285
288
  if resolved == current {
286
289
  return resolved, true
287
290
  }
@@ -293,11 +296,12 @@ func physicalTypeScriptPath(absolute string) (string, bool) {
293
296
  // An unsaved or deleted file still has an identity. Expand the deepest existing
294
297
  // prefix so Windows short names do not reappear when EvalSymlinks cannot read
295
298
  // the complete path, then restore the missing suffix without changing its case.
296
- func expandTypeScriptPath(absolute string) string {
299
+ func expandTypeScriptPath(absolute string, readers ...evidenceInputReader) string {
300
+ inputs := inputReader(readers)
297
301
  probe := absolute
298
302
  suffix := []string{}
299
303
  for {
300
- if final, err := filepath.EvalSymlinks(probe); err == nil {
304
+ if final, err := inputs.EvalSymlinks(probe); err == nil {
301
305
  if !strings.EqualFold(final, probe) {
302
306
  probe = final
303
307
  }
@@ -352,7 +356,7 @@ func (loader *typeScriptLoader) projectPath(relative string) string {
352
356
 
353
357
  // isCleanProjectRelativePath reports whether a path is already the identity
354
358
  // `projectPath` would produce: forward slashes, no drive or leading separator,
355
- // and no `.` or `..` segment to collapse.
359
+ // and no empty, `.`, or `..` segment to collapse.
356
360
  func isCleanProjectRelativePath(value string) bool {
357
361
  if value == "" || strings.ContainsRune(value, '\\') {
358
362
  return false
@@ -447,16 +451,23 @@ type installedPackageLocation struct {
447
451
  func (loader *typeScriptLoader) locateInstalledPackage(
448
452
  name string,
449
453
  ) (string, map[string]json.RawMessage) {
454
+ current := filepath.Clean(filepath.FromSlash(loader.root))
450
455
  prefix := ""
451
- for range 32 {
456
+ for {
452
457
  directory := path.Join(prefix, "node_modules", name)
453
458
  manifest := readPackageManifest(
454
459
  loader.root,
455
460
  path.Join(directory, "package.json"),
461
+ loader.inputs,
456
462
  )
457
463
  if manifest != nil {
458
464
  return directory, manifest
459
465
  }
466
+ parent := filepath.Dir(current)
467
+ if parent == current {
468
+ break
469
+ }
470
+ current = parent
460
471
  prefix = path.Join(prefix, "..")
461
472
  }
462
473
  return path.Join("node_modules", name), nil
@@ -474,7 +485,7 @@ func (loader *typeScriptLoader) locateInstalledPackage(
474
485
  // The second answer is what separates an exhausted chain from a resolved
475
486
  // directory, which are one string without it: a caller comparing paths against
476
487
  // a link that still names another link cannot tell that it holds one, and that
477
- // is how the leaf case of #1269 stayed silent until the caller was told.
488
+ // is how a leaf link stays silent unless the caller is told.
478
489
  //
479
490
  // The bound counts links followed, not answers given. A chain that ends exactly
480
491
  // on the last hop this rule follows has landed on its directory with no
@@ -485,17 +496,18 @@ func (loader *typeScriptLoader) locateInstalledPackage(
485
496
  // every earlier ending is a return from inside the loop. Refusing it instead
486
497
  // would turn a root that works into an error at the boundary, which is what
487
498
  // this rule exists to keep from happening in the other direction.
488
- func resolveLinkedDirectory(directory string) (string, bool) {
499
+ func resolveLinkedDirectory(directory string, readers ...evidenceInputReader) (string, bool) {
500
+ inputs := inputReader(readers)
489
501
  current := directory
490
502
  for range 32 {
491
- info, err := os.Lstat(current)
503
+ info, err := inputs.Lstat(current)
492
504
  if err != nil || info.IsDir() {
493
505
  return current, true
494
506
  }
495
- if target, err := os.Stat(current); err != nil || !target.IsDir() {
507
+ if target, err := inputs.Stat(current); err != nil || !target.IsDir() {
496
508
  return current, true
497
509
  }
498
- linked, err := os.Readlink(current)
510
+ linked, err := inputs.Readlink(current)
499
511
  if err != nil {
500
512
  return current, true
501
513
  }
@@ -504,7 +516,7 @@ func resolveLinkedDirectory(directory string) (string, bool) {
504
516
  }
505
517
  current = filepath.ToSlash(linked)
506
518
  }
507
- info, err := os.Lstat(current)
519
+ info, err := inputs.Lstat(current)
508
520
  return current, err == nil && info.IsDir()
509
521
  }
510
522
 
@@ -527,10 +539,10 @@ func (loader *typeScriptLoader) walk(base string) ([]string, string) {
527
539
  // owed there is a better cause, not a diagnostic that does not exist. A
528
540
  // declared base has no such report behind it, which is why
529
541
  // `resolvedBaseDirectory` does refuse.
530
- walked, _ := resolveLinkedDirectory(root)
542
+ walked, _ := resolveLinkedDirectory(root, loader.inputs)
531
543
  found := []string{}
532
544
  problem := ""
533
- err := filepath.WalkDir(walked, func(current string, entry fs.DirEntry, err error) error {
545
+ err := loader.inputs.WalkDir(walked, func(current string, entry fs.DirEntry, err error) error {
534
546
  if walked != root {
535
547
  if inside, ok := containedProjectPath(walked, filepath.ToSlash(current)); ok {
536
548
  current = path.Join(root, inside)