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.
- 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/cmd/graphbench/main.go
CHANGED
|
@@ -29,10 +29,16 @@ type metrics struct {
|
|
|
29
29
|
Nodes int `json:"nodes"`
|
|
30
30
|
ExternalNodes int `json:"externalNodes"`
|
|
31
31
|
Edges map[string]int `json:"edges"`
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
// EdgeVocabulary names which of the three edge-kind vocabularies Edges is
|
|
33
|
+
// keyed by, so a reader comparing these numbers against a dump does not
|
|
34
|
+
// compare the wrong things. Build produces *internal* kinds; a dump carries
|
|
35
|
+
// finer *wire* kinds (value-call splits into calls/instantiates/renders);
|
|
36
|
+
// the viewer folds wire kinds onto *display* families.
|
|
37
|
+
EdgeVocabulary string `json:"edgeVocabulary"`
|
|
38
|
+
TotalEdges int `json:"totalEdges"`
|
|
39
|
+
SymbolFiles int `json:"symbolFiles"`
|
|
40
|
+
CoveredFiles int `json:"coveredFiles"`
|
|
41
|
+
Coverage float64 `json:"coverage"`
|
|
36
42
|
}
|
|
37
43
|
|
|
38
44
|
func main() {
|
|
@@ -82,10 +88,13 @@ func run() int {
|
|
|
82
88
|
// counts, edges per kind, and fair coverage (workspace files holding at least one
|
|
83
89
|
// node that an edge connects to a node in another file).
|
|
84
90
|
func summarize(g *graph.Graph, sourceFiles int) metrics {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
91
|
+
// Seeded with every kind Build can produce, so a family that happened not to
|
|
92
|
+
// occur reports 0 rather than vanishing from the object. Three of the seven
|
|
93
|
+
// used to be seeded and the map grew the rest on first sight, which meant the
|
|
94
|
+
// reported shape depended on the project measured.
|
|
95
|
+
edges := map[string]int{}
|
|
96
|
+
for _, kind := range graph.EdgeKinds() {
|
|
97
|
+
edges[string(kind)] = 0
|
|
89
98
|
}
|
|
90
99
|
for _, edge := range g.Edges {
|
|
91
100
|
edges[string(edge.Kind)]++
|
|
@@ -123,13 +132,14 @@ func summarize(g *graph.Graph, sourceFiles int) metrics {
|
|
|
123
132
|
}
|
|
124
133
|
|
|
125
134
|
return metrics{
|
|
126
|
-
SourceFiles:
|
|
127
|
-
Nodes:
|
|
128
|
-
ExternalNodes:
|
|
129
|
-
Edges:
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
135
|
+
SourceFiles: sourceFiles,
|
|
136
|
+
Nodes: len(g.Nodes),
|
|
137
|
+
ExternalNodes: external,
|
|
138
|
+
Edges: edges,
|
|
139
|
+
EdgeVocabulary: "internal",
|
|
140
|
+
TotalEdges: len(g.Edges),
|
|
141
|
+
SymbolFiles: len(symbolFiles),
|
|
142
|
+
CoveredFiles: len(coveredFiles),
|
|
143
|
+
Coverage: coverage,
|
|
134
144
|
}
|
|
135
145
|
}
|
package/cmd/graphdump/main.go
CHANGED
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
package main
|
|
7
7
|
|
|
8
8
|
import (
|
|
9
|
+
"errors"
|
|
9
10
|
"flag"
|
|
10
11
|
"fmt"
|
|
12
|
+
"io"
|
|
11
13
|
"os"
|
|
12
14
|
"path/filepath"
|
|
13
15
|
|
|
@@ -17,18 +19,37 @@ import (
|
|
|
17
19
|
"github.com/samchon/ttsc/packages/ttsc/internal/graph"
|
|
18
20
|
)
|
|
19
21
|
|
|
22
|
+
// Package-level streams so command tests can capture I/O without patching the
|
|
23
|
+
// os globals, and argv arrives as a parameter for the same reason. Both mirror
|
|
24
|
+
// the shipped sibling in cmd/ttscgraph, whose capability claim is tested the
|
|
25
|
+
// same way; this command's claim went undefended because it had neither seam.
|
|
26
|
+
var (
|
|
27
|
+
stdout io.Writer = os.Stdout
|
|
28
|
+
stderr io.Writer = os.Stderr
|
|
29
|
+
)
|
|
30
|
+
|
|
20
31
|
func main() {
|
|
21
|
-
os.Exit(run())
|
|
32
|
+
os.Exit(run(os.Args[1:]))
|
|
22
33
|
}
|
|
23
34
|
|
|
24
|
-
func run() int {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
35
|
+
func run(args []string) int {
|
|
36
|
+
fs := flag.NewFlagSet("graphdump", flag.ContinueOnError)
|
|
37
|
+
fs.SetOutput(stderr)
|
|
38
|
+
cwd := fs.String("cwd", ".", "project root")
|
|
39
|
+
tsconfig := fs.String("tsconfig", "tsconfig.json", "tsconfig path, relative to cwd")
|
|
40
|
+
pretty := fs.Bool("pretty", false, "indent the JSON output")
|
|
41
|
+
if err := fs.Parse(args); err != nil {
|
|
42
|
+
// `-h` asks for the usage this just printed, so it is a request that
|
|
43
|
+
// succeeded rather than an argument that failed. Under the global flag set
|
|
44
|
+
// this command used to read, `ExitOnError` already exited 0 for it.
|
|
45
|
+
if errors.Is(err, flag.ErrHelp) {
|
|
46
|
+
return 0
|
|
47
|
+
}
|
|
48
|
+
return 2
|
|
49
|
+
}
|
|
29
50
|
|
|
30
51
|
// Resolve the project root the same way LoadProgram does (absolute, then
|
|
31
|
-
// tsgo-normalized) so the
|
|
52
|
+
// tsgo-normalized) so the dump path mapper receives the same canonical root
|
|
32
53
|
// grammar and drive-letter case as the compiler's source paths.
|
|
33
54
|
root := *cwd
|
|
34
55
|
if abs, err := filepath.Abs(root); err == nil {
|
|
@@ -38,11 +59,11 @@ func run() int {
|
|
|
38
59
|
|
|
39
60
|
prog, _, err := driver.LoadProgram(root, *tsconfig, driver.LoadProgramOptions{})
|
|
40
61
|
if err != nil {
|
|
41
|
-
fmt.Fprintf(
|
|
62
|
+
fmt.Fprintf(stderr, "graphdump: could not load %s/%s: %v\n", root, *tsconfig, err)
|
|
42
63
|
return 1
|
|
43
64
|
}
|
|
44
65
|
if prog == nil {
|
|
45
|
-
fmt.Fprintf(
|
|
66
|
+
fmt.Fprintf(stderr, "graphdump: could not load %s/%s\n", root, *tsconfig)
|
|
46
67
|
return 1
|
|
47
68
|
}
|
|
48
69
|
defer func() { _ = prog.Close() }()
|
|
@@ -62,7 +83,11 @@ func run() int {
|
|
|
62
83
|
// No version: this tool is built from the tree on demand and never
|
|
63
84
|
// stamped, and an invented one would be worse than an absent one.
|
|
64
85
|
graph.Producer{Tool: "graphdump", Typescript: graph.TypescriptVersion()},
|
|
65
|
-
|
|
86
|
+
// The declaration walk is the shipped one, so this dump carries every
|
|
87
|
+
// documentation tag and must say so: under the capability contract an
|
|
88
|
+
// absent claim means the producer never looked, which would make a
|
|
89
|
+
// consumer read a tagged declaration as citing nothing.
|
|
90
|
+
[]string{graph.CapabilitySourceDigests, graph.CapabilityDocTags},
|
|
66
91
|
nil,
|
|
67
92
|
nil,
|
|
68
93
|
texts,
|
|
@@ -70,9 +95,9 @@ func run() int {
|
|
|
70
95
|
),
|
|
71
96
|
}, *pretty)
|
|
72
97
|
if err != nil {
|
|
73
|
-
fmt.Fprintf(
|
|
98
|
+
fmt.Fprintf(stderr, "graphdump: %v\n", err)
|
|
74
99
|
return 1
|
|
75
100
|
}
|
|
76
|
-
fmt.
|
|
101
|
+
fmt.Fprintln(stdout, string(data))
|
|
77
102
|
return 0
|
|
78
103
|
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"os"
|
|
7
|
+
"path/filepath"
|
|
8
|
+
"slices"
|
|
9
|
+
"strings"
|
|
10
|
+
"testing"
|
|
11
|
+
|
|
12
|
+
"github.com/samchon/ttsc/packages/ttsc/internal/graph"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
// TestRunDeclaresTheCapabilityItsOutputCarries verifies that every capability
|
|
16
|
+
// this command declares is paired with the output that backs it, and that a
|
|
17
|
+
// project carrying no documentation tag emits that field nowhere.
|
|
18
|
+
//
|
|
19
|
+
// The claim is a separate literal from the shipped `ttscgraph dump`'s, so the
|
|
20
|
+
// test that guards that one guards nothing here: deleting graph.CapabilityDocTags
|
|
21
|
+
// from this command's list used to break nothing at all, in a package that had
|
|
22
|
+
// never had a test file. The claim matters because the two states a consumer must
|
|
23
|
+
// distinguish — "this declaration cites nothing" and "this producer never
|
|
24
|
+
// looked" — are the same absent field, so a dump that carries tags and forgets to
|
|
25
|
+
// say so is read as a repository where nothing cites anything.
|
|
26
|
+
//
|
|
27
|
+
// The declared set is compared whole rather than member by member, so a
|
|
28
|
+
// capability added to the list arrives with the evidence for it or fails here.
|
|
29
|
+
//
|
|
30
|
+
// 1. Run the command over a project with a tag, capturing stdout.
|
|
31
|
+
// 2. Assert the declared set is exactly what this command claims, and that each
|
|
32
|
+
// claim is backed: a tag in the output, and a digested source.
|
|
33
|
+
// 3. Run it over a project with no tag and assert the field appears nowhere,
|
|
34
|
+
// while both claims still do.
|
|
35
|
+
func TestRunDeclaresTheCapabilityItsOutputCarries(t *testing.T) {
|
|
36
|
+
tagged := runGraphdump(t, `/** @evidence docs/a.md#x Cited. */
|
|
37
|
+
export function subject(): void {}
|
|
38
|
+
`)
|
|
39
|
+
untagged := runGraphdump(t, `/** Ordinary documentation. */
|
|
40
|
+
export function subject(): void {}
|
|
41
|
+
`)
|
|
42
|
+
|
|
43
|
+
declared := []string{graph.CapabilityDocTags, graph.CapabilitySourceDigests}
|
|
44
|
+
slices.Sort(declared)
|
|
45
|
+
for name, raw := range map[string]string{"tagged": tagged, "untagged": untagged} {
|
|
46
|
+
var parsed struct {
|
|
47
|
+
Provenance struct {
|
|
48
|
+
Capabilities []string `json:"capabilities"`
|
|
49
|
+
Sources []struct {
|
|
50
|
+
File string `json:"file"`
|
|
51
|
+
CheckerDigest string `json:"checkerDigest"`
|
|
52
|
+
} `json:"sources"`
|
|
53
|
+
} `json:"provenance"`
|
|
54
|
+
}
|
|
55
|
+
if err := json.Unmarshal([]byte(raw), &parsed); err != nil {
|
|
56
|
+
t.Fatalf("%s dump is not JSON: %v", name, err)
|
|
57
|
+
}
|
|
58
|
+
got := slices.Clone(parsed.Provenance.Capabilities)
|
|
59
|
+
slices.Sort(got)
|
|
60
|
+
if !slices.Equal(got, declared) {
|
|
61
|
+
t.Fatalf("%s dump declares %v, want %v; an absent claim means the producer "+
|
|
62
|
+
"never looked, so a tagged declaration reads as citing nothing and a "+
|
|
63
|
+
"digested source reads as unread",
|
|
64
|
+
name, got, declared)
|
|
65
|
+
}
|
|
66
|
+
// The sourceDigests claim, backed the same way the docTags claim is: a
|
|
67
|
+
// declared capability whose output carries nothing is the tautology this
|
|
68
|
+
// test exists to avoid.
|
|
69
|
+
digested := 0
|
|
70
|
+
for _, source := range parsed.Provenance.Sources {
|
|
71
|
+
if source.CheckerDigest != "" {
|
|
72
|
+
digested++
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if digested == 0 {
|
|
76
|
+
t.Fatalf("%s dump declares %q over %d sources, none of which carries a checker digest",
|
|
77
|
+
name, graph.CapabilitySourceDigests, len(parsed.Provenance.Sources))
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if !strings.Contains(tagged, `"docs/a.md#x Cited."`) {
|
|
82
|
+
t.Fatalf("the tagged dump carried no tag text")
|
|
83
|
+
}
|
|
84
|
+
// Read the raw document rather than a decoded slice: an absent field and an
|
|
85
|
+
// empty one decode identically, and absent is what the wire contract says. The
|
|
86
|
+
// key is matched with its colon, because the capability shares the name and
|
|
87
|
+
// rides the same document as a bare string.
|
|
88
|
+
if strings.Contains(untagged, `"docTags":`) {
|
|
89
|
+
t.Fatalf("the untagged dump carried a docTags field; it must be absent")
|
|
90
|
+
}
|
|
91
|
+
if !strings.Contains(tagged, `"docTags":`) {
|
|
92
|
+
t.Fatalf("the tagged dump carried no docTags field, so the check above proves nothing")
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// runGraphdump runs the command over a one-file project and returns its stdout.
|
|
97
|
+
//
|
|
98
|
+
// It goes through run(args) and the package streams rather than through os.Args,
|
|
99
|
+
// flag.CommandLine, and an os.Stdout pipe. Those seams exist on the shipped
|
|
100
|
+
// sibling for this reason, and this command grew them so its own claim could be
|
|
101
|
+
// asserted against its own output without patching process globals.
|
|
102
|
+
func runGraphdump(t *testing.T, source string) string {
|
|
103
|
+
t.Helper()
|
|
104
|
+
root := t.TempDir()
|
|
105
|
+
writeGraphdumpFile(t, filepath.Join(root, "tsconfig.json"), `{
|
|
106
|
+
"compilerOptions": {
|
|
107
|
+
"target": "ES2022",
|
|
108
|
+
"module": "commonjs",
|
|
109
|
+
"strict": true,
|
|
110
|
+
"rootDir": "src",
|
|
111
|
+
"outDir": "dist"
|
|
112
|
+
},
|
|
113
|
+
"files": ["src/main.ts"]
|
|
114
|
+
}
|
|
115
|
+
`)
|
|
116
|
+
writeGraphdumpFile(t, filepath.Join(root, "src", "main.ts"), source)
|
|
117
|
+
|
|
118
|
+
var out, errOut bytes.Buffer
|
|
119
|
+
restoreStdout, restoreStderr := stdout, stderr
|
|
120
|
+
stdout, stderr = &out, &errOut
|
|
121
|
+
defer func() { stdout, stderr = restoreStdout, restoreStderr }()
|
|
122
|
+
|
|
123
|
+
if code := run([]string{"--cwd", root, "--tsconfig", "tsconfig.json"}); code != 0 {
|
|
124
|
+
t.Fatalf("graphdump exited %d: %s", code, errOut.String())
|
|
125
|
+
}
|
|
126
|
+
return out.String()
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
func writeGraphdumpFile(t *testing.T, path, content string) {
|
|
130
|
+
t.Helper()
|
|
131
|
+
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
132
|
+
t.Fatal(err)
|
|
133
|
+
}
|
|
134
|
+
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
|
135
|
+
t.Fatal(err)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"strings"
|
|
6
|
+
"testing"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
// TestRunReportsAnUnknownFlagWithoutExitingTheProcess verifies that a bad
|
|
10
|
+
// argument returns an exit code and writes usage to the command's own stream,
|
|
11
|
+
// instead of terminating the process.
|
|
12
|
+
//
|
|
13
|
+
// This is the negative twin of the capability case and the reason the seams
|
|
14
|
+
// exist. The command used to parse flag.CommandLine, whose default error
|
|
15
|
+
// handling is flag.ExitOnError: a malformed argument called os.Exit(2) from
|
|
16
|
+
// inside run, which no test can observe and which would take a test binary down
|
|
17
|
+
// with it. Nothing else about the command is reachable while that is true.
|
|
18
|
+
//
|
|
19
|
+
// 1. Run the command with an unknown flag, capturing both streams.
|
|
20
|
+
// 2. Assert it returns 2 rather than exiting, and that nothing reached stdout.
|
|
21
|
+
// 3. Assert the usage text went to the command's stderr stream.
|
|
22
|
+
// 4. Assert `-h` still succeeds, because asking for usage is not an error.
|
|
23
|
+
func TestRunReportsAnUnknownFlagWithoutExitingTheProcess(t *testing.T) {
|
|
24
|
+
var out, errOut bytes.Buffer
|
|
25
|
+
restoreStdout, restoreStderr := stdout, stderr
|
|
26
|
+
stdout, stderr = &out, &errOut
|
|
27
|
+
defer func() { stdout, stderr = restoreStdout, restoreStderr }()
|
|
28
|
+
|
|
29
|
+
code := run([]string{"--not-a-flag"})
|
|
30
|
+
|
|
31
|
+
if code != 2 {
|
|
32
|
+
t.Fatalf("graphdump exited %d for an unknown flag, want 2", code)
|
|
33
|
+
}
|
|
34
|
+
if out.Len() != 0 {
|
|
35
|
+
t.Fatalf("a rejected invocation wrote %q to stdout; the viewer pipeline parses that stream as JSON", out.String())
|
|
36
|
+
}
|
|
37
|
+
if !strings.Contains(errOut.String(), "not-a-flag") {
|
|
38
|
+
t.Fatalf("stderr does not name the rejected flag: %q", errOut.String())
|
|
39
|
+
}
|
|
40
|
+
if !strings.Contains(errOut.String(), "-tsconfig") {
|
|
41
|
+
t.Fatalf("stderr carried no usage text: %q", errOut.String())
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// The negative twin of the rejection: `flag.ContinueOnError` reports `-h` as
|
|
45
|
+
// an error too, and mapping every parse failure to 2 would turn the help flag
|
|
46
|
+
// into a failed invocation. The global flag set this command used to read
|
|
47
|
+
// exited 0 for it.
|
|
48
|
+
out.Reset()
|
|
49
|
+
errOut.Reset()
|
|
50
|
+
if code := run([]string{"-h"}); code != 0 {
|
|
51
|
+
t.Fatalf("graphdump exited %d for -h, want 0", code)
|
|
52
|
+
}
|
|
53
|
+
if !strings.Contains(errOut.String(), "-tsconfig") {
|
|
54
|
+
t.Fatalf("-h printed no usage text: %q", errOut.String())
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"strings"
|
|
6
|
+
"testing"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
// TestRunReportsAnUnloadableProjectWithoutPrintingADump verifies that a project
|
|
10
|
+
// the command cannot load exits non-zero, prints nothing to stdout, and names
|
|
11
|
+
// what it could not load.
|
|
12
|
+
//
|
|
13
|
+
// The benchmark viewer pipeline runs this command through `execFileSync`, which
|
|
14
|
+
// raises only on a non-zero exit and otherwise parses stdout as JSON. Every
|
|
15
|
+
// failure path here therefore has to be a non-zero exit: turning one into 0
|
|
16
|
+
// hands the pipeline an empty string, and the caller dies inside its JSON parse
|
|
17
|
+
// with an opaque error instead of reading the diagnostic this command already
|
|
18
|
+
// wrote. All three failure paths used to be unpinned, so replacing every
|
|
19
|
+
// `return 1` with `return 0` broke no test.
|
|
20
|
+
//
|
|
21
|
+
// 1. Run the command against a tsconfig that does not exist.
|
|
22
|
+
// 2. Assert it returns 1 and wrote nothing to stdout.
|
|
23
|
+
// 3. Assert stderr names the command and the path it could not load.
|
|
24
|
+
func TestRunReportsAnUnloadableProjectWithoutPrintingADump(t *testing.T) {
|
|
25
|
+
root := t.TempDir()
|
|
26
|
+
|
|
27
|
+
var out, errOut bytes.Buffer
|
|
28
|
+
restoreStdout, restoreStderr := stdout, stderr
|
|
29
|
+
stdout, stderr = &out, &errOut
|
|
30
|
+
defer func() { stdout, stderr = restoreStdout, restoreStderr }()
|
|
31
|
+
|
|
32
|
+
code := run([]string{"--cwd", root, "--tsconfig", "absent.json"})
|
|
33
|
+
|
|
34
|
+
if code != 1 {
|
|
35
|
+
t.Fatalf("graphdump exited %d for an unloadable project, want 1", code)
|
|
36
|
+
}
|
|
37
|
+
if out.Len() != 0 {
|
|
38
|
+
t.Fatalf("a failed run wrote %q to stdout; the viewer pipeline parses that stream as JSON", out.String())
|
|
39
|
+
}
|
|
40
|
+
if !strings.Contains(errOut.String(), "graphdump:") {
|
|
41
|
+
t.Fatalf("stderr does not identify the command: %q", errOut.String())
|
|
42
|
+
}
|
|
43
|
+
if !strings.Contains(errOut.String(), "absent.json") {
|
|
44
|
+
t.Fatalf("stderr does not name the tsconfig it could not load: %q", errOut.String())
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -26,6 +26,13 @@ type apiTransformResult struct {
|
|
|
26
26
|
// content can influence a transformed module, so bundler caches invalidate
|
|
27
27
|
// soundly without per-plugin reporting.
|
|
28
28
|
Graph *driver.TransformGraph `json:"graph,omitempty"`
|
|
29
|
+
// Dependencies and DependenciesComplete are the envelope's dependency side
|
|
30
|
+
// channel. This lane runs no plugin transform at all — each output is the
|
|
31
|
+
// file's own parsed text — so every file is complete with an empty list
|
|
32
|
+
// unless a linked plugin is active and declares otherwise; see
|
|
33
|
+
// driver.Program.TransformDependenciesFor.
|
|
34
|
+
Dependencies map[string][]string `json:"dependencies,omitempty"`
|
|
35
|
+
DependenciesComplete []string `json:"dependenciesComplete,omitempty"`
|
|
29
36
|
// HostInputs are absolute native plugin config files evaluated while this
|
|
30
37
|
// generation loaded. JavaScript hosts merge them with descriptor inputs.
|
|
31
38
|
HostInputs []string `json:"hostInputs,omitempty"`
|
|
@@ -74,6 +81,7 @@ func runAPITransform(args []string) int {
|
|
|
74
81
|
return 2
|
|
75
82
|
}
|
|
76
83
|
typescript := map[string]string{}
|
|
84
|
+
var dependencies driver.TransformDependencies
|
|
77
85
|
var graph *driver.TransformGraph
|
|
78
86
|
var hostInputs []string
|
|
79
87
|
var hostInputHashes map[string]*string
|
|
@@ -89,18 +97,21 @@ func runAPITransform(args []string) int {
|
|
|
89
97
|
typescript[apiOutputKey(cwd, file.FileName())] = file.Text()
|
|
90
98
|
}
|
|
91
99
|
diags = append(diags, prog.Diagnostics()...)
|
|
100
|
+
dependencies = prog.TransformDependenciesFor(cwd)
|
|
92
101
|
hostInputs = prog.PluginHostInputs()
|
|
93
102
|
hostInputHashes = prog.PluginHostInputHashes()
|
|
94
103
|
hostInputRealpaths = prog.PluginHostInputRealpaths()
|
|
95
104
|
}
|
|
96
105
|
|
|
97
106
|
result := apiTransformResult{
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
107
|
+
Dependencies: dependencies.Dependencies,
|
|
108
|
+
DependenciesComplete: dependencies.Complete,
|
|
109
|
+
Diagnostics: make([]apiCompileDiagnostic, 0, len(diags)),
|
|
110
|
+
Graph: graph,
|
|
111
|
+
HostInputs: hostInputs,
|
|
112
|
+
HostInputHashes: hostInputHashes,
|
|
113
|
+
HostInputRealpaths: hostInputRealpaths,
|
|
114
|
+
TypeScript: typescript,
|
|
104
115
|
}
|
|
105
116
|
for _, diag := range diags {
|
|
106
117
|
result.Diagnostics = append(result.Diagnostics, toAPICompileDiagnostic(diag))
|
package/cmd/ttscgraph/dump.go
CHANGED
|
@@ -23,6 +23,11 @@ func runDump(args []string) int {
|
|
|
23
23
|
cwdFlag := fs.String("cwd", "", "project root (defaults to process cwd)")
|
|
24
24
|
tsconfigFlag := fs.String("tsconfig", "tsconfig.json", "project tsconfig path")
|
|
25
25
|
prettyFlag := fs.Bool("pretty", false, "indent the JSON output")
|
|
26
|
+
artifactsFlag := fs.String(
|
|
27
|
+
"artifacts",
|
|
28
|
+
"",
|
|
29
|
+
"path to the artifacts a plugin published (JSON); absent or missing means none",
|
|
30
|
+
)
|
|
26
31
|
if err := fs.Parse(args); err != nil {
|
|
27
32
|
return 2
|
|
28
33
|
}
|
|
@@ -37,7 +42,7 @@ func runDump(args []string) int {
|
|
|
37
42
|
cwd = resolved
|
|
38
43
|
}
|
|
39
44
|
// Resolve the project root the same way LoadProgram does (absolute, then
|
|
40
|
-
// tsgo-normalized) so the
|
|
45
|
+
// tsgo-normalized) so the dump path mapper receives the same canonical root
|
|
41
46
|
// grammar and drive-letter case as the compiler's source paths.
|
|
42
47
|
if abs, err := filepath.Abs(cwd); err == nil {
|
|
43
48
|
cwd = abs
|
|
@@ -57,9 +62,19 @@ func runDump(args []string) int {
|
|
|
57
62
|
defer func() { _ = prog.Close() }()
|
|
58
63
|
|
|
59
64
|
g := graph.Build(prog)
|
|
65
|
+
// The artifacts arrive from a plugin that parsed documents this Program never
|
|
66
|
+
// read, so they are a second producer's facts and the origin says so. Applying
|
|
67
|
+
// them before the dump is what turns a citation of a document section from a
|
|
68
|
+
// token into a relation.
|
|
69
|
+
artifacts, err := graph.LoadArtifacts(strings.TrimSpace(*artifactsFlag))
|
|
70
|
+
if err != nil {
|
|
71
|
+
fmt.Fprintf(stderr, "ttscgraph: could not read the published artifacts: %v\n", err)
|
|
72
|
+
return 1
|
|
73
|
+
}
|
|
74
|
+
graph.ApplyArtifacts(g, artifacts)
|
|
60
75
|
ignored := graph.GitIgnoredFiles(cwd, g)
|
|
61
76
|
texts := graph.SourceTexts(prog)
|
|
62
|
-
origin, err := dumpOrigin(prog, texts)
|
|
77
|
+
origin, err := dumpOrigin(prog, texts, strings.TrimSpace(*artifactsFlag) != "")
|
|
63
78
|
if err != nil {
|
|
64
79
|
fmt.Fprintf(stderr, "ttscgraph: %v\n", err)
|
|
65
80
|
return 1
|
|
@@ -89,7 +104,16 @@ func runDump(args []string) int {
|
|
|
89
104
|
// saw the project, so it carries the same provenance a served snapshot does:
|
|
90
105
|
// without it the file is a pile of facts with no way to tell which program, or
|
|
91
106
|
// which day, they describe.
|
|
92
|
-
|
|
107
|
+
// dumpOrigin builds the provenance for one dump. askedForArtifacts is what
|
|
108
|
+
// turns the artifact capability from a guess into a claim: a producer that was
|
|
109
|
+
// never pointed at a publisher did not look, and saying it did would make a
|
|
110
|
+
// project with no artifacts indistinguishable from one whose artifacts were
|
|
111
|
+
// never requested.
|
|
112
|
+
func dumpOrigin(
|
|
113
|
+
prog *driver.Program,
|
|
114
|
+
texts map[string]string,
|
|
115
|
+
askedForArtifacts bool,
|
|
116
|
+
) (graph.DumpOrigin, error) {
|
|
93
117
|
configs, err := parsedConfigs(prog)
|
|
94
118
|
if err != nil {
|
|
95
119
|
return graph.DumpOrigin{}, err
|
|
@@ -102,15 +126,23 @@ func dumpOrigin(prog *driver.Program, texts map[string]string) (graph.DumpOrigin
|
|
|
102
126
|
if err != nil {
|
|
103
127
|
return graph.DumpOrigin{}, err
|
|
104
128
|
}
|
|
129
|
+
capabilities := fullSnapshotCapabilities
|
|
130
|
+
if askedForArtifacts {
|
|
131
|
+
capabilities = append(append([]string{}, capabilities...), graph.CapabilityArtifactNodes)
|
|
132
|
+
}
|
|
133
|
+
provenance := graph.NewProvenance(
|
|
134
|
+
serveProducer(),
|
|
135
|
+
capabilities,
|
|
136
|
+
fileDigests(configHashes),
|
|
137
|
+
rootFileEntries(projectRootFilesFromConfigs(configs, false)),
|
|
138
|
+
texts,
|
|
139
|
+
diskDigests,
|
|
140
|
+
)
|
|
141
|
+
if askedForArtifacts {
|
|
142
|
+
provenance.ArtifactProducer = &graph.Producer{Tool: "@ttsc/lint graph-nodes"}
|
|
143
|
+
}
|
|
105
144
|
return graph.DumpOrigin{
|
|
106
|
-
Provenance:
|
|
107
|
-
serveProducer(),
|
|
108
|
-
fullSnapshotCapabilities,
|
|
109
|
-
fileDigests(configHashes),
|
|
110
|
-
rootFileEntries(projectRootFilesFromConfigs(configs, false)),
|
|
111
|
-
texts,
|
|
112
|
-
diskDigests,
|
|
113
|
-
),
|
|
145
|
+
Provenance: provenance,
|
|
114
146
|
Diagnostics: graph.NewDiagnostics(prog),
|
|
115
147
|
}, nil
|
|
116
148
|
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"os"
|
|
7
|
+
"path/filepath"
|
|
8
|
+
"slices"
|
|
9
|
+
"strings"
|
|
10
|
+
"testing"
|
|
11
|
+
|
|
12
|
+
"github.com/samchon/ttsc/packages/ttsc/internal/graph"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
// TestRunDumpClaimsArtifactsOnlyWhenItAsked verifies that the artifact
|
|
16
|
+
// capability is a statement about what this producer did, not about what it
|
|
17
|
+
// found.
|
|
18
|
+
//
|
|
19
|
+
// The two states a consumer must distinguish are "this project publishes no
|
|
20
|
+
// artifacts" and "this producer was never pointed at a publisher". Both are the
|
|
21
|
+
// same absent nodes, so only the claim separates them — and a producer that
|
|
22
|
+
// claimed unconditionally would make a project with no lint install look like a
|
|
23
|
+
// project whose documents cite nothing. The second producer is named for the
|
|
24
|
+
// same reason: these facts did not come from this Program, and the dump's
|
|
25
|
+
// one-generation contract stays honest by saying so rather than by hiding it.
|
|
26
|
+
//
|
|
27
|
+
// 1. Dump a project with no `--artifacts`, and assert neither the claim nor the
|
|
28
|
+
// second producer appears, and that the citation stays a token.
|
|
29
|
+
// 2. Dump the same project with a published set, and assert both appear and the
|
|
30
|
+
// citation resolved to a node carrying its heading and its line.
|
|
31
|
+
func TestRunDumpClaimsArtifactsOnlyWhenItAsked(t *testing.T) {
|
|
32
|
+
root := t.TempDir()
|
|
33
|
+
writeGraphFile(t, filepath.Join(root, "tsconfig.json"), `{
|
|
34
|
+
"compilerOptions": { "target": "ES2022", "module": "commonjs", "strict": true },
|
|
35
|
+
"files": ["src/main.ts"]
|
|
36
|
+
}
|
|
37
|
+
`)
|
|
38
|
+
writeGraphFile(t, filepath.Join(root, "src", "main.ts"), `/** @evidence docs/sale.md#pricing States the rule. */
|
|
39
|
+
export function priced(): void {}
|
|
40
|
+
`)
|
|
41
|
+
|
|
42
|
+
plain := decodeArtifactDump(t, runDumpForArtifacts(t, root, ""))
|
|
43
|
+
if slices.Contains(plain.Provenance.Capabilities, graph.CapabilityArtifactNodes) {
|
|
44
|
+
t.Fatalf("a dump that was never pointed at a publisher claimed %q", graph.CapabilityArtifactNodes)
|
|
45
|
+
}
|
|
46
|
+
if plain.Provenance.ArtifactProducer != nil {
|
|
47
|
+
t.Fatal("a dump with no artifacts named a second producer")
|
|
48
|
+
}
|
|
49
|
+
for _, node := range plain.Nodes {
|
|
50
|
+
if node.ID == "docs/sale.md#pricing" {
|
|
51
|
+
t.Fatal("an unrequested dump fabricated an artifact node")
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
published := filepath.Join(root, "artifacts.json")
|
|
56
|
+
if err := os.WriteFile(published, []byte(`[
|
|
57
|
+
{"address":"docs/sale.md","kind":"markdown_document","readable":"Sale","file":"docs/sale.md","line":1},
|
|
58
|
+
{"address":"docs/sale.md#pricing","kind":"markdown_section","readable":"Pricing","parent":"docs/sale.md","file":"docs/sale.md","line":7}
|
|
59
|
+
]`), 0o644); err != nil {
|
|
60
|
+
t.Fatal(err)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
indexed := decodeArtifactDump(t, runDumpForArtifacts(t, root, published))
|
|
64
|
+
if !slices.Contains(indexed.Provenance.Capabilities, graph.CapabilityArtifactNodes) {
|
|
65
|
+
t.Fatalf("a dump that asked declares %v, missing %q",
|
|
66
|
+
indexed.Provenance.Capabilities, graph.CapabilityArtifactNodes)
|
|
67
|
+
}
|
|
68
|
+
if indexed.Provenance.ArtifactProducer == nil {
|
|
69
|
+
t.Fatal("the artifact nodes came from another Program and the dump did not say so")
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
var section *graph.DumpNode
|
|
73
|
+
for index := range indexed.Nodes {
|
|
74
|
+
if indexed.Nodes[index].ID == "docs/sale.md#pricing" {
|
|
75
|
+
section = &indexed.Nodes[index]
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if section == nil {
|
|
79
|
+
t.Fatal("the cited section is not a node")
|
|
80
|
+
}
|
|
81
|
+
if section.Name != "Pricing" {
|
|
82
|
+
t.Fatalf("the section is named %q, not the heading text an index exists to hold", section.Name)
|
|
83
|
+
}
|
|
84
|
+
if section.Parent != "docs/sale.md" {
|
|
85
|
+
t.Fatalf("the section's parent is %q, want its document", section.Parent)
|
|
86
|
+
}
|
|
87
|
+
if section.Evidence == nil || section.Evidence.StartLine != 7 {
|
|
88
|
+
t.Fatalf("the section carries no span at its heading: %+v", section.Evidence)
|
|
89
|
+
}
|
|
90
|
+
cited := false
|
|
91
|
+
for _, edge := range indexed.Edges {
|
|
92
|
+
if edge.Kind == "doc_ref" && edge.To == "docs/sale.md#pricing" {
|
|
93
|
+
cited = true
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if !cited {
|
|
97
|
+
t.Fatal("the citation stayed a token; it has to resolve to the node now that one exists")
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// runDumpForArtifacts runs the dump subcommand and returns its stdout.
|
|
102
|
+
func runDumpForArtifacts(t *testing.T, root, artifacts string) string {
|
|
103
|
+
t.Helper()
|
|
104
|
+
args := []string{"dump", "--cwd", root, "--tsconfig", "tsconfig.json"}
|
|
105
|
+
if artifacts != "" {
|
|
106
|
+
args = append(args, "--artifacts", artifacts)
|
|
107
|
+
}
|
|
108
|
+
var out, errOut bytes.Buffer
|
|
109
|
+
restoreStdout, restoreStderr := stdout, stderr
|
|
110
|
+
stdout, stderr = &out, &errOut
|
|
111
|
+
defer func() { stdout, stderr = restoreStdout, restoreStderr }()
|
|
112
|
+
|
|
113
|
+
if code := run(args); code != 0 {
|
|
114
|
+
t.Fatalf("dump exited %d: %s", code, errOut.String())
|
|
115
|
+
}
|
|
116
|
+
return out.String()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
type artifactDump struct {
|
|
120
|
+
Provenance struct {
|
|
121
|
+
Capabilities []string `json:"capabilities"`
|
|
122
|
+
ArtifactProducer *graph.Producer `json:"artifactProducer"`
|
|
123
|
+
} `json:"provenance"`
|
|
124
|
+
Nodes []graph.DumpNode `json:"nodes"`
|
|
125
|
+
Edges []graph.DumpEdge `json:"edges"`
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
func decodeArtifactDump(t *testing.T, raw string) artifactDump {
|
|
129
|
+
t.Helper()
|
|
130
|
+
var parsed artifactDump
|
|
131
|
+
if err := json.Unmarshal([]byte(strings.TrimSpace(raw)), &parsed); err != nil {
|
|
132
|
+
t.Fatalf("dump is not JSON: %v", err)
|
|
133
|
+
}
|
|
134
|
+
return parsed
|
|
135
|
+
}
|