ttsc 0.27.0 → 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.
Files changed (73) hide show
  1. package/cmd/graphbench/main.go +26 -16
  2. package/cmd/graphdump/main.go +37 -12
  3. package/cmd/graphdump/run_declares_the_capability_its_output_carries_test.go +137 -0
  4. package/cmd/graphdump/run_reports_an_unknown_flag_without_exiting_the_process_test.go +56 -0
  5. package/cmd/graphdump/run_reports_an_unloadable_project_without_printing_a_dump_test.go +46 -0
  6. package/cmd/ttsc/api_transform.go +17 -6
  7. package/cmd/ttscgraph/dump.go +43 -11
  8. package/cmd/ttscgraph/run_dump_claims_artifacts_only_when_it_asked_test.go +135 -0
  9. package/cmd/ttscgraph/run_dump_declares_the_capability_its_output_carries_test.go +96 -0
  10. package/cmd/ttscgraph/serve.go +253 -25
  11. package/cmd/ttscgraph/serve_adopts_a_republished_artifact_set_test.go +348 -0
  12. package/cmd/ttscgraph/serve_projects_an_artifact_no_source_owns_test.go +90 -0
  13. package/cmd/ttscgraph/serve_shards.go +31 -9
  14. package/driver/config_discovery.go +121 -0
  15. package/driver/emit_plugin.go +57 -8
  16. package/driver/plugins.go +125 -14
  17. package/driver/transform_dependencies.go +283 -0
  18. package/driver/transform_dependencies_declares_every_file_without_a_contributor_test.go +28 -0
  19. package/driver/transform_dependencies_drops_a_self_reported_dependency_test.go +21 -0
  20. package/driver/transform_dependencies_keys_a_reported_path_like_the_envelope_test.go +46 -0
  21. package/driver/transform_dependencies_requires_every_contributor_to_declare_test.go +38 -0
  22. package/driver/transform_dependencies_stays_silent_for_an_undeclared_contributor_test.go +23 -0
  23. package/driver/transform_dependencies_withdraws_completeness_after_an_unusable_dependency_test.go +37 -0
  24. package/internal/graph/artifacts.go +230 -0
  25. package/internal/graph/artifacts_index_what_a_plugin_published_test.go +154 -0
  26. package/internal/graph/artifacts_survive_a_partial_rebuild_test.go +89 -0
  27. package/internal/graph/build.go +35 -0
  28. package/internal/graph/build_releases_the_scratch_it_no_longer_reads_test.go +118 -0
  29. package/internal/graph/doc_refs_are_the_use_the_checker_already_counts_test.go +93 -0
  30. package/internal/graph/doc_refs_resolve_documentation_links_test.go +335 -0
  31. package/internal/graph/doc_tags.go +416 -0
  32. package/internal/graph/doc_tags_are_claimed_before_they_are_read_test.go +131 -0
  33. package/internal/graph/doc_tags_capture_every_unrecognized_tag_test.go +165 -0
  34. package/internal/graph/doc_tags_read_only_what_the_parser_attached_test.go +97 -0
  35. package/internal/graph/doc_tags_stop_at_the_external_boundary_test.go +59 -0
  36. package/internal/graph/doc_tags_survive_line_endings_and_repeated_blocks_test.go +146 -0
  37. package/internal/graph/dump.go +60 -13
  38. package/internal/graph/dump_checkout_paths_are_stable_test.go +2 -2
  39. package/internal/graph/dump_paths.go +2 -2
  40. package/internal/graph/dump_paths_are_portable_and_injective_test.go +1 -1
  41. package/internal/graph/edge_kinds_names_every_declared_kind_test.go +49 -0
  42. package/internal/graph/edges.go +1 -0
  43. package/internal/graph/graph.go +79 -4
  44. package/internal/graph/provenance.go +29 -2
  45. package/lib/compiler/internal/project/createNativeProjectContextArgs.d.ts +9 -0
  46. package/lib/compiler/internal/project/createNativeProjectContextArgs.js +16 -4
  47. package/lib/compiler/internal/project/createNativeProjectContextArgs.js.map +1 -1
  48. package/lib/index.d.ts +1 -0
  49. package/lib/index.js +1 -0
  50. package/lib/index.js.map +1 -1
  51. package/lib/launcher/internal/runtimeHooks.d.ts +10 -2
  52. package/lib/launcher/internal/runtimeHooks.js +93 -2
  53. package/lib/launcher/internal/runtimeHooks.js.map +1 -1
  54. package/lib/plugin/internal/capabilityResolutionCache.d.ts +76 -0
  55. package/lib/plugin/internal/capabilityResolutionCache.js +224 -0
  56. package/lib/plugin/internal/capabilityResolutionCache.js.map +1 -0
  57. package/lib/plugin/resolveCapabilityPlugins.d.ts +55 -0
  58. package/lib/plugin/resolveCapabilityPlugins.js +118 -0
  59. package/lib/plugin/resolveCapabilityPlugins.js.map +1 -0
  60. package/lib/structures/ITtscCompilerTransformation.d.ts +7 -0
  61. package/package.json +8 -8
  62. package/shim/ast/parent.go +16 -3
  63. package/shim/checker/enums_gen.go +11 -0
  64. package/shim/checker/shim.go +3 -0
  65. package/shim/compiler/emithook.go +10 -0
  66. package/src/compiler/internal/project/createNativeProjectContextArgs.ts +19 -4
  67. package/src/index.ts +1 -0
  68. package/src/launcher/internal/runtimeHooks.ts +111 -2
  69. package/src/plugin/internal/capabilityResolutionCache.ts +305 -0
  70. package/src/plugin/resolveCapabilityPlugins.ts +171 -0
  71. package/src/structures/ITtscCompilerTransformation.ts +7 -0
  72. package/tools/shim_audit/baseline.json +0 -1
  73. package/utility/host.go +15 -6
@@ -96,13 +96,30 @@ func (p *Program) EmitLinkedTransforms(writeFile shimcompiler.WriteFile) ([]Diag
96
96
  // because the emit context's update hook only records the original, it does not
97
97
  // copy `DeclarationBase.Symbol`.
98
98
  //
99
- // tsgo's emit resolver then walks the transformed tree in
100
- // MarkLinkedReferencesRecursively and, when it resolves an identifier whose
101
- // scope chain passes through one of those rebuilt containers, calls
102
- // getSymbolOfDeclaration(container) — which reads container.Symbol() and nil-
103
- // panics on a rebuilt class/interface/enum. Restoring the symbol from the
104
- // original (the symbol object is shared and node-independent for lookup) lets
105
- // the resolver mark references the same way it would on the parse tree.
99
+ // #201 added this against a real panic in the walk that
100
+ // MarkLinkedReferencesRecursively used to run over the transformed tree. That
101
+ // walk no longer happens here — the builtin chain is built from the parse tree
102
+ // below — so the original trigger is gone, and no probe reproduces a panic with
103
+ // this call stubbed out.
104
+ //
105
+ // Keep it anyway, because the remaining exposure is unproven rather than
106
+ // absent. tsgo's own defense against a plugin-built node is ast.IsParseTreeNode,
107
+ // which most EmitResolver reference methods test to bail out early — and a
108
+ // REBUILT container does not trip it. The emit context stamps
109
+ // NodeFlagsSynthesized when its factory creates a node, but ast.updateNode then
110
+ // ASSIGNS `updated.Flags = original.Flags` and clears the marker again, so a
111
+ // container carrying no symbol still answers "parse tree node" and is let
112
+ // through to checker name resolution. There resolveName reaches
113
+ // getSymbolOfDeclaration(container), an unguarded dereference for a class,
114
+ // class expression, or interface (enum and module are nil-checked upstream).
115
+ // What the probes could not settle is whether the builtin transformers ever
116
+ // actually hand a rebuilt container to one of those methods.
117
+ //
118
+ // Restoring the symbol from the original (the symbol object is shared and
119
+ // node-independent for lookup) makes that path resolve the way it would on the
120
+ // parse tree, at the cost of one walk per file. EmitContext.ParseNode is
121
+ // unaffected either way: it walks MostOriginal before testing the predicate, so
122
+ // it always lands on the genuine parse node.
106
123
  func restoreOriginalDeclarationSymbols(ec *shimprinter.EmitContext, node *shimast.Node) {
107
124
  if node == nil {
108
125
  return
@@ -180,7 +197,39 @@ func (p *Program) EmitWithPluginTransformers(transforms []PluginTransform, write
180
197
  }
181
198
  shimast.SetParentInChildrenUnset(out.AsNode())
182
199
  restoreOriginalDeclarationSymbols(ec, out.AsNode())
183
- for _, tr := range shimcompiler.GetScriptTransformers(ec, host, out) {
200
+ // Build the chain from the PARSE tree and transform the plugin's tree.
201
+ // Upstream passes one file to both roles only because it has no plugin
202
+ // pass between them: emitJSFile hands getScriptTransformers the file it
203
+ // parsed, then reassigns it to the transform result. Here they differ.
204
+ //
205
+ // getScriptTransformers reads its file for three things: in-JS-file, JSX
206
+ // language variant, and emitResolver.MarkLinkedReferencesRecursively. The
207
+ // first two survive UpdateSourceFile, so marking is the whole difference,
208
+ // and marking is a per-node checker resolution walk under the single
209
+ // checker mutex.
210
+ //
211
+ // Marking the plugin's tree does not merely spend that walk on nodes the
212
+ // binder never saw, it emits broken JavaScript. Elision reads the marks
213
+ // back for the file's parse-tree imports, so a reference the plugin
214
+ // REBUILT (a fresh identifier linked with SetOriginal, which is what any
215
+ // partial rewrite produces) leaves its import unmarked. The module
216
+ // transform still aliases that reference and elision still drops the
217
+ // binding the alias names: `dep_1.foo` with no `const dep_1 =
218
+ // require(...)`, a ReferenceError at load. The emit_plugin_rebuilt_*
219
+ // tests pin each import binding shape, plus the ES module lane where the
220
+ // whole import declaration disappears instead of just its binding.
221
+ //
222
+ // Nothing is lost by skipping the plugin's nodes. UpdateSourceFile
223
+ // rebuilds a SourceFile through copyFrom, which carries over neither
224
+ // Locals nor Symbol, and an ordinary import binds into Locals, so no
225
+ // resolveName branch reaches an import from a synthetic identifier. An
226
+ // import the plugin synthesized needs no mark at all: it has no parse
227
+ // original, and elision preserves it unconditionally.
228
+ //
229
+ // Marks accumulate on the checker and are never cleared, so marking one
230
+ // fixed tree per file also stops a second emit on the same Program from
231
+ // inheriting the first pass's plugin-tree marks.
232
+ for _, tr := range shimcompiler.GetScriptTransformers(ec, host, sf) {
184
233
  out = tr.TransformSourceFile(out)
185
234
  }
186
235
  // Print through the source-map-aware helper so a `sourceMap` /
package/driver/plugins.go CHANGED
@@ -106,6 +106,10 @@ type PluginContext struct {
106
106
  reportHostInputHashUnknown func(string)
107
107
  reportHostInputRealpath func(string, *string)
108
108
  reportHostInputRealpathUnknown func(string)
109
+ reportFileDependency func(string, string)
110
+ reportFileDependencyRejected func(string)
111
+ reportFileComplete func(string)
112
+ reportEveryFileComplete func()
109
113
  }
110
114
 
111
115
  // ReportHostInputHash declares the exact file state consumed by a native
@@ -186,6 +190,94 @@ func (ctx PluginContext) ReportHostInput(file string) {
186
190
  ctx.reportHostInput(filepath.Clean(file))
187
191
  }
188
192
 
193
+ // ReportFileDependency declares one file whose content influenced this
194
+ // plugin's contribution to the transformed file file. Both are resolved
195
+ // against the plugin's cwd and keyed like every other envelope section, so a
196
+ // plugin reports the program's own file names and the host joins the sections
197
+ // by key.
198
+ //
199
+ // Reporting alone only widens what a consumer invalidates on; it is
200
+ // ReportFileDependenciesComplete that turns the reported set into the file's
201
+ // whole input set.
202
+ //
203
+ // A dependency this cannot resolve to a key is not silently forgotten: it
204
+ // withdraws this plugin's completeness claim for that file, because a complete
205
+ // list missing a member is exactly what serves stale output. The plugin's other
206
+ // files keep theirs.
207
+ func (ctx PluginContext) ReportFileDependency(file string, dependency string) {
208
+ if ctx.reportFileDependency == nil {
209
+ return
210
+ }
211
+ target, ok := ctx.transformKey(file)
212
+ if !ok {
213
+ return
214
+ }
215
+ input, ok := ctx.transformKey(dependency)
216
+ if !ok {
217
+ if ctx.reportFileDependencyRejected != nil {
218
+ ctx.reportFileDependencyRejected(target)
219
+ }
220
+ return
221
+ }
222
+ ctx.reportFileDependency(target, input)
223
+ }
224
+
225
+ // ReportFileDependenciesComplete declares that everything this plugin's
226
+ // contribution to file consumed, beyond file's own text and the universal
227
+ // compiler-option chain, was reported through ReportFileDependency — possibly
228
+ // nothing at all.
229
+ //
230
+ // The declaration is per (plugin, file), the way the protocol's completeness
231
+ // contract defines it: the host lists a file in dependenciesComplete only when
232
+ // every plugin that can contribute to it declared it, because a consumer cannot
233
+ // attribute one plugin's entries back to it.
234
+ func (ctx PluginContext) ReportFileDependenciesComplete(file string) {
235
+ if ctx.reportFileComplete == nil {
236
+ return
237
+ }
238
+ target, ok := ctx.transformKey(file)
239
+ if !ok {
240
+ return
241
+ }
242
+ ctx.reportFileComplete(target)
243
+ }
244
+
245
+ // ReportDependenciesComplete makes the same declaration for every file in the
246
+ // program at once: this plugin's contribution to any transformed file is a
247
+ // function of that file's own text, whatever it reported for that file, its
248
+ // reported host inputs, and the compiler options.
249
+ //
250
+ // That is the honest claim of a syntactic transform — one that decides from the
251
+ // file in front of it and its own configuration rather than from the type
252
+ // system — and it is the only form available to a hook that never sees the
253
+ // program, such as SourcePreamble.
254
+ func (ctx PluginContext) ReportDependenciesComplete() {
255
+ if ctx.reportEveryFileComplete == nil {
256
+ return
257
+ }
258
+ ctx.reportEveryFileComplete()
259
+ }
260
+
261
+ // transformKey resolves one plugin-reported path to the key convention every
262
+ // envelope section uses. A plugin reports the spellings its host handed it
263
+ // (absolute program file names, or paths relative to the plugin's cwd), and
264
+ // the compiler's own file names are slash-normalized on every platform, so
265
+ // both sides must pass through TransformOutputKey to meet.
266
+ //
267
+ // Separators and the project prefix reconcile, but the suffix keeps the case
268
+ // the caller wrote: report the program's own spelling of a file rather than a
269
+ // re-cased one, or the key will not match on a case-insensitive filesystem and
270
+ // the declaration is dropped.
271
+ func (ctx PluginContext) transformKey(file string) (string, bool) {
272
+ if strings.TrimSpace(file) == "" {
273
+ return "", false
274
+ }
275
+ if !filepath.IsAbs(file) {
276
+ file = filepath.Join(ctx.Cwd, file)
277
+ }
278
+ return TransformOutputKey(ctx.Cwd, filepath.Clean(file)), true
279
+ }
280
+
189
281
  // SourcePreamblePlugin can inject source text before TypeScript-Go parses the
190
282
  // project. This is intentionally generic: the driver knows only the registered
191
283
  // plugin name and the project plugin manifest.
@@ -214,10 +306,11 @@ type EmitTransformPlugin interface {
214
306
  }
215
307
 
216
308
  type linkedPluginState struct {
217
- cwd string
218
- entries []PluginEntry
219
- inputs *pluginHostInputScopes
220
- tsconfig string
309
+ cwd string
310
+ declarations *pluginFileDeclarations
311
+ entries []PluginEntry
312
+ inputs *pluginHostInputScopes
313
+ tsconfig string
221
314
  }
222
315
 
223
316
  // pluginHostInputScopes keeps one observation set per plugin hook. A hash from
@@ -472,17 +565,23 @@ func RegisterPlugin(plugin any) {
472
565
  func loadLinkedPluginState(cwd, tsconfigPath string) (linkedPluginState, error) {
473
566
  input := strings.TrimSpace(os.Getenv(LinkedPluginsEnv))
474
567
  if input == "" {
475
- return linkedPluginState{cwd: cwd, inputs: newPluginHostInputScopes(), tsconfig: tsconfigPath}, nil
568
+ return linkedPluginState{
569
+ cwd: cwd,
570
+ declarations: newPluginFileDeclarations(),
571
+ inputs: newPluginHostInputScopes(),
572
+ tsconfig: tsconfigPath,
573
+ }, nil
476
574
  }
477
575
  var entries []PluginEntry
478
576
  if err := json.Unmarshal([]byte(input), &entries); err != nil {
479
577
  return linkedPluginState{}, fmt.Errorf("ttsc driver: invalid %s: %w", LinkedPluginsEnv, err)
480
578
  }
481
579
  return linkedPluginState{
482
- cwd: cwd,
483
- entries: entries,
484
- inputs: newPluginHostInputScopes(),
485
- tsconfig: tsconfigPath,
580
+ cwd: cwd,
581
+ declarations: newPluginFileDeclarations(),
582
+ entries: entries,
583
+ inputs: newPluginHostInputScopes(),
584
+ tsconfig: tsconfigPath,
486
585
  }, nil
487
586
  }
488
587
 
@@ -500,7 +599,7 @@ func (state linkedPluginState) sourcePreamble() (string, error) {
500
599
  if !ok {
501
600
  continue
502
601
  }
503
- text, err := preamble.SourcePreamble(state.context(entry))
602
+ text, err := preamble.SourcePreamble(state.contextAt(index, entry))
504
603
  if err != nil {
505
604
  return "", err
506
605
  }
@@ -521,7 +620,7 @@ func (state linkedPluginState) apply(prog *Program) error {
521
620
  if !ok {
522
621
  continue
523
622
  }
524
- if err := transform.ApplyProgram(prog, state.context(entry)); err != nil {
623
+ if err := transform.ApplyProgram(prog, state.contextAt(index, entry)); err != nil {
525
624
  return err
526
625
  }
527
626
  }
@@ -555,7 +654,7 @@ func (state linkedPluginState) emitTransforms() ([]PluginTransform, error) {
555
654
  if !ok {
556
655
  continue
557
656
  }
558
- transform, err := emitter.EmitTransform(state.context(entry))
657
+ transform, err := emitter.EmitTransform(state.contextAt(index, entry))
559
658
  if err != nil {
560
659
  return nil, err
561
660
  }
@@ -576,9 +675,17 @@ func registeredPlugin(index int) (any, bool) {
576
675
  return pluginRegistry[index], true
577
676
  }
578
677
 
579
- // context builds the PluginContext the driver passes to each plugin hook.
580
- func (state linkedPluginState) context(entry PluginEntry) PluginContext {
678
+ // contextAt builds the PluginContext the driver passes to one plugin hook.
679
+ //
680
+ // Host-input observations are scoped per hook, because a hash one hook reported
681
+ // cannot prove another hook's dependency on the same path. Dependency
682
+ // declarations are scoped per plugin instead: completeness is a claim about
683
+ // everything one plugin contributes to a file, so the claims of its preamble
684
+ // and program hooks are the same plugin's claim and must not be attributed
685
+ // separately.
686
+ func (state linkedPluginState) contextAt(index int, entry PluginEntry) PluginContext {
581
687
  inputs := state.inputs.newScope()
688
+ declaration := state.declarations.forPlugin(index)
582
689
  return PluginContext{
583
690
  Cwd: state.cwd,
584
691
  Entry: entry,
@@ -588,6 +695,10 @@ func (state linkedPluginState) context(entry PluginEntry) PluginContext {
588
695
  reportHostInputHashUnknown: inputs.invalidateHash,
589
696
  reportHostInputRealpath: inputs.addRealpath,
590
697
  reportHostInputRealpathUnknown: inputs.invalidateRealpath,
698
+ reportFileDependency: declaration.addDependency,
699
+ reportFileDependencyRejected: declaration.rejectDependency,
700
+ reportFileComplete: declaration.addComplete,
701
+ reportEveryFileComplete: declaration.completeEveryFile,
591
702
  }
592
703
  }
593
704
 
@@ -0,0 +1,283 @@
1
+ package driver
2
+
3
+ import (
4
+ "sort"
5
+ "sync"
6
+ )
7
+
8
+ // TransformDependencies is the transform envelope's dependency side channel:
9
+ // the per-file input lists a producer reports and the subset of files whose
10
+ // list is complete.
11
+ //
12
+ // A producer that declares nothing leaves both fields empty, so its envelope is
13
+ // byte-identical to what it was before this existed. The lane with no linked
14
+ // plugin at all is the deliberate exception: nothing there can contribute to a
15
+ // file, so every file is listed.
16
+ type TransformDependencies struct {
17
+ // Complete lists the files whose Dependencies entry is the whole input set
18
+ // beyond the file itself and the universal compiler-option chain.
19
+ Complete []string
20
+ // Dependencies maps a transformed file to the files whose content influenced
21
+ // its output, keyed and valued like every other envelope section.
22
+ Dependencies map[string][]string
23
+ }
24
+
25
+ // TransformDependenciesFor computes the dependency side channel for the files
26
+ // this program transforms, keyed against cwd.
27
+ //
28
+ // **The rule that makes any of this declarable.** ttsc's own source-to-source
29
+ // transform is syntactic: the built-in native host answers with each file's
30
+ // parsed text, and the linked-plugin generic host prints the parsed AST through
31
+ // a printer that is handed neither a checker nor an emit resolver. Neither lane
32
+ // runs the emit transformer chain, so none of the type-driven lowerings happen
33
+ // there — no type-driven import elision, no `design:type` metadata, no `enum`,
34
+ // `namespace`, or JSX lowering, and no declaration emit. The output of a file
35
+ // the host alone produced is therefore a function of that file's own text and
36
+ // the compiler options, and nothing the type system knows about any other file
37
+ // can change it.
38
+ //
39
+ // What that leaves is the plugins. A source preamble is prepended to every
40
+ // file's text before parsing, and a program plugin mutates the parsed AST, so
41
+ // either can make an output depend on anything it consulted — including, for a
42
+ // checker-driven plugin, the whole type graph. Such a plugin is the only party
43
+ // that knows what it read, which is why the declaration is per (plugin, file)
44
+ // and why a file is complete only when every plugin that can contribute to it
45
+ // declared it. An emit-only plugin is not a contributor here: its transform runs
46
+ // in `build`, which produces no envelope.
47
+ //
48
+ // With no linked plugin at all the contributor set is empty and every file is
49
+ // complete with an empty list, which is the rule above stated for the lane that
50
+ // has nothing but the host in it. A contributor that declares nothing leaves
51
+ // every file unlisted, which is exactly the behaviour of every producer written
52
+ // before this existed.
53
+ //
54
+ // An embedder that supplies LoadProgramOptions.SourcePreamble itself, rather
55
+ // than obtaining it from a linked plugin, makes the same claim about that text
56
+ // by calling this: the preamble must be a function of inputs its envelope
57
+ // reports elsewhere, the way a plugin's preamble is a function of the config
58
+ // files it reports as host inputs.
59
+ func (p *Program) TransformDependenciesFor(cwd string) TransformDependencies {
60
+ if p == nil {
61
+ return TransformDependencies{}
62
+ }
63
+ // Declarations are made from inside the plugin hooks, so they exist only
64
+ // after those hooks ran. The apply is latched, so this cannot re-run them.
65
+ _ = p.ApplyLinkedPlugins()
66
+ files := p.sourceFilesRaw()
67
+ keys := make([]string, 0, len(files))
68
+ for _, file := range files {
69
+ keys = append(keys, TransformOutputKey(cwd, file.FileName()))
70
+ }
71
+ return p.plugins.transformDependencies(keys)
72
+ }
73
+
74
+ // transformDependencies aggregates every contributing plugin's declarations
75
+ // over the envelope keys of the transformed files.
76
+ func (state linkedPluginState) transformDependencies(keys []string) TransformDependencies {
77
+ return aggregateTransformDependencies(keys, state.transformContributors(), state.declarations)
78
+ }
79
+
80
+ // aggregateTransformDependencies folds the declarations of the given
81
+ // contributors into one envelope side channel.
82
+ //
83
+ // Separated from the entry classification above so the aggregation rule can be
84
+ // exercised without the process-wide plugin registry that classification reads.
85
+ func aggregateTransformDependencies(keys []string, contributors []int, declarations *pluginFileDeclarations) TransformDependencies {
86
+ out := TransformDependencies{}
87
+ for _, key := range keys {
88
+ declared := 0
89
+ inputs := map[string]struct{}{}
90
+ for _, index := range contributors {
91
+ declaration := declarations.lookup(index)
92
+ if declaration == nil {
93
+ continue
94
+ }
95
+ for _, input := range declaration.dependenciesOf(key) {
96
+ // A file never depends on itself: the file's own text is outside the
97
+ // completeness contract by construction, and a self-edge would only
98
+ // make consumers register the module they are already transforming.
99
+ if input != key {
100
+ inputs[input] = struct{}{}
101
+ }
102
+ }
103
+ if declaration.declaresComplete(key) {
104
+ declared++
105
+ }
106
+ }
107
+ if len(inputs) != 0 {
108
+ out.Dependencies = appendDependencyEntry(out.Dependencies, key, inputs)
109
+ }
110
+ if declared == len(contributors) {
111
+ out.Complete = append(out.Complete, key)
112
+ }
113
+ }
114
+ sort.Strings(out.Complete)
115
+ return out
116
+ }
117
+
118
+ // appendDependencyEntry records one file's sorted dependency list, allocating
119
+ // the map only for a producer that reported something.
120
+ func appendDependencyEntry(into map[string][]string, key string, inputs map[string]struct{}) map[string][]string {
121
+ if into == nil {
122
+ into = map[string][]string{}
123
+ }
124
+ entry := make([]string, 0, len(inputs))
125
+ for input := range inputs {
126
+ entry = append(entry, input)
127
+ }
128
+ sort.Strings(entry)
129
+ into[key] = entry
130
+ return into
131
+ }
132
+
133
+ // transformContributors returns the indexes of the linked plugin entries that
134
+ // can influence source-to-source transform output.
135
+ //
136
+ // An entry with no registered plugin cannot be inspected, so it counts as a
137
+ // contributor that declared nothing; the host fails that entry elsewhere, and
138
+ // until it does, silence is the conservative answer.
139
+ func (state linkedPluginState) transformContributors() []int {
140
+ contributors := make([]int, 0, len(state.entries))
141
+ for index := range state.entries {
142
+ plugin, ok := registeredPlugin(index)
143
+ if !ok {
144
+ contributors = append(contributors, index)
145
+ continue
146
+ }
147
+ _, preamble := plugin.(SourcePreamblePlugin)
148
+ _, program := plugin.(ProgramPlugin)
149
+ if preamble || program {
150
+ contributors = append(contributors, index)
151
+ }
152
+ }
153
+ return contributors
154
+ }
155
+
156
+ // pluginFileDeclarations holds one declaration record per linked plugin entry.
157
+ type pluginFileDeclarations struct {
158
+ mu sync.Mutex
159
+ plugins map[int]*pluginFileDeclaration
160
+ }
161
+
162
+ // pluginFileDeclaration is one plugin's reported dependencies and completeness
163
+ // claims, keyed by envelope key.
164
+ type pluginFileDeclaration struct {
165
+ complete map[string]struct{}
166
+ completeAll bool
167
+ dependencies map[string]map[string]struct{}
168
+ mu sync.Mutex
169
+ // rejected holds the files whose reported list this plugin could not state
170
+ // in full, because one of its members was unusable as a key.
171
+ rejected map[string]struct{}
172
+ }
173
+
174
+ func newPluginFileDeclarations() *pluginFileDeclarations {
175
+ return &pluginFileDeclarations{plugins: map[int]*pluginFileDeclaration{}}
176
+ }
177
+
178
+ // forPlugin returns the declaration record of one plugin entry, creating it on
179
+ // first use. A nil ledger still answers a usable record so a hand-built state
180
+ // (unit tests, embedders) never panics through a plugin's reporting call.
181
+ func (declarations *pluginFileDeclarations) forPlugin(index int) *pluginFileDeclaration {
182
+ record := &pluginFileDeclaration{
183
+ complete: map[string]struct{}{},
184
+ dependencies: map[string]map[string]struct{}{},
185
+ rejected: map[string]struct{}{},
186
+ }
187
+ if declarations == nil {
188
+ return record
189
+ }
190
+ declarations.mu.Lock()
191
+ defer declarations.mu.Unlock()
192
+ if existing, ok := declarations.plugins[index]; ok {
193
+ return existing
194
+ }
195
+ declarations.plugins[index] = record
196
+ return record
197
+ }
198
+
199
+ // lookup returns the declaration record of one plugin entry, or nil when that
200
+ // entry never reported anything.
201
+ func (declarations *pluginFileDeclarations) lookup(index int) *pluginFileDeclaration {
202
+ if declarations == nil {
203
+ return nil
204
+ }
205
+ declarations.mu.Lock()
206
+ defer declarations.mu.Unlock()
207
+ return declarations.plugins[index]
208
+ }
209
+
210
+ func (declaration *pluginFileDeclaration) addDependency(file string, dependency string) {
211
+ if declaration == nil {
212
+ return
213
+ }
214
+ declaration.mu.Lock()
215
+ defer declaration.mu.Unlock()
216
+ entry, ok := declaration.dependencies[file]
217
+ if !ok {
218
+ entry = map[string]struct{}{}
219
+ declaration.dependencies[file] = entry
220
+ }
221
+ entry[dependency] = struct{}{}
222
+ }
223
+
224
+ // rejectDependency withdraws this plugin's completeness claim for one file.
225
+ func (declaration *pluginFileDeclaration) rejectDependency(file string) {
226
+ if declaration == nil {
227
+ return
228
+ }
229
+ declaration.mu.Lock()
230
+ defer declaration.mu.Unlock()
231
+ declaration.rejected[file] = struct{}{}
232
+ }
233
+
234
+ func (declaration *pluginFileDeclaration) addComplete(file string) {
235
+ if declaration == nil {
236
+ return
237
+ }
238
+ declaration.mu.Lock()
239
+ defer declaration.mu.Unlock()
240
+ declaration.complete[file] = struct{}{}
241
+ }
242
+
243
+ func (declaration *pluginFileDeclaration) completeEveryFile() {
244
+ if declaration == nil {
245
+ return
246
+ }
247
+ declaration.mu.Lock()
248
+ defer declaration.mu.Unlock()
249
+ declaration.completeAll = true
250
+ }
251
+
252
+ func (declaration *pluginFileDeclaration) dependenciesOf(file string) []string {
253
+ if declaration == nil {
254
+ return nil
255
+ }
256
+ declaration.mu.Lock()
257
+ defer declaration.mu.Unlock()
258
+ entry, ok := declaration.dependencies[file]
259
+ if !ok {
260
+ return nil
261
+ }
262
+ out := make([]string, 0, len(entry))
263
+ for input := range entry {
264
+ out = append(out, input)
265
+ }
266
+ return out
267
+ }
268
+
269
+ func (declaration *pluginFileDeclaration) declaresComplete(file string) bool {
270
+ if declaration == nil {
271
+ return false
272
+ }
273
+ declaration.mu.Lock()
274
+ defer declaration.mu.Unlock()
275
+ if _, rejected := declaration.rejected[file]; rejected {
276
+ return false
277
+ }
278
+ if declaration.completeAll {
279
+ return true
280
+ }
281
+ _, ok := declaration.complete[file]
282
+ return ok
283
+ }
@@ -0,0 +1,28 @@
1
+ package driver
2
+
3
+ import (
4
+ "reflect"
5
+ "testing"
6
+ )
7
+
8
+ // TestTransformDependenciesDeclaresEveryFileWithoutAContributor verifies the
9
+ // rule the host declares under when nothing else can contribute: a
10
+ // source-to-source transform is syntactic, so a file the host alone produced
11
+ // depends on its own text and the compiler options, and on nothing else.
12
+ //
13
+ // With an empty contributor set the aggregation is vacuously satisfied for
14
+ // every file, which is what lets a plugin-free project's consumer stop
15
+ // revalidating each delivered module's whole reference closure
16
+ // (samchon/ttsc#1259).
17
+ func TestTransformDependenciesDeclaresEveryFileWithoutAContributor(t *testing.T) {
18
+ keys := []string{"src/main.ts", "src/types.ts"}
19
+
20
+ out := aggregateTransformDependencies(keys, nil, newPluginFileDeclarations())
21
+
22
+ if !reflect.DeepEqual(out.Complete, []string{"src/main.ts", "src/types.ts"}) {
23
+ t.Fatalf("expected every file declared complete, got %v", out.Complete)
24
+ }
25
+ if out.Dependencies != nil {
26
+ t.Fatalf("expected no dependency entries, got %v", out.Dependencies)
27
+ }
28
+ }
@@ -0,0 +1,21 @@
1
+ package driver
2
+
3
+ import "testing"
4
+
5
+ // TestTransformDependenciesDropsASelfReportedDependency verifies a file never
6
+ // enters its own dependency list.
7
+ //
8
+ // The file's own text sits outside the completeness contract by construction —
9
+ // every consumer compares it before anything else — so a self-edge would only
10
+ // make a bundler register the module it is already transforming as one of that
11
+ // module's watch inputs.
12
+ func TestTransformDependenciesDropsASelfReportedDependency(t *testing.T) {
13
+ declarations := newPluginFileDeclarations()
14
+ declarations.forPlugin(0).addDependency("src/main.ts", "src/main.ts")
15
+
16
+ out := aggregateTransformDependencies([]string{"src/main.ts"}, []int{0}, declarations)
17
+
18
+ if len(out.Dependencies) != 0 {
19
+ t.Fatalf("expected the self-reported dependency to be dropped, got %v", out.Dependencies)
20
+ }
21
+ }
@@ -0,0 +1,46 @@
1
+ package driver
2
+
3
+ import (
4
+ "path/filepath"
5
+ "reflect"
6
+ "testing"
7
+ )
8
+
9
+ // TestTransformDependenciesKeysAReportedPathLikeTheEnvelope verifies a plugin's
10
+ // reported paths reach the envelope under the same key convention as every other
11
+ // section.
12
+ //
13
+ // A plugin reports the spellings its host handed it — absolute program file
14
+ // names, which the compiler normalizes to forward slashes on every platform, or
15
+ // paths relative to the plugin's cwd. Keying either differently from
16
+ // `typescript` would leave a consumer unable to join the sections at all, and on
17
+ // Windows the two spellings differ by separator alone, which is exactly the kind
18
+ // of mismatch that passes on POSIX CI and fails on a developer's machine.
19
+ func TestTransformDependenciesKeysAReportedPathLikeTheEnvelope(t *testing.T) {
20
+ // A real directory, so the absolute spellings below are absolute under the
21
+ // running platform's own rule rather than under POSIX's alone.
22
+ cwd := t.TempDir()
23
+ declarations := newPluginFileDeclarations()
24
+ ctx := PluginContext{
25
+ Cwd: cwd,
26
+ reportFileDependency: declarations.forPlugin(0).addDependency,
27
+ reportFileComplete: declarations.forPlugin(0).addComplete,
28
+ }
29
+
30
+ // The compiler's own spelling of a source file, and a cwd-relative one.
31
+ ctx.ReportFileDependency(
32
+ filepath.ToSlash(filepath.Join(cwd, "src", "main.ts")),
33
+ filepath.Join("src", "consulted.d.ts"),
34
+ )
35
+ ctx.ReportFileDependenciesComplete(filepath.Join(cwd, "src", "main.ts"))
36
+ ctx.ReportFileDependency(" ", "src/ignored.d.ts")
37
+
38
+ out := aggregateTransformDependencies([]string{"src/main.ts"}, []int{0}, declarations)
39
+
40
+ if !reflect.DeepEqual(out.Complete, []string{"src/main.ts"}) {
41
+ t.Fatalf("expected the reported file to key like the envelope, got %v", out.Complete)
42
+ }
43
+ if !reflect.DeepEqual(out.Dependencies["src/main.ts"], []string{"src/consulted.d.ts"}) {
44
+ t.Fatalf("expected the reported dependency to key like the envelope, got %v", out.Dependencies)
45
+ }
46
+ }