@ttsc/evidence 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +279 -0
  3. package/lib/index.d.ts +65 -0
  4. package/lib/index.js +87 -0
  5. package/lib/internal/loadPrismaModels.d.ts +1 -0
  6. package/lib/internal/loadPrismaModels.js +197 -0
  7. package/lib/internal/loadSwaggerOperations.d.ts +1 -0
  8. package/lib/internal/loadSwaggerOperations.js +156 -0
  9. package/lib/internal/normalizeSwaggerDocument.d.ts +1 -0
  10. package/lib/internal/normalizeSwaggerDocument.js +7 -0
  11. package/lib/structures/ITtscEvidenceDocumentedConfig.d.ts +32 -0
  12. package/lib/structures/ITtscEvidenceDocumentedConfig.js +2 -0
  13. package/lib/structures/ITtscEvidenceGraphClaim.d.ts +14 -0
  14. package/lib/structures/ITtscEvidenceGraphClaim.js +2 -0
  15. package/lib/structures/ITtscEvidenceGraphClaimBase.d.ts +102 -0
  16. package/lib/structures/ITtscEvidenceGraphClaimBase.js +2 -0
  17. package/lib/structures/ITtscEvidenceGraphConfig.d.ts +19 -0
  18. package/lib/structures/ITtscEvidenceGraphConfig.js +2 -0
  19. package/lib/structures/ITtscEvidenceGraphMarkdownClaim.d.ts +40 -0
  20. package/lib/structures/ITtscEvidenceGraphMarkdownClaim.js +2 -0
  21. package/lib/structures/ITtscEvidenceGraphMarkdownReference.d.ts +76 -0
  22. package/lib/structures/ITtscEvidenceGraphMarkdownReference.js +2 -0
  23. package/lib/structures/ITtscEvidenceGraphPrismaClaim.d.ts +65 -0
  24. package/lib/structures/ITtscEvidenceGraphPrismaClaim.js +2 -0
  25. package/lib/structures/ITtscEvidenceGraphPrismaReference.d.ts +75 -0
  26. package/lib/structures/ITtscEvidenceGraphPrismaReference.js +2 -0
  27. package/lib/structures/ITtscEvidenceGraphReference.d.ts +14 -0
  28. package/lib/structures/ITtscEvidenceGraphReference.js +2 -0
  29. package/lib/structures/ITtscEvidenceGraphReferenceBase.d.ts +73 -0
  30. package/lib/structures/ITtscEvidenceGraphReferenceBase.js +2 -0
  31. package/lib/structures/ITtscEvidenceGraphSwaggerReference.d.ts +39 -0
  32. package/lib/structures/ITtscEvidenceGraphSwaggerReference.js +2 -0
  33. package/lib/structures/ITtscEvidenceGraphTypeScriptClaim.d.ts +54 -0
  34. package/lib/structures/ITtscEvidenceGraphTypeScriptClaim.js +2 -0
  35. package/lib/structures/ITtscEvidenceGraphTypeScriptReference.d.ts +83 -0
  36. package/lib/structures/ITtscEvidenceGraphTypeScriptReference.js +2 -0
  37. package/lib/structures/index.d.ts +13 -0
  38. package/lib/structures/index.js +29 -0
  39. package/lib/typings/ITtscLintContributorRules.d.ts +34 -0
  40. package/lib/typings/ITtscLintContributorRules.js +2 -0
  41. package/lib/typings/ITtscLintRuleOptionsMap.d.ts +20 -0
  42. package/lib/typings/ITtscLintRuleOptionsMap.js +2 -0
  43. package/lib/typings/TtscEvidenceGraphMarkdownSymbol.d.ts +32 -0
  44. package/lib/typings/TtscEvidenceGraphMarkdownSymbol.js +2 -0
  45. package/lib/typings/TtscEvidenceGraphPrismaSymbol.d.ts +38 -0
  46. package/lib/typings/TtscEvidenceGraphPrismaSymbol.js +2 -0
  47. package/lib/typings/TtscEvidenceGraphTypeScriptSymbol.d.ts +64 -0
  48. package/lib/typings/TtscEvidenceGraphTypeScriptSymbol.js +2 -0
  49. package/lib/typings/index.d.ts +5 -0
  50. package/lib/typings/index.js +21 -0
  51. package/native/base.go +298 -0
  52. package/native/config.go +798 -0
  53. package/native/declaration.go +240 -0
  54. package/native/documented.go +353 -0
  55. package/native/exports.go +484 -0
  56. package/native/glob.go +207 -0
  57. package/native/graph.go +1409 -0
  58. package/native/health.go +45 -0
  59. package/native/hints.go +261 -0
  60. package/native/imports.go +373 -0
  61. package/native/inputs.go +125 -0
  62. package/native/loader.go +424 -0
  63. package/native/markdown.go +423 -0
  64. package/native/model.go +296 -0
  65. package/native/prisma.go +719 -0
  66. package/native/prisma_cache.go +213 -0
  67. package/native/prisma_locator.go +417 -0
  68. package/native/singular.go +369 -0
  69. package/native/swagger.go +331 -0
  70. package/native/swagger_cache.go +274 -0
  71. package/native/todo.go +138 -0
  72. package/native/typescript.go +1199 -0
  73. package/native/typescript_cache.go +96 -0
  74. package/package.json +64 -0
@@ -0,0 +1,125 @@
1
+ package evidence
2
+
3
+ import (
4
+ "strings"
5
+
6
+ "github.com/samchon/ttsc/packages/lint/rule"
7
+ )
8
+
9
+ // ProjectInputs declares the files this rule reads from outside the Program.
10
+ //
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.
18
+ //
19
+ // The host calls this after resolving options and project identity but before
20
+ // loading a Program, so nothing here may read `ctx.Sources`, and nothing here
21
+ // may touch the filesystem — this declares configured topology rather than the
22
+ // files a successful load happened to reach.
23
+ func (graphRule) ProjectInputs(ctx *rule.ProjectInputContext) []rule.ProjectInput {
24
+ if ctx == nil {
25
+ return nil
26
+ }
27
+ // Configuration problems belong to Check, which has a reporter and a source
28
+ // position to name. Declaring the inputs of whichever claims did decode is
29
+ // strictly better than declaring none: a graph the author is midway through
30
+ // repairing is exactly when the editor must keep watching the sources.
31
+ config, _ := decodeGraphConfig(ctx.Options)
32
+ return graphProjectInputs(config)
33
+ }
34
+
35
+ // graphProjectInputs collects the external dependency of every configured
36
+ // obligation.
37
+ //
38
+ // TypeScript claims and references are absent by design. Their inventories are
39
+ // materialized from `ctx.Sources`, which is the Program the host already
40
+ // watches, so declaring them again would ask for a second watcher on a file
41
+ // that already has one.
42
+ func graphProjectInputs(config graphConfig) []rule.ProjectInput {
43
+ config = enabledGraphConfig(config)
44
+ inputs := []rule.ProjectInput{}
45
+ for _, claim := range config.Claims {
46
+ switch claim.Type {
47
+ case artifactMarkdown, artifactPrisma:
48
+ inputs = append(inputs, globInputs(claim.Root, claim.Files)...)
49
+ }
50
+ for _, reference := range claim.References {
51
+ switch reference.Type {
52
+ case artifactMarkdown, artifactPrisma:
53
+ inputs = append(inputs, globInputs(reference.Root, reference.Files)...)
54
+ case artifactSwagger:
55
+ inputs = append(inputs, localSwaggerInputs(reference.Source)...)
56
+ }
57
+ }
58
+ }
59
+ return inputs
60
+ }
61
+
62
+ // globInputs publishes the positive half of one glob set, anchored on the root
63
+ // its population resolves against.
64
+ //
65
+ // Exclusions are dropped rather than translated, because the host's dependency
66
+ // model has no negation — declaring `!docs/private/**` as a glob would watch
67
+ // precisely the documents this graph refuses to read. What is left over-selects
68
+ // slightly, and that is the safe direction: a spurious rebuild costs one cycle,
69
+ // while a missing one leaves a stale citation reporting green, which is the
70
+ // whole failure this contract exists to remove.
71
+ //
72
+ // The declared pattern is the compiled segment form rather than `Raw`, so the
73
+ // host receives a pattern with the exclusion marker already stripped, `\`
74
+ // already normalized to `/`, and a leading `./` already gone.
75
+ //
76
+ // The root is prefixed as the author wrote it rather than resolved here. The
77
+ // host anchors a relative pattern against the same physical project root this
78
+ // rule uses (`linthost/project_inputs.go:151-171`), and it accepts one that
79
+ // ascends or is absolute — so `../../docs/**/*.md` and `C:/shared/docs/**/*.md`
80
+ // both arrive at the directory `Check` will read. Resolving it here would
81
+ // duplicate that arithmetic and make this contract depend on a project identity
82
+ // it is supposed to be able to answer without.
83
+ func globInputs(root string, globs globSet) []rule.ProjectInput {
84
+ inputs := make([]rule.ProjectInput, 0, len(globs.Patterns))
85
+ for _, pattern := range globs.Patterns {
86
+ if pattern.Exclude {
87
+ continue
88
+ }
89
+ joined := strings.Join(pattern.Segments, "/")
90
+ if root != "" {
91
+ joined = strings.TrimSuffix(root, "/") + "/" + joined
92
+ }
93
+ inputs = append(inputs, rule.ProjectInput{
94
+ Kind: rule.ProjectInputGlob,
95
+ Pattern: joined,
96
+ })
97
+ }
98
+ return inputs
99
+ }
100
+
101
+ // localSwaggerInputs publishes a Swagger reference only when it names a file.
102
+ //
103
+ // Withholding the URL form is not a shortcut. The host rejects a remote pattern
104
+ // outright (`linthost/project_inputs.go:159-161`), and one rejected input
105
+ // discards the whole snapshot for every project rule in the run
106
+ // (`linthost/project_inputs.go:113-135`) — so declaring a URL here would take
107
+ // this graph's Markdown globs down with it and restore the exact blindness the
108
+ // contract removes.
109
+ //
110
+ // The boundary is also real rather than imposed: a URL has no filesystem event
111
+ // to observe, so its freshness is per-evaluation and belongs to the loader.
112
+ //
113
+ // A local path is published exactly as it normalized, including one that
114
+ // ascends out of the project or names an absolute location. The host resolves
115
+ // both against the same root this rule reads them from, so a generated document
116
+ // in a sibling package is watched on the same terms as one inside the project.
117
+ func localSwaggerInputs(source string) []rule.ProjectInput {
118
+ if source == "" || isRemoteSwaggerSource(source) {
119
+ return nil
120
+ }
121
+ return []rule.ProjectInput{{
122
+ Kind: rule.ProjectInputFile,
123
+ Pattern: source,
124
+ }}
125
+ }
@@ -0,0 +1,424 @@
1
+ package evidence
2
+
3
+ import (
4
+ "encoding/json"
5
+ "io/fs"
6
+ "os"
7
+ "path"
8
+ "path/filepath"
9
+ "sort"
10
+ "strings"
11
+
12
+ shimast "github.com/microsoft/typescript-go/shim/ast"
13
+ shimcore "github.com/microsoft/typescript-go/shim/core"
14
+ shimparser "github.com/microsoft/typescript-go/shim/parser"
15
+ )
16
+
17
+ // typeScriptLoader materializes TypeScript inventories for a reference
18
+ // population, from the Program where possible and from disk where necessary.
19
+ //
20
+ // Disk parsing is not an optimization detail, it is the requirement. A package
21
+ // symbol that nothing imports is absent from the Program by definition, and
22
+ // that symbol is precisely the one an obligation needs to name — an evidence
23
+ // graph exists to report the operation the frontend never called.
24
+ type typeScriptLoader struct {
25
+ root string
26
+ program map[string]*artifactInventory
27
+ parsed map[string]*artifactInventory
28
+ resolved map[string]string
29
+ failures map[string]string
30
+ installs map[string]installedPackageLocation
31
+ }
32
+
33
+ func newTypeScriptLoader(
34
+ root string,
35
+ program map[string]*artifactInventory,
36
+ ) *typeScriptLoader {
37
+ loader := &typeScriptLoader{
38
+ root: strings.ReplaceAll(root, "\\", "/"),
39
+ program: map[string]*artifactInventory{},
40
+ parsed: map[string]*artifactInventory{},
41
+ resolved: map[string]string{},
42
+ failures: map[string]string{},
43
+ installs: map[string]installedPackageLocation{},
44
+ }
45
+ for _, inventory := range program {
46
+ if inventory == nil || inventory.Path == "" {
47
+ continue
48
+ }
49
+ location := loader.projectPath(inventory.Path)
50
+ current := loader.program[location]
51
+ if current == nil ||
52
+ current.Address != current.Path && inventory.Address == inventory.Path {
53
+ loader.program[location] = inventory
54
+ }
55
+ }
56
+ return loader
57
+ }
58
+
59
+ // inventory returns the scanned form of a project-relative TypeScript file.
60
+ //
61
+ // The Program's copy wins when it exists so that a file under edit is read as
62
+ // the editor has it, not as the disk last saw it.
63
+ func (loader *typeScriptLoader) inventory(relative string) *artifactInventory {
64
+ if relative == "" {
65
+ return nil
66
+ }
67
+ relative = loader.projectPath(relative)
68
+ if inventory := loader.program[relative]; inventory != nil {
69
+ return inventory
70
+ }
71
+ if inventory, cached := loader.parsed[relative]; cached {
72
+ return inventory
73
+ }
74
+ loader.parsed[relative] = loader.parse(relative)
75
+ return loader.parsed[relative]
76
+ }
77
+
78
+ func (loader *typeScriptLoader) parse(relative string) *artifactInventory {
79
+ relative = loader.projectPath(relative)
80
+ content, err := os.ReadFile(path.Join(loader.root, relative))
81
+ if err != nil {
82
+ loader.failures[relative] = err.Error()
83
+ return nil
84
+ }
85
+ kind := shimcore.ScriptKindTS
86
+ if strings.HasSuffix(strings.ToLower(relative), ".tsx") {
87
+ kind = shimcore.ScriptKindTSX
88
+ }
89
+ file := shimparser.ParseSourceFile(
90
+ shimast.SourceFileParseOptions{
91
+ FileName: path.Join(loader.root, relative),
92
+ },
93
+ string(content),
94
+ kind,
95
+ )
96
+ if file == nil {
97
+ loader.failures[relative] = "the TypeScript parser returned no source file"
98
+ return nil
99
+ }
100
+ return scanTypeScriptInventory(relative, file)
101
+ }
102
+
103
+ func (loader *typeScriptLoader) failure(relative string) string {
104
+ if loader == nil {
105
+ return ""
106
+ }
107
+ return loader.failures[relative]
108
+ }
109
+
110
+ // exists reports whether a project-relative TypeScript file can be read at all,
111
+ // without paying to scan it.
112
+ func (loader *typeScriptLoader) exists(relative string) bool {
113
+ relative = loader.projectPath(relative)
114
+ if loader.program[relative] != nil {
115
+ return true
116
+ }
117
+ return loader.existsOnDisk(relative)
118
+ }
119
+
120
+ // existsOnDisk probes the filesystem for an already-normalized path.
121
+ //
122
+ // The probe is the expensive half of module resolution — one syscall per
123
+ // candidate, and a watch cycle resolves every re-export in the population — so
124
+ // callers that can answer from the Program should do that first.
125
+ func (loader *typeScriptLoader) existsOnDisk(relative string) bool {
126
+ info, err := os.Stat(path.Join(loader.root, relative))
127
+ return err == nil && !info.IsDir()
128
+ }
129
+
130
+ // resolve maps a module specifier written in one file to a project-relative
131
+ // path, trying the same candidates TypeScript would.
132
+ func (loader *typeScriptLoader) resolve(from string, specifier string) string {
133
+ key := from + "\x00" + specifier
134
+ if cached, exists := loader.resolved[key]; exists {
135
+ return cached
136
+ }
137
+ loader.resolved[key] = loader.resolveUncached(from, specifier)
138
+ return loader.resolved[key]
139
+ }
140
+
141
+ func (loader *typeScriptLoader) resolveUncached(
142
+ from string,
143
+ specifier string,
144
+ ) string {
145
+ if strings.HasPrefix(specifier, "./") || strings.HasPrefix(specifier, "../") {
146
+ base := path.Clean(path.Join(path.Dir(from), specifier))
147
+ candidates := moduleCandidates(base)
148
+ normalized := make([]string, 0, len(candidates))
149
+ for _, candidate := range candidates {
150
+ normalized = append(normalized, loader.projectPath(candidate))
151
+ }
152
+ // The Program answers first, and not only because it answers without a
153
+ // syscall. A project that emits beside its sources has both `x.js` and
154
+ // `x.ts` on disk, and `x.js` is written earlier in the candidate order;
155
+ // resolving an import to emitted JavaScript would read a module whose
156
+ // declarations the graph cannot address.
157
+ for _, candidate := range normalized {
158
+ if loader.program[candidate] != nil {
159
+ return candidate
160
+ }
161
+ }
162
+ for _, candidate := range normalized {
163
+ if loader.existsOnDisk(candidate) {
164
+ return candidate
165
+ }
166
+ }
167
+ return ""
168
+ }
169
+ if strings.HasPrefix(specifier, "/") {
170
+ return ""
171
+ }
172
+ return loader.resolvePackage(specifier)
173
+ }
174
+
175
+ // projectPath gives every Program source and module candidate one identity
176
+ // relative to the ttsc project. A rooted claim may name the same physical file
177
+ // through `../api`, while an import can arrive there through a different
178
+ // sequence of sibling segments; resolving both through the project root keeps
179
+ // Windows and POSIX separators from creating distinct module identities.
180
+ func (loader *typeScriptLoader) projectPath(relative string) string {
181
+ if loader == nil || relative == "" {
182
+ return relative
183
+ }
184
+ // A path that is already a clean, forward-slashed, project-relative name is
185
+ // its own identity. Saying so here matters: module resolution normalizes
186
+ // every candidate of every specifier, and the general form below walks the
187
+ // path twice through the filesystem package to learn nothing.
188
+ if isCleanProjectRelativePath(relative) {
189
+ return relative
190
+ }
191
+ local := filepath.FromSlash(relative)
192
+ absolute := local
193
+ if !filepath.IsAbs(local) {
194
+ absolute = filepath.Join(filepath.FromSlash(loader.root), local)
195
+ }
196
+ projectRelative, err := filepath.Rel(
197
+ filepath.FromSlash(loader.root),
198
+ filepath.Clean(absolute),
199
+ )
200
+ if err != nil {
201
+ return filepath.ToSlash(filepath.Clean(absolute))
202
+ }
203
+ return strings.TrimPrefix(filepath.ToSlash(projectRelative), "./")
204
+ }
205
+
206
+ // isCleanProjectRelativePath reports whether a path is already the identity
207
+ // `projectPath` would produce: forward slashes, no drive or leading separator,
208
+ // and no `.` or `..` segment to collapse.
209
+ func isCleanProjectRelativePath(value string) bool {
210
+ if value == "" || strings.ContainsRune(value, '\\') {
211
+ return false
212
+ }
213
+ if strings.HasPrefix(value, "/") || filepath.IsAbs(value) {
214
+ return false
215
+ }
216
+ for segment := range strings.SplitSeq(value, "/") {
217
+ if segment == "" || segment == "." || segment == ".." {
218
+ return false
219
+ }
220
+ }
221
+ return true
222
+ }
223
+
224
+ // resolvePackage finds the declaration entry of an installed package.
225
+ //
226
+ // The entry comes from the `types` condition of an `exports` map, then
227
+ // `typesVersions`, then `types` or `typings`. A package that emits JavaScript
228
+ // stops there, because its runtime entry names the emit rather than the
229
+ // declarations a citation addresses.
230
+ //
231
+ // A source-first workspace package is followed one step further: when its
232
+ // `exports` target or `main` names TypeScript itself, that file is both the
233
+ // entry a consumer imports and the declarations, so it is the entry. This is
234
+ // the ordinary shape in a pnpm TypeScript monorepo, where the dependency is a
235
+ // link to a package that has no emit to point at.
236
+ func (loader *typeScriptLoader) resolvePackage(specifier string) string {
237
+ name, subpath := splitPackageSpecifier(specifier)
238
+ if name == "" {
239
+ return ""
240
+ }
241
+ directory, manifest := loader.installedPackage(name)
242
+ entry := packageTypeEntry(manifest, subpath)
243
+ if entry == "" {
244
+ if subpath == "" {
245
+ return ""
246
+ }
247
+ entry = subpath
248
+ }
249
+ for _, candidate := range moduleCandidates(path.Join(directory, entry)) {
250
+ if loader.exists(candidate) {
251
+ return candidate
252
+ }
253
+ }
254
+ return ""
255
+ }
256
+
257
+ // packageEntryModule resolves the declaration entry a package reference starts
258
+ // its traversal from.
259
+ func (loader *typeScriptLoader) packageEntryModule(name string) string {
260
+ return loader.resolvePackage(name)
261
+ }
262
+
263
+ // installedPackage finds where a package is installed, and its manifest.
264
+ //
265
+ // Resolution walks up from the project root the way Node's own does, because a
266
+ // nested Program does not have its own install. `packages/backend/test` is its
267
+ // own ttsc project — `test/tsconfig.json` compiles the tests together with the
268
+ // backend source — but the package manager installed into
269
+ // `packages/backend/node_modules`, one level above. Looking only beside the
270
+ // project root leaves that Program unable to see a dependency it imports, and a
271
+ // package reference that cannot read a manifest resolves no entry, which
272
+ // publishes its units under the module that matched instead of under the
273
+ // specifier a citation can spell.
274
+ //
275
+ // The walk stops at the filesystem root. A directory that holds no
276
+ // `node_modules` for this name simply is not the install, so the loop asks the
277
+ // parent rather than concluding the package is absent.
278
+ func (loader *typeScriptLoader) installedPackage(
279
+ name string,
280
+ ) (string, map[string]json.RawMessage) {
281
+ if cached, exists := loader.installs[name]; exists {
282
+ return cached.Directory, cached.Manifest
283
+ }
284
+ directory, manifest := loader.locateInstalledPackage(name)
285
+ loader.installs[name] = installedPackageLocation{
286
+ Directory: directory,
287
+ Manifest: manifest,
288
+ }
289
+ return directory, manifest
290
+ }
291
+
292
+ // installedPackageLocation caches one upward search. The walk costs a read per
293
+ // level, and both the glob base and the entry ask for the same package on every
294
+ // rebuild.
295
+ type installedPackageLocation struct {
296
+ Directory string
297
+ Manifest map[string]json.RawMessage
298
+ }
299
+
300
+ func (loader *typeScriptLoader) locateInstalledPackage(
301
+ name string,
302
+ ) (string, map[string]json.RawMessage) {
303
+ prefix := ""
304
+ for range 32 {
305
+ directory := path.Join(prefix, "node_modules", name)
306
+ manifest := readPackageManifest(
307
+ loader.root,
308
+ path.Join(directory, "package.json"),
309
+ )
310
+ if manifest != nil {
311
+ return directory, manifest
312
+ }
313
+ prefix = path.Join(prefix, "..")
314
+ }
315
+ return path.Join("node_modules", name), nil
316
+ }
317
+
318
+ // resolveLinkedDirectory returns the directory a path ultimately names.
319
+ //
320
+ // `filepath.EvalSymlinks` answers this for a POSIX symlink but not for a
321
+ // Windows junction, which is exactly what pnpm creates for a workspace
322
+ // dependency there: it returns the junction unchanged, so a caller that trusts
323
+ // it walks the link and finds nothing. `os.Readlink` does report a junction's
324
+ // target, and `os.Stat` sees through both, so ask those instead.
325
+ func resolveLinkedDirectory(directory string) string {
326
+ current := directory
327
+ for range 32 {
328
+ info, err := os.Lstat(current)
329
+ if err != nil || info.IsDir() {
330
+ return current
331
+ }
332
+ if target, err := os.Stat(current); err != nil || !target.IsDir() {
333
+ return current
334
+ }
335
+ linked, err := os.Readlink(current)
336
+ if err != nil {
337
+ return current
338
+ }
339
+ if !filepath.IsAbs(linked) {
340
+ linked = filepath.Join(filepath.Dir(current), linked)
341
+ }
342
+ current = filepath.ToSlash(linked)
343
+ }
344
+ return current
345
+ }
346
+
347
+ // walk lists the project-relative TypeScript files below a directory.
348
+ //
349
+ // A package's files are enumerated from disk for the same reason its entry is
350
+ // parsed from disk: the ones an obligation most needs to name are precisely the
351
+ // ones nothing imported, so the Program cannot be the source of truth.
352
+ func (loader *typeScriptLoader) walk(base string) ([]string, string) {
353
+ root := path.Join(loader.root, base)
354
+ // A workspace dependency is a link, not a directory: pnpm installs one that
355
+ // way on every platform, and npm and Yarn do the same for a linked package.
356
+ // `filepath.WalkDir` reports a link as a plain entry and descends into
357
+ // nothing, so walking the spelled path finds no files and the reference
358
+ // looks empty rather than unresolvable. Walk what the link points at, and
359
+ // report the files under the spelled path so addresses stay stable.
360
+ walked := resolveLinkedDirectory(root)
361
+ found := []string{}
362
+ problem := ""
363
+ err := filepath.WalkDir(walked, func(current string, entry fs.DirEntry, err error) error {
364
+ if walked != root {
365
+ if inside, ok := containedProjectPath(walked, filepath.ToSlash(current)); ok {
366
+ current = path.Join(root, inside)
367
+ } else if filepath.ToSlash(current) == walked {
368
+ current = root
369
+ }
370
+ }
371
+ if err != nil {
372
+ if problem == "" {
373
+ problem = err.Error()
374
+ }
375
+ if entry != nil && entry.IsDir() {
376
+ return filepath.SkipDir
377
+ }
378
+ return nil
379
+ }
380
+ if entry.IsDir() {
381
+ if entry.Name() == "node_modules" && filepath.ToSlash(current) != root {
382
+ return filepath.SkipDir
383
+ }
384
+ return nil
385
+ }
386
+ relative, ok := relativeProjectPath(loader.root, filepath.ToSlash(current))
387
+ if !ok {
388
+ // An install can sit above the project: a nested Program resolves its
389
+ // dependencies out of an ancestor's `node_modules`. Name such a file
390
+ // through the project the same way a rooted population above the
391
+ // project is named, rather than dropping it and reporting the package
392
+ // as empty.
393
+ relative = loader.projectPath(filepath.ToSlash(current))
394
+ }
395
+ if relative == "" || !isTypeScriptPath(relative) {
396
+ return nil
397
+ }
398
+ found = append(found, relative)
399
+ return nil
400
+ })
401
+ if err != nil && problem == "" {
402
+ problem = err.Error()
403
+ }
404
+ sort.Strings(found)
405
+ return found, problem
406
+ }
407
+
408
+ // referenceBase gives the directory a package reference enumerates.
409
+ //
410
+ // It asks the loader rather than assuming `node_modules` sits beside the
411
+ // project root, so the walk and the entry agree on where the package is. A
412
+ // nested Program is installed into an ancestor's `node_modules`, and a walk
413
+ // that looked only beside its own root would enumerate nothing while the entry
414
+ // resolved fine.
415
+ func referenceBase(loader *typeScriptLoader, reference referenceSpec) string {
416
+ if reference.Package == "" {
417
+ return ""
418
+ }
419
+ if loader == nil {
420
+ return path.Join("node_modules", reference.Package)
421
+ }
422
+ directory, _ := loader.installedPackage(reference.Package)
423
+ return directory
424
+ }