ttsc 0.27.0 → 0.28.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cmd/graphbench/main.go +26 -16
- package/cmd/graphdump/main.go +37 -12
- package/cmd/graphdump/run_declares_the_capability_its_output_carries_test.go +137 -0
- package/cmd/graphdump/run_reports_an_unknown_flag_without_exiting_the_process_test.go +56 -0
- package/cmd/graphdump/run_reports_an_unloadable_project_without_printing_a_dump_test.go +46 -0
- package/cmd/ttsc/api_transform.go +17 -6
- package/cmd/ttscgraph/dump.go +43 -11
- package/cmd/ttscgraph/run_dump_claims_artifacts_only_when_it_asked_test.go +135 -0
- package/cmd/ttscgraph/run_dump_declares_the_capability_its_output_carries_test.go +96 -0
- package/cmd/ttscgraph/serve.go +253 -25
- package/cmd/ttscgraph/serve_adopts_a_republished_artifact_set_test.go +348 -0
- package/cmd/ttscgraph/serve_projects_an_artifact_no_source_owns_test.go +90 -0
- package/cmd/ttscgraph/serve_shards.go +31 -9
- package/driver/config_discovery.go +121 -0
- package/driver/emit_plugin.go +57 -8
- package/driver/plugins.go +125 -14
- package/driver/transform_dependencies.go +283 -0
- package/driver/transform_dependencies_declares_every_file_without_a_contributor_test.go +28 -0
- package/driver/transform_dependencies_drops_a_self_reported_dependency_test.go +21 -0
- package/driver/transform_dependencies_keys_a_reported_path_like_the_envelope_test.go +46 -0
- package/driver/transform_dependencies_requires_every_contributor_to_declare_test.go +38 -0
- package/driver/transform_dependencies_stays_silent_for_an_undeclared_contributor_test.go +23 -0
- package/driver/transform_dependencies_withdraws_completeness_after_an_unusable_dependency_test.go +37 -0
- package/internal/graph/artifacts.go +230 -0
- package/internal/graph/artifacts_index_what_a_plugin_published_test.go +154 -0
- package/internal/graph/artifacts_survive_a_partial_rebuild_test.go +89 -0
- package/internal/graph/build.go +35 -0
- package/internal/graph/build_releases_the_scratch_it_no_longer_reads_test.go +118 -0
- package/internal/graph/doc_refs_are_the_use_the_checker_already_counts_test.go +93 -0
- package/internal/graph/doc_refs_resolve_documentation_links_test.go +335 -0
- package/internal/graph/doc_tags.go +416 -0
- package/internal/graph/doc_tags_are_claimed_before_they_are_read_test.go +131 -0
- package/internal/graph/doc_tags_capture_every_unrecognized_tag_test.go +165 -0
- package/internal/graph/doc_tags_read_only_what_the_parser_attached_test.go +97 -0
- package/internal/graph/doc_tags_stop_at_the_external_boundary_test.go +59 -0
- package/internal/graph/doc_tags_survive_line_endings_and_repeated_blocks_test.go +146 -0
- package/internal/graph/dump.go +60 -13
- package/internal/graph/dump_checkout_paths_are_stable_test.go +2 -2
- package/internal/graph/dump_paths.go +2 -2
- package/internal/graph/dump_paths_are_portable_and_injective_test.go +1 -1
- package/internal/graph/edge_kinds_names_every_declared_kind_test.go +49 -0
- package/internal/graph/edges.go +1 -0
- package/internal/graph/graph.go +79 -4
- package/internal/graph/provenance.go +29 -2
- package/lib/compiler/internal/project/createNativeProjectContextArgs.d.ts +9 -0
- package/lib/compiler/internal/project/createNativeProjectContextArgs.js +16 -4
- package/lib/compiler/internal/project/createNativeProjectContextArgs.js.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/launcher/internal/runtimeHooks.d.ts +10 -2
- package/lib/launcher/internal/runtimeHooks.js +93 -2
- package/lib/launcher/internal/runtimeHooks.js.map +1 -1
- package/lib/plugin/internal/capabilityResolutionCache.d.ts +76 -0
- package/lib/plugin/internal/capabilityResolutionCache.js +224 -0
- package/lib/plugin/internal/capabilityResolutionCache.js.map +1 -0
- package/lib/plugin/resolveCapabilityPlugins.d.ts +55 -0
- package/lib/plugin/resolveCapabilityPlugins.js +118 -0
- package/lib/plugin/resolveCapabilityPlugins.js.map +1 -0
- package/lib/structures/ITtscCompilerTransformation.d.ts +7 -0
- package/package.json +8 -8
- package/shim/ast/parent.go +16 -3
- package/shim/checker/enums_gen.go +11 -0
- package/shim/checker/shim.go +3 -0
- package/shim/compiler/emithook.go +10 -0
- package/src/compiler/internal/project/createNativeProjectContextArgs.ts +19 -4
- package/src/index.ts +1 -0
- package/src/launcher/internal/runtimeHooks.ts +111 -2
- package/src/plugin/internal/capabilityResolutionCache.ts +305 -0
- package/src/plugin/resolveCapabilityPlugins.ts +171 -0
- package/src/structures/ITtscCompilerTransformation.ts +7 -0
- package/tools/shim_audit/baseline.json +0 -1
- package/utility/host.go +15 -6
package/driver/emit_plugin.go
CHANGED
|
@@ -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
|
-
//
|
|
100
|
-
// MarkLinkedReferencesRecursively
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
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
|
-
|
|
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
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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{
|
|
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:
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
//
|
|
580
|
-
|
|
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
|
+
}
|