@ttsc/evidence 0.29.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,13 +13,25 @@ import type { ITtscEvidenceGraphReferenceBase } from "./ITtscEvidenceGraphRefere
13
13
  * contracts deserve documentary proof.
14
14
  */
15
15
  export interface ITtscEvidenceGraphTypeScriptReference extends ITtscEvidenceGraphReferenceBase<"typescript"> {
16
+ /**
17
+ * Explicit directory whose TypeScript exports are read from disk.
18
+ *
19
+ * Resolves relative to the lint project's root, or accepts an absolute path.
20
+ * Files patterns select entry modules relative to this directory. Their
21
+ * re-exports must remain inside it. Active Program sources take precedence
22
+ * over disk snapshots. The directory is watched for changes and new files.
23
+ *
24
+ * Cannot combine with package. Omit both to select only the active Program.
25
+ * File-qualified citation paths remain relative to the citing file.
26
+ */
27
+ root?: string;
16
28
  /**
17
29
  * Installed package whose declarations form this population.
18
30
  *
19
- * Omit it to select the active project. When present, {@link files} resolves
20
- * against the package root instead of the project root, so the globs read as
21
- * a consumer thinks of the package rather than carrying its `node_modules`
22
- * location.
31
+ * Omit it to select the active project or the explicit {@link root}. When
32
+ * present, {@link files} resolves against the package root instead of the
33
+ * project root, so the globs read as a consumer thinks of the package rather
34
+ * than carrying its `node_modules` location.
23
35
  *
24
36
  * A package population is read from disk rather than from the `ttsc` program.
25
37
  * That is the point: a symbol nothing imports is absent from the program by
@@ -39,8 +51,8 @@ export interface ITtscEvidenceGraphTypeScriptReference extends ITtscEvidenceGrap
39
51
  * `ttsc` project. A matching file outside the project's `tsconfig` program is
40
52
  * not available to the rule and does not count as a match.
41
53
  *
42
- * When {@link package} is set these are package-relative instead, and the
43
- * files are read from disk rather than from the program.
54
+ * With {@link root} or {@link package}, these are relative to that directory or
55
+ * package and entry files can be read from disk outside the Program.
44
56
  *
45
57
  * These are globs, not regular expressions. `*` matches within one path
46
58
  * segment, `**` crosses any number of path segments, and `?` matches one
@@ -58,8 +58,9 @@
58
58
  * prepend their namespace, such as `Orders.create`. A namespace itself uses its
59
59
  * qualified name, such as `Orders` or `Outer.Inner`. A local declaration
60
60
  * exposed as `export { Local as Public }` therefore uses `Public`. A named
61
- * default declaration keeps its declaration name; anonymous and default-only
62
- * aliases have no stable target and are not selected. Members of an ambient
61
+ * default declaration or default alias is publicly addressed as `default`;
62
+ * anonymous default classes and functions also have that stable address. A
63
+ * default-only local binding is not a named export. Members of an ambient
63
64
  * namespace are public without their own `export` modifier. A type-only alias
64
65
  * exposes a namespace or a class, its public type-space descendants, and
65
66
  * everything an object-shaped type alias, or an interface no class merges with,
@@ -79,17 +80,15 @@
79
80
  * form. Static class members use `ClassName.member`; instance members use
80
81
  * `ClassName.prototype.member`, and a constructor parameter property is an
81
82
  * instance member addressed that way. Computed names are not selected, even
82
- * when their expression is a literal. Literal names must be whitespace-free
83
- * because a declaration target is one whitespace-delimited token. A dot inside
84
- * a literal name is rendered unchanged; if that spelling collides with
85
- * qualification, the target is ambiguous.
83
+ * when their expression is a literal. File-qualified accessors use bracketed
84
+ * JSON strings for literal names, including spaces and dots. Existing inline
85
+ * links keep their dotted spelling and report ambiguous literal qualification.
86
86
  *
87
- * These targets deliberately omit file paths. If selected files expose the same
88
- * qualified target, a declaration using that target is ambiguous; rename or
89
- * further qualify the public symbols. A re-export whose declaration lives in
90
- * another file does not create a second unit in the barrel file. TypeScript
91
- * target characters are matched exactly; Markdown path-separator normalization
92
- * does not rewrite literal symbol names.
87
+ * A code citation identifies its module through a file path or the citing
88
+ * TypeScript module's imports. A re-export whose declaration lives in another
89
+ * file does not create a second unit in the barrel file. TypeScript target
90
+ * characters are matched exactly; Markdown path-separator normalization does
91
+ * not rewrite literal symbol names.
93
92
  *
94
93
  * Every supported public declaration described here may carry
95
94
  * `@evidenceExclude` when its file belongs to a claim, even when that claim's
package/native/config.go CHANGED
@@ -288,6 +288,7 @@ func decodeReference(
288
288
  Type: kind,
289
289
  Policy: policy,
290
290
  Root: root,
291
+ Rooted: kind == artifactTypeScript && object["root"] != nil,
291
292
  Files: files,
292
293
  Source: source,
293
294
  Package: packageName,
@@ -434,24 +435,8 @@ func rejectChecklistConflicts(
434
435
  return problems
435
436
  }
436
437
 
437
- // rejectForeignTypeScriptReference refuses a code population to a claim that
438
- // cannot address one.
439
- //
440
- // A symbol is cited through an inline link, and that link resolves in the
441
- // citing module's import scope — which only a TypeScript file has. Every other
442
- // claim would have to fall back to matching the bare name against one
443
- // repository-wide table, and that makes symbol-name uniqueness across the whole
444
- // repository load-bearing: two modules exporting `IPage` make the citation
445
- // impossible, and the only repair such a diagnostic can offer is renaming
446
- // production code to suit a lint rule.
447
- //
448
- // The check is here rather than at resolution because a configuration error
449
- // belongs where the configuration is read. Reported once per reference, before
450
- // any file is opened, instead of once per citation that later fails to resolve.
451
- //
452
- // What this gives up is the decision it reverses: documentation can no longer
453
- // cite code, and the inverse obligation is not the same one. Do not restore the fallback
454
- // without restoring that record.
438
+ // Code citations require an import scope or the Markdown/TypeScript file-link
439
+ // grammar. Bare repository-wide symbol lookup remains unavailable.
455
440
  func rejectForeignTypeScriptReference(
456
441
  claimKind artifactKind,
457
442
  referenceKind artifactKind,
@@ -459,13 +444,13 @@ func rejectForeignTypeScriptReference(
459
444
  ) string {
460
445
  if referenceKind != artifactTypeScript ||
461
446
  claimKind == artifactTypeScript ||
447
+ claimKind == artifactMarkdown ||
462
448
  claimKind == "" {
463
449
  return ""
464
450
  }
465
451
  return "Invalid evidence/graph configuration at " + path +
466
- ".type: only a TypeScript claim can cite TypeScript evidence, because a symbol citation resolves through the citing module's imports and a " +
467
- string(claimKind) +
468
- " comment has none. Invert the obligation so the code cites this artifact, or move the citation into TypeScript."
452
+ ".type: TypeScript evidence requires a Markdown or TypeScript claim; a " + string(claimKind) +
453
+ " claim does not support file-qualified code citations. Move the citation to a supported claim."
469
454
  }
470
455
 
471
456
  // decodeTypeScriptReference reads the two ways a TypeScript population is
@@ -482,6 +467,9 @@ func decodeTypeScriptReference(
482
467
  problems := []string{}
483
468
  reference := referenceSpec{}
484
469
  if raw, exists := object["package"]; exists {
470
+ if _, rooted := object["root"]; rooted {
471
+ problems = append(problems, "Invalid evidence/graph configuration at "+path+": 'root' and 'package' select different TypeScript populations and cannot be combined. Choose one base and its 'files' patterns.")
472
+ }
485
473
  var value string
486
474
  if err := json.Unmarshal(raw, &value); err != nil {
487
475
  problems = append(problems, "Invalid evidence/graph configuration at "+path+".package: expected an installed package name.")
@@ -588,15 +576,8 @@ func decodeClaimRoot(
588
576
  return decodePopulationRoot(raw, configPath)
589
577
  }
590
578
 
591
- // decodeRoot reads the directory a Markdown or Prisma population resolves
592
- // against.
593
- //
594
- // The property is refused on the two artifact kinds that cannot use it, and
595
- // each refusal names the channel that artifact does have. A TypeScript
596
- // population is materialized from the ttsc Program, so a directory outside the
597
- // project contains no file it could ever reach — `package` is the escape there,
598
- // and it already exists. A Swagger reference names one exact document, so the
599
- // location belongs in `file`, where it is visible without a second property.
579
+ // decodeRoot reads a population directory. A TypeScript reference uses its
580
+ // presence to opt into disk loading; Swagger still owns one exact file.
600
581
  func decodeRoot(
601
582
  raw json.RawMessage,
602
583
  kind artifactKind,
@@ -606,11 +587,7 @@ func decodeRoot(
606
587
  return "", nil
607
588
  }
608
589
  switch kind {
609
- case artifactMarkdown, artifactPrisma:
610
- case artifactTypeScript:
611
- return "", []string{
612
- "Invalid evidence/graph configuration at " + configPath + ": a TypeScript reference selects the active ttsc program with 'files', or an installed package with 'package'; only a TypeScript claim accepts 'root'.",
613
- }
590
+ case artifactMarkdown, artifactPrisma, artifactTypeScript:
614
591
  case artifactSwagger:
615
592
  return "", []string{
616
593
  "Invalid evidence/graph configuration at " + configPath + ": a Swagger reference owns one exact document; write the ancestor-relative or absolute location in 'file' instead.",
@@ -35,10 +35,14 @@ func commentHidingTag(comment string) string {
35
35
  comment = strings.TrimPrefix(comment, "/**")
36
36
  comment = strings.TrimPrefix(comment, "/*")
37
37
  comment = strings.TrimSuffix(comment, "*/")
38
+ fence := commentFence{}
38
39
  for _, rawLine := range strings.Split(comment, "\n") {
39
40
  line := strings.TrimSpace(rawLine)
40
41
  line = strings.TrimSpace(strings.TrimPrefix(line, "*"))
41
42
  line = strings.TrimSpace(strings.TrimPrefix(line, "///"))
43
+ if fence.consume(line) {
44
+ continue
45
+ }
42
46
  for _, tag := range hiddenDeclarationTags {
43
47
  if line != tag && !strings.HasPrefix(line, tag+" ") {
44
48
  continue
@@ -75,6 +79,7 @@ func parseCommentDeclarations(
75
79
  lineOffset int
76
80
  }
77
81
  var pending *pendingDeclaration
82
+ fence := commentFence{}
78
83
  parsed := []parsedDeclaration{}
79
84
  flush := func() {
80
85
  if pending == nil {
@@ -92,6 +97,12 @@ func parseCommentDeclarations(
92
97
  for index, rawLine := range lines {
93
98
  line := strings.TrimSpace(rawLine)
94
99
  line = strings.TrimSpace(strings.TrimPrefix(line, "*"))
100
+ if fence.consume(line) {
101
+ if pending != nil {
102
+ pending.body = append(pending.body, line)
103
+ }
104
+ continue
105
+ }
95
106
  tag, body, found := declarationLine(line)
96
107
  if found {
97
108
  flush()
@@ -136,6 +147,7 @@ func declarationLine(line string) (tagKind, string, bool) {
136
147
  }{
137
148
  {marker: "@evidenceExclude", tag: tagExclude},
138
149
  {marker: "@evidence", tag: tagEvidence},
150
+ {marker: "@link", tag: tagEvidence},
139
151
  } {
140
152
  if !strings.HasPrefix(line, candidate.marker) {
141
153
  continue
@@ -144,7 +156,11 @@ func declarationLine(line string) (tagKind, string, bool) {
144
156
  if remainder != "" && remainder[0] != ' ' && remainder[0] != '\t' {
145
157
  continue
146
158
  }
147
- return candidate.tag, strings.TrimSpace(remainder), true
159
+ body := strings.TrimSpace(remainder)
160
+ if candidate.marker == "@link" {
161
+ body = fileLinkPrefix + body
162
+ }
163
+ return candidate.tag, body, true
148
164
  }
149
165
  return "", "", false
150
166
  }
@@ -161,6 +177,9 @@ func splitDeclarationBody(body string) (string, string) {
161
177
  if body == "" {
162
178
  return "", ""
163
179
  }
180
+ if target, reason, found := splitFileLinkBody(body); found {
181
+ return target, reason
182
+ }
164
183
  if target, reason, found := splitInlineLinkBody(body); found {
165
184
  return target, reason
166
185
  }
@@ -221,6 +240,9 @@ func inlineLinkTarget(target string) string {
221
240
 
222
241
  // displayTarget renders a target the way its author wrote it.
223
242
  func displayTarget(target string) string {
243
+ if isFileLinkTarget(target) {
244
+ return strings.TrimPrefix(target, fileLinkPrefix)
245
+ }
224
246
  if isInlineLinkTarget(target) {
225
247
  return "{@link " + inlineLinkTarget(target) + "}"
226
248
  }
@@ -315,6 +337,7 @@ func parseReviews(comment string) []parsedReview {
315
337
  comment = strings.TrimSuffix(comment, "*/")
316
338
  reviews := []parsedReview{}
317
339
  var pending *parsedReview
340
+ fence := commentFence{}
318
341
  var body []string
319
342
  flush := func() {
320
343
  if pending == nil {
@@ -333,6 +356,12 @@ func parseReviews(comment string) []parsedReview {
333
356
  line := strings.TrimSpace(rawLine)
334
357
  line = strings.TrimSpace(strings.TrimPrefix(line, "*"))
335
358
  line = strings.TrimSpace(strings.TrimPrefix(line, "///"))
359
+ if fence.consume(line) {
360
+ if pending != nil {
361
+ body = append(body, line)
362
+ }
363
+ continue
364
+ }
336
365
  if reviews, remainder, opened := reviewLine(line); opened {
337
366
  flush()
338
367
  pending = &parsedReview{
@@ -442,6 +471,26 @@ func containsWhitespace(value string) bool {
442
471
  return false
443
472
  }
444
473
 
474
+ // Fences inside documentation are examples, not declarations. Their text is
475
+ // still retained when it belongs to a pending reason or review description.
476
+ type commentFence struct {
477
+ marker rune
478
+ length int
479
+ }
480
+
481
+ func (fence *commentFence) consume(line string) bool {
482
+ marker, length, remainder, opened := markdownFence(line)
483
+ if opened {
484
+ if fence.marker == 0 {
485
+ fence.marker, fence.length = marker, length
486
+ } else if marker == fence.marker && length >= fence.length && strings.TrimSpace(remainder) == "" {
487
+ fence.marker, fence.length = 0, 0
488
+ }
489
+ return true
490
+ }
491
+ return fence.marker != 0
492
+ }
493
+
445
494
  func lineAt(content string, offset int) int {
446
495
  if offset < 0 {
447
496
  return 1
@@ -0,0 +1,62 @@
1
+ package evidence
2
+
3
+ import shimast "github.com/microsoft/typescript-go/shim/ast"
4
+
5
+ // Default aliases retain the named declaration's identity. Anonymous class
6
+ // and function declarations own the module's default identity directly.
7
+ func typeScriptDeclarationName(node *shimast.Node) string {
8
+ if node == nil {
9
+ return ""
10
+ }
11
+ if name := declarationName(node.Name()); name != "" {
12
+ return name
13
+ }
14
+ if isDefaultExported(node) {
15
+ return "default"
16
+ }
17
+ return ""
18
+ }
19
+
20
+ func collectDefaultExportBindings(statements *shimast.NodeList) map[string]exportedName {
21
+ bindings := map[string]exportedName{}
22
+ if statements == nil {
23
+ return bindings
24
+ }
25
+ for _, statement := range statements.Nodes {
26
+ if statement == nil {
27
+ continue
28
+ }
29
+ if statement.Kind == shimast.KindExportAssignment {
30
+ assignment := statement.AsExportAssignment()
31
+ expression := assignment.Expression
32
+ if !assignment.IsExportEquals && expression != nil && expression.Kind == shimast.KindIdentifier {
33
+ local := declarationName(expression)
34
+ bindings[local] = exportedName{Public: local}
35
+ }
36
+ }
37
+ if statement.Kind != shimast.KindExportDeclaration {
38
+ continue
39
+ }
40
+ declaration := statement.AsExportDeclaration()
41
+ if declaration.ModuleSpecifier != nil || declaration.ExportClause == nil || declaration.ExportClause.Kind != shimast.KindNamedExports {
42
+ continue
43
+ }
44
+ for _, element := range declaration.ExportClause.AsNamedExports().Elements.Nodes {
45
+ specifier := element.AsExportSpecifier()
46
+ if declarationName(specifier.Name()) != "default" {
47
+ continue
48
+ }
49
+ local := specifier.PropertyName
50
+ if local == nil {
51
+ local = specifier.Name()
52
+ }
53
+ name := declarationName(local)
54
+ typeOnly := declaration.IsTypeOnly || specifier.IsTypeOnly
55
+ if previous, exists := bindings[name]; exists {
56
+ typeOnly = typeOnly && previous.TypeOnly
57
+ }
58
+ bindings[name] = exportedName{Public: name, TypeOnly: typeOnly}
59
+ }
60
+ }
61
+ return bindings
62
+ }
@@ -0,0 +1,275 @@
1
+ package evidence
2
+
3
+ import (
4
+ "sort"
5
+ "strings"
6
+ )
7
+
8
+ // Export names are finite even when modules re-export each other. Discover
9
+ // that vocabulary before resolving bindings, and expand namespace paths only
10
+ // after a binding is known. A module recursion guard cannot answer whether a
11
+ // particular export exists: the module may be busy resolving another name.
12
+ type typeScriptExportResolver struct {
13
+ loader *typeScriptLoader
14
+ modules map[string]*typeScriptExportModule
15
+ bindings map[scopedTargetKey][]reachedSymbol
16
+ owned *ownedUnitIndex
17
+ }
18
+
19
+ type typeScriptExportModule struct {
20
+ exports []moduleExport
21
+ targets map[string]string
22
+ names map[string]bool
23
+ named map[string][]moduleExport
24
+ stars []moduleExport
25
+ }
26
+
27
+ func newTypeScriptExportResolver(loader *typeScriptLoader, entries []string) *typeScriptExportResolver {
28
+ resolver := &typeScriptExportResolver{loader: loader, modules: map[string]*typeScriptExportModule{}, bindings: map[scopedTargetKey][]reachedSymbol{}}
29
+ resolver.owned = newOwnedUnitIndex(loader)
30
+ for _, entry := range entries {
31
+ resolver.load(entry)
32
+ }
33
+ keys := make([]string, 0, len(resolver.modules))
34
+ for key := range resolver.modules {
35
+ keys = append(keys, key)
36
+ }
37
+ sort.Strings(keys)
38
+ // Only star edges add names. Namespace and named re-exports already have
39
+ // explicit names, so namespace cycles cannot grow this fixed point forever.
40
+ for changed := true; changed; {
41
+ changed = false
42
+ for _, key := range keys {
43
+ module := resolver.modules[key]
44
+ for _, export := range module.stars {
45
+ target := resolver.modules[module.targets[export.Specifier]]
46
+ if target == nil {
47
+ continue
48
+ }
49
+ for name := range target.names {
50
+ if name != "default" && !module.names[name] {
51
+ module.names[name] = true
52
+ changed = true
53
+ }
54
+ }
55
+ }
56
+ }
57
+ }
58
+ if loader.boundary != nil {
59
+ for _, key := range keys {
60
+ module := resolver.modules[key]
61
+ for _, export := range module.exports {
62
+ if export.Specifier == "" || export.Namespace || export.Imported == "" {
63
+ continue
64
+ }
65
+ target := module.targets[export.Specifier]
66
+ if resolver.modules[target] == nil {
67
+ continue
68
+ }
69
+ if len(resolver.resolve(target, export.Imported)) == 0 {
70
+ loader.failures[key+" -> "+export.Specifier+"#"+export.Imported] = "the module has no public export named '" + export.Imported + "'"
71
+ }
72
+ }
73
+ }
74
+ }
75
+ return resolver
76
+ }
77
+
78
+ func (resolver *typeScriptExportResolver) load(entry string) {
79
+ if _, loaded := resolver.modules[entry]; loaded {
80
+ return
81
+ }
82
+ inventory := resolver.loader.inventory(entry)
83
+ if inventory == nil {
84
+ return
85
+ }
86
+ module := &typeScriptExportModule{exports: inventory.Exports, targets: map[string]string{}, names: map[string]bool{}, named: map[string][]moduleExport{}}
87
+ declarations := map[string]bool{}
88
+ if inventory.Source != nil && inventory.Source.Statements != nil {
89
+ for _, statement := range inventory.Source.Statements.Nodes {
90
+ if statement == nil {
91
+ continue
92
+ }
93
+ for _, declaration := range topLevelDeclaredNames(statement) {
94
+ declarations[declaration.Name] = true
95
+ }
96
+ if name := typeScriptDeclarationName(statement); name != "" {
97
+ declarations[name] = true
98
+ }
99
+ }
100
+ }
101
+ resolver.modules[entry] = module
102
+ for _, export := range module.exports {
103
+ if resolver.loader.boundary != nil && export.Specifier == "" && !declarations[export.Local] {
104
+ resolver.loader.failures[entry+"#"+export.Public] = "the exported binding '" + export.Local + "' has no declaration in this module"
105
+ continue
106
+ }
107
+ if export.Public != "" {
108
+ module.names[export.Public] = true
109
+ module.named[export.Public] = append(module.named[export.Public], export)
110
+ } else if export.Specifier != "" {
111
+ module.stars = append(module.stars, export)
112
+ }
113
+ if export.Specifier == "" {
114
+ continue
115
+ }
116
+ if _, resolved := module.targets[export.Specifier]; resolved {
117
+ continue
118
+ }
119
+ target := resolver.loader.resolve(entry, export.Specifier)
120
+ module.targets[export.Specifier] = target
121
+ if target != "" {
122
+ resolver.load(target)
123
+ }
124
+ }
125
+ }
126
+
127
+ // accessor follows only the namespace segments the citation actually names.
128
+ // Each hop consumes at least one query segment, so namespace cycles are finite.
129
+ // Legacy inline spelling can join several query segments into a literal export
130
+ // name; Address records the consumed query segments for the remaining lookup.
131
+ func (resolver *typeScriptExportResolver) accessor(entry string, segments []string, prefix []string, typeOnly bool, legacy bool) []reachedSymbol {
132
+ if len(segments) == 0 {
133
+ return nil
134
+ }
135
+ result := []reachedSymbol{}
136
+ limit := 1
137
+ if legacy {
138
+ limit = len(segments)
139
+ }
140
+ for consumed := 1; consumed <= limit; consumed++ {
141
+ name := strings.Join(segments[:consumed], ".")
142
+ for _, binding := range resolver.resolve(entry, name) {
143
+ binding.Address = append(append([]string{}, prefix...), segments[:consumed]...)
144
+ binding.TypeOnly = binding.TypeOnly || typeOnly
145
+ // Keep namespace prefixes for diagnostics, including when the next name
146
+ // is missing. They never become units in lookup or population traversal.
147
+ result = append(result, binding)
148
+ if binding.Local == "" {
149
+ result = append(result, resolver.accessor(binding.Path, segments[consumed:], binding.Address, binding.TypeOnly, legacy)...)
150
+ }
151
+ }
152
+ }
153
+ return result
154
+ }
155
+
156
+ func (resolver *typeScriptExportResolver) lookup(entry string, segments []string, legacy bool) []*evidenceUnit {
157
+ result := []*evidenceUnit{}
158
+ seen := map[string]bool{}
159
+ for _, binding := range resolver.accessor(entry, segments, nil, false, legacy) {
160
+ if binding.Local == "" {
161
+ continue
162
+ }
163
+ target := append([]string{binding.Local}, segments[len(binding.Address):]...)
164
+ for _, unit := range resolver.owned.of(binding.Path, binding.Local) {
165
+ if binding.TypeOnly && unit.ValueSpace && !unit.TypeSpace {
166
+ continue
167
+ }
168
+ matches := encodeTypeScriptIdentity(unit.Identity) == encodeTypeScriptIdentity(target)
169
+ if legacy {
170
+ matches = strings.Join(unit.Identity, ".") == strings.Join(target, ".")
171
+ }
172
+ if matches && !seen[unit.ID] {
173
+ seen[unit.ID] = true
174
+ result = append(result, unit)
175
+ }
176
+ }
177
+ }
178
+ return result
179
+ }
180
+
181
+ func (resolver *typeScriptExportResolver) resolve(module string, name string) []reachedSymbol {
182
+ key := scopedTargetKey{path: module, target: name}
183
+ if cached, exists := resolver.bindings[key]; exists {
184
+ return cached
185
+ }
186
+ resolved := resolver.resolveFrom(key, map[scopedTargetKey]bool{})
187
+ resolver.bindings[key] = resolved
188
+ return resolved
189
+ }
190
+
191
+ func (resolver *typeScriptExportResolver) resolveFrom(key scopedTargetKey, visited map[scopedTargetKey]bool) []reachedSymbol {
192
+ if visited[key] {
193
+ return nil
194
+ }
195
+ if cached, exists := resolver.bindings[key]; exists {
196
+ return cached
197
+ }
198
+ module := resolver.modules[key.path]
199
+ if module == nil {
200
+ return nil
201
+ }
202
+ visited[key] = true
203
+ defer delete(visited, key)
204
+ result := []reachedSymbol{}
205
+ byBinding := map[scopedTargetKey]int{}
206
+ add := func(binding reachedSymbol, typeOnly bool) {
207
+ binding.TypeOnly = binding.TypeOnly || typeOnly
208
+ identity := scopedTargetKey{path: binding.Path, target: binding.Local}
209
+ if index, exists := byBinding[identity]; exists {
210
+ result[index].TypeOnly = result[index].TypeOnly && binding.TypeOnly
211
+ return
212
+ }
213
+ byBinding[identity] = len(result)
214
+ result = append(result, binding)
215
+ }
216
+ exports := module.named[key.target]
217
+ if len(exports) == 0 && key.target != "default" {
218
+ exports = module.stars
219
+ }
220
+ for _, export := range exports {
221
+ if export.Specifier == "" {
222
+ local := export.Public
223
+ if export.Identity != "" {
224
+ local = export.Identity
225
+ }
226
+ add(reachedSymbol{Path: key.path, Local: local}, export.TypeOnly)
227
+ continue
228
+ }
229
+ target := module.targets[export.Specifier]
230
+ if resolver.modules[target] == nil {
231
+ continue
232
+ }
233
+ if export.Namespace {
234
+ add(reachedSymbol{Path: target}, export.TypeOnly)
235
+ continue
236
+ }
237
+ imported := key.target
238
+ if export.Imported != "" {
239
+ imported = export.Imported
240
+ }
241
+ for _, binding := range resolver.resolveFrom(scopedTargetKey{path: target, target: imported}, visited) {
242
+ add(binding, export.TypeOnly)
243
+ }
244
+ }
245
+ return result
246
+ }
247
+
248
+ func (resolver *typeScriptExportResolver) traverse(entry string, prefix []string, visited map[string]bool, typeOnly bool) []reachedSymbol {
249
+ if visited[entry] {
250
+ return nil
251
+ }
252
+ module := resolver.modules[entry]
253
+ if module == nil {
254
+ return nil
255
+ }
256
+ visited[entry] = true
257
+ defer delete(visited, entry)
258
+ names := make([]string, 0, len(module.names))
259
+ for name := range module.names {
260
+ names = append(names, name)
261
+ }
262
+ sort.Strings(names)
263
+ reached := []reachedSymbol{}
264
+ for _, name := range names {
265
+ for _, binding := range resolver.resolve(entry, name) {
266
+ binding.Address = append(append([]string{}, prefix...), name)
267
+ binding.TypeOnly = binding.TypeOnly || typeOnly
268
+ reached = append(reached, binding)
269
+ if binding.Local == "" {
270
+ reached = append(reached, resolver.traverse(binding.Path, binding.Address, visited, binding.TypeOnly)...)
271
+ }
272
+ }
273
+ }
274
+ return reached
275
+ }