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
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
package driver
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"reflect"
|
|
5
|
+
"testing"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
// TestTransformDependenciesRequiresEveryContributorToDeclare verifies the
|
|
9
|
+
// aggregation rule over a composed plugin set: a file is complete only when
|
|
10
|
+
// every contributing plugin declared it, because a consumer cannot attribute
|
|
11
|
+
// one plugin's reported inputs back to it.
|
|
12
|
+
//
|
|
13
|
+
// It also pins that reporting and declaring are separate acts: a plugin's
|
|
14
|
+
// dependency list widens what consumers invalidate on even while the file
|
|
15
|
+
// itself stays unlisted (samchon/ttsc#1263).
|
|
16
|
+
func TestTransformDependenciesRequiresEveryContributorToDeclare(t *testing.T) {
|
|
17
|
+
declarations := newPluginFileDeclarations()
|
|
18
|
+
first := declarations.forPlugin(0)
|
|
19
|
+
first.addDependency("src/main.ts", "src/consulted.d.ts")
|
|
20
|
+
first.addComplete("src/main.ts")
|
|
21
|
+
keys := []string{"src/main.ts"}
|
|
22
|
+
|
|
23
|
+
partial := aggregateTransformDependencies(keys, []int{0, 1}, declarations)
|
|
24
|
+
|
|
25
|
+
if len(partial.Complete) != 0 {
|
|
26
|
+
t.Fatalf("expected the silent contributor to block the claim, got %v", partial.Complete)
|
|
27
|
+
}
|
|
28
|
+
if !reflect.DeepEqual(partial.Dependencies["src/main.ts"], []string{"src/consulted.d.ts"}) {
|
|
29
|
+
t.Fatalf("expected the reported dependency to survive, got %v", partial.Dependencies)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
declarations.forPlugin(1).completeEveryFile()
|
|
33
|
+
full := aggregateTransformDependencies(keys, []int{0, 1}, declarations)
|
|
34
|
+
|
|
35
|
+
if !reflect.DeepEqual(full.Complete, []string{"src/main.ts"}) {
|
|
36
|
+
t.Fatalf("expected both contributors to complete the claim, got %v", full.Complete)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
package driver
|
|
2
|
+
|
|
3
|
+
import "testing"
|
|
4
|
+
|
|
5
|
+
// TestTransformDependenciesStaysSilentForAnUndeclaredContributor verifies that
|
|
6
|
+
// a plugin able to change transform output blocks every completeness claim
|
|
7
|
+
// until it makes one itself.
|
|
8
|
+
//
|
|
9
|
+
// This is the compatibility half of the contract: every producer written before
|
|
10
|
+
// the declaration existed keeps the host-owned bound exactly, and a plugin whose
|
|
11
|
+
// output depends on the type graph cannot have that bound narrowed on its
|
|
12
|
+
// behalf (samchon/ttsc#1263).
|
|
13
|
+
func TestTransformDependenciesStaysSilentForAnUndeclaredContributor(t *testing.T) {
|
|
14
|
+
out := aggregateTransformDependencies(
|
|
15
|
+
[]string{"src/main.ts"},
|
|
16
|
+
[]int{0},
|
|
17
|
+
newPluginFileDeclarations(),
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
if len(out.Complete) != 0 {
|
|
21
|
+
t.Fatalf("expected no completeness declaration, got %v", out.Complete)
|
|
22
|
+
}
|
|
23
|
+
}
|
package/driver/transform_dependencies_withdraws_completeness_after_an_unusable_dependency_test.go
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
package driver
|
|
2
|
+
|
|
3
|
+
import "testing"
|
|
4
|
+
|
|
5
|
+
// TestTransformDependenciesWithdrawsCompletenessAfterAnUnusableDependency
|
|
6
|
+
// verifies a dependency the host could not key withdraws the claim it belonged
|
|
7
|
+
// to, instead of leaving a complete list with a member missing from it.
|
|
8
|
+
//
|
|
9
|
+
// A plugin whose own lookup answered nothing for one input is the case: it
|
|
10
|
+
// reports what it has and declares the file complete, and a host that dropped
|
|
11
|
+
// the unusable member would publish exactly the under-declaration the protocol
|
|
12
|
+
// blames on the plugin — manufactured by the host. Only the affected file loses
|
|
13
|
+
// the claim; the plugin's other files keep theirs.
|
|
14
|
+
func TestTransformDependenciesWithdrawsCompletenessAfterAnUnusableDependency(t *testing.T) {
|
|
15
|
+
cwd := t.TempDir()
|
|
16
|
+
declarations := newPluginFileDeclarations()
|
|
17
|
+
record := declarations.forPlugin(0)
|
|
18
|
+
ctx := PluginContext{
|
|
19
|
+
Cwd: cwd,
|
|
20
|
+
reportFileDependency: record.addDependency,
|
|
21
|
+
reportFileDependencyRejected: record.rejectDependency,
|
|
22
|
+
reportEveryFileComplete: record.completeEveryFile,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
ctx.ReportDependenciesComplete()
|
|
26
|
+
ctx.ReportFileDependency("src/main.ts", "")
|
|
27
|
+
|
|
28
|
+
out := aggregateTransformDependencies(
|
|
29
|
+
[]string{"src/main.ts", "src/other.ts"},
|
|
30
|
+
[]int{0},
|
|
31
|
+
declarations,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
if len(out.Complete) != 1 || out.Complete[0] != "src/other.ts" {
|
|
35
|
+
t.Fatalf("expected only the unaffected file to stay complete, got %v", out.Complete)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
package graph
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"os"
|
|
6
|
+
"sort"
|
|
7
|
+
"strings"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// Artifact is one thing a declaration's documentation can cite that is not a
|
|
11
|
+
// TypeScript declaration — a document section, a data model field, an API
|
|
12
|
+
// operation.
|
|
13
|
+
//
|
|
14
|
+
// It is the wire shape `@ttsc/lint`'s `graph-nodes` verb prints, restated here
|
|
15
|
+
// rather than imported: `packages/lint` is its own Go module and deliberately
|
|
16
|
+
// carries no requirement on this one, with its reason written in that manifest.
|
|
17
|
+
// What binds the two is the JSON contract, so the keys below are the contract
|
|
18
|
+
// and the Go field names are not — the same rule DumpNode already states.
|
|
19
|
+
//
|
|
20
|
+
// Nothing here is a judgement. The rule that materialized these decides what is
|
|
21
|
+
// covered, what is a legitimate exclusion, and what is missing, and it delivers
|
|
22
|
+
// those as compile errors. The graph reports; the linter judges.
|
|
23
|
+
type Artifact struct {
|
|
24
|
+
Address string `json:"address"`
|
|
25
|
+
Kind string `json:"kind"`
|
|
26
|
+
Readable string `json:"readable,omitempty"`
|
|
27
|
+
Parent string `json:"parent,omitempty"`
|
|
28
|
+
File string `json:"file,omitempty"`
|
|
29
|
+
Line int `json:"line,omitempty"`
|
|
30
|
+
Aliases []string `json:"aliases,omitempty"`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// artifactNodeKinds maps a published artifact kind onto this graph's node kind.
|
|
34
|
+
//
|
|
35
|
+
// The vocabulary is closed on purpose. A kind the graph does not model is a node
|
|
36
|
+
// a consumer would rank, contain, and display as something it is not, so it is
|
|
37
|
+
// dropped rather than guessed at — and the capability claim is what tells a
|
|
38
|
+
// consumer the producer looked at all.
|
|
39
|
+
var artifactNodeKinds = map[string]NodeKind{
|
|
40
|
+
"markdown_document": NodeMarkdownDocument,
|
|
41
|
+
"markdown_section": NodeMarkdownSection,
|
|
42
|
+
"prisma_model": NodePrismaModel,
|
|
43
|
+
"prisma_column": NodePrismaColumn,
|
|
44
|
+
"prisma_relation": NodePrismaRelation,
|
|
45
|
+
"swagger_operation": NodeSwaggerOperation,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// IsArtifactKind reports whether a node kind names a published artifact rather
|
|
49
|
+
// than a TypeScript declaration.
|
|
50
|
+
//
|
|
51
|
+
// It is what keeps an artifact out of the places that assume a declaration: an
|
|
52
|
+
// id built from the `path#name:kind` grammar, and a tour seed. A document
|
|
53
|
+
// section is neither the shape of a declaration nor an answer to "what is this
|
|
54
|
+
// project and how does it run".
|
|
55
|
+
func IsArtifactKind(kind NodeKind) bool {
|
|
56
|
+
for _, mapped := range artifactNodeKinds {
|
|
57
|
+
if mapped == kind {
|
|
58
|
+
return true
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return false
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// LoadArtifacts reads the artifact set a plugin published, or nil when no path
|
|
65
|
+
// was given.
|
|
66
|
+
//
|
|
67
|
+
// A missing file is not an error: the caller names a path a plugin may or may
|
|
68
|
+
// not have produced, and a project that publishes nothing is the common case. A
|
|
69
|
+
// file that exists and does not parse is an error, because that is a producer
|
|
70
|
+
// the caller was told to expect.
|
|
71
|
+
func LoadArtifacts(path string) ([]Artifact, error) {
|
|
72
|
+
if strings.TrimSpace(path) == "" {
|
|
73
|
+
return nil, nil
|
|
74
|
+
}
|
|
75
|
+
data, err := os.ReadFile(path)
|
|
76
|
+
if err != nil {
|
|
77
|
+
if os.IsNotExist(err) {
|
|
78
|
+
return nil, nil
|
|
79
|
+
}
|
|
80
|
+
return nil, err
|
|
81
|
+
}
|
|
82
|
+
return ParseArtifacts(data)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ParseArtifacts decodes a published set from the bytes it was read as.
|
|
86
|
+
//
|
|
87
|
+
// Split from LoadArtifacts for a caller that has to hash the same bytes it
|
|
88
|
+
// parses. Reading the file twice — once to state what it holds and once to
|
|
89
|
+
// decode it — lets an overwrite land between the two, leaving a session whose
|
|
90
|
+
// recorded identity describes a set it is not holding.
|
|
91
|
+
func ParseArtifacts(data []byte) ([]Artifact, error) {
|
|
92
|
+
var artifacts []Artifact
|
|
93
|
+
if err := json.Unmarshal(data, &artifacts); err != nil {
|
|
94
|
+
return nil, err
|
|
95
|
+
}
|
|
96
|
+
return artifacts, nil
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ApplyArtifacts adds the published artifacts to a built graph as nodes, records
|
|
100
|
+
// each one's parent, and resolves every documentation tag whose target names one
|
|
101
|
+
// into a doc-ref edge.
|
|
102
|
+
//
|
|
103
|
+
// The address is the node id verbatim. That is deliberate and has a real
|
|
104
|
+
// benefit: the token an author wrote in a tag is the handle an agent submits
|
|
105
|
+
// back. It also means these ids do not follow the `path#name:kind` grammar —
|
|
106
|
+
// `prisma:Sale.price` carries no path because a Prisma model name is unique
|
|
107
|
+
// across the schema folder, and `POST:/orders` has no file at all — which is why
|
|
108
|
+
// a consumer parsing an id has to gate on the kind first.
|
|
109
|
+
//
|
|
110
|
+
// An address that collides with an existing node loses: a checker-resolved
|
|
111
|
+
// declaration is a fact of this Program, and a published artifact is not.
|
|
112
|
+
func ApplyArtifacts(g *Graph, artifacts []Artifact) {
|
|
113
|
+
if g == nil || len(artifacts) == 0 {
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
// Sorted so the emitted node and edge order is a function of the addresses
|
|
117
|
+
// rather than of the order a plugin happened to walk its documents.
|
|
118
|
+
sorted := make([]Artifact, len(artifacts))
|
|
119
|
+
copy(sorted, artifacts)
|
|
120
|
+
sort.SliceStable(sorted, func(i, j int) bool { return sorted[i].Address < sorted[j].Address })
|
|
121
|
+
|
|
122
|
+
// Every address an artifact answers to, including its aliases, so a citation
|
|
123
|
+
// written against either spelling resolves to the one node.
|
|
124
|
+
byAddress := map[string]string{}
|
|
125
|
+
for _, artifact := range sorted {
|
|
126
|
+
kind, known := artifactNodeKinds[artifact.Kind]
|
|
127
|
+
if !known || artifact.Address == "" {
|
|
128
|
+
continue
|
|
129
|
+
}
|
|
130
|
+
if _, exists := g.Nodes[artifact.Address]; exists {
|
|
131
|
+
continue
|
|
132
|
+
}
|
|
133
|
+
node := &Node{
|
|
134
|
+
ID: artifact.Address,
|
|
135
|
+
Name: artifact.Address,
|
|
136
|
+
Simple: artifact.Readable,
|
|
137
|
+
Kind: kind,
|
|
138
|
+
File: artifact.File,
|
|
139
|
+
}
|
|
140
|
+
if artifact.Line > 0 {
|
|
141
|
+
node.ArtifactLine = artifact.Line
|
|
142
|
+
}
|
|
143
|
+
g.Nodes[artifact.Address] = node
|
|
144
|
+
byAddress[artifact.Address] = artifact.Address
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Aliases are registered after every address, and only where nothing claims
|
|
148
|
+
// the spelling. An alias is an additional name for one artifact, never a
|
|
149
|
+
// rename of another: registering it in the same pass let one artifact's alias
|
|
150
|
+
// overwrite an earlier artifact's own address, and a citation of that address
|
|
151
|
+
// then resolved to the wrong node — a confident wrong answer, which is worse
|
|
152
|
+
// than the token it replaced.
|
|
153
|
+
for _, artifact := range sorted {
|
|
154
|
+
if _, published := g.Nodes[artifact.Address]; !published {
|
|
155
|
+
continue
|
|
156
|
+
}
|
|
157
|
+
for _, alias := range artifact.Aliases {
|
|
158
|
+
if alias == "" || alias == artifact.Address {
|
|
159
|
+
continue
|
|
160
|
+
}
|
|
161
|
+
if _, claimed := byAddress[alias]; claimed {
|
|
162
|
+
continue
|
|
163
|
+
}
|
|
164
|
+
byAddress[alias] = artifact.Address
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Containment rides the node rather than an edge, because `contains` is
|
|
169
|
+
// synthesized by the TypeScript memory layer for declarations and a second
|
|
170
|
+
// producer of the same relation would put two answers in the graph. A parent
|
|
171
|
+
// naming nothing published is cleared: the child is still a real artifact and
|
|
172
|
+
// sits at the top of its chain, and fabricating the parent is what must not
|
|
173
|
+
// happen.
|
|
174
|
+
for _, artifact := range sorted {
|
|
175
|
+
node, exists := g.Nodes[artifact.Address]
|
|
176
|
+
if !exists || artifact.Parent == "" {
|
|
177
|
+
continue
|
|
178
|
+
}
|
|
179
|
+
if parent, resolved := byAddress[artifact.Parent]; resolved && parent != artifact.Address {
|
|
180
|
+
node.ArtifactParent = parent
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// A tag's target is its leading token, the same rule the reverse index uses.
|
|
185
|
+
// A tag naming nothing published resolves to nothing and stays what it always
|
|
186
|
+
// was: text on the node, for the linter to judge.
|
|
187
|
+
//
|
|
188
|
+
// The edges are appended with a dedup of their own rather than through
|
|
189
|
+
// addEdge. This runs after the build, and the build releases the scratch it no
|
|
190
|
+
// longer reads — `seen` among it — so the shared helper would write to a map
|
|
191
|
+
// that is deliberately nil. Two declarations citing one address are two edges;
|
|
192
|
+
// one declaration citing it twice, under two tag names, is one.
|
|
193
|
+
seen := map[edgeKey]struct{}{}
|
|
194
|
+
for _, edge := range g.Edges {
|
|
195
|
+
seen[edgeKey{from: edge.From, to: edge.To, kind: wireEdgeKind(edge.Kind, edge.Origin)}] = struct{}{}
|
|
196
|
+
}
|
|
197
|
+
for _, tag := range g.DocTags {
|
|
198
|
+
if tag == nil {
|
|
199
|
+
continue
|
|
200
|
+
}
|
|
201
|
+
target := leadingToken(tag.Text)
|
|
202
|
+
if target == "" {
|
|
203
|
+
continue
|
|
204
|
+
}
|
|
205
|
+
address, resolved := byAddress[target]
|
|
206
|
+
if !resolved {
|
|
207
|
+
continue
|
|
208
|
+
}
|
|
209
|
+
key := edgeKey{from: tag.Target, to: address, kind: wireEdgeKind(EdgeDocRef, "")}
|
|
210
|
+
if _, exists := seen[key]; exists {
|
|
211
|
+
continue
|
|
212
|
+
}
|
|
213
|
+
seen[key] = struct{}{}
|
|
214
|
+
g.Edges = append(g.Edges, &Edge{From: tag.Target, To: address, Kind: EdgeDocRef, Pos: -1, End: -1})
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// leadingToken is the first whitespace-delimited word of a tag's text, which is
|
|
219
|
+
// the address a citation names. Everything after it is prose the author wrote
|
|
220
|
+
// for a human.
|
|
221
|
+
func leadingToken(text string) string {
|
|
222
|
+
trimmed := strings.TrimSpace(text)
|
|
223
|
+
if trimmed == "" {
|
|
224
|
+
return ""
|
|
225
|
+
}
|
|
226
|
+
if space := strings.IndexAny(trimmed, " \t"); space >= 0 {
|
|
227
|
+
return trimmed[:space]
|
|
228
|
+
}
|
|
229
|
+
return trimmed
|
|
230
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
package graph
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"path/filepath"
|
|
5
|
+
"testing"
|
|
6
|
+
|
|
7
|
+
"github.com/samchon/ttsc/packages/ttsc/driver"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// TestArtifactsIndexWhatAPluginPublished verifies that a published artifact
|
|
11
|
+
// becomes a node a citation resolves to, and that nothing else does.
|
|
12
|
+
//
|
|
13
|
+
// The reverse question already worked without this: a lookup on an address
|
|
14
|
+
// answered with the declarations citing it, keyed on the token. What it could
|
|
15
|
+
// not answer is what the address names — the heading's own text, what sits under
|
|
16
|
+
// it — and that is what a node adds. The edge is the same `doc-ref` a resolved
|
|
17
|
+
// `{@link}` already produced; the difference is that it now has an other end.
|
|
18
|
+
//
|
|
19
|
+
// Every negative here is a way the index could lie. A fabricated node for an
|
|
20
|
+
// address nobody published would answer a question with an invention; a parent
|
|
21
|
+
// naming nothing would put an artifact under a container that does not exist;
|
|
22
|
+
// and an unknown kind would be ranked and contained as something it is not.
|
|
23
|
+
//
|
|
24
|
+
// 1. Build a graph for a project whose declarations cite four addresses.
|
|
25
|
+
// 2. Apply a published set covering three of them, one by an alias, one under
|
|
26
|
+
// a parent, plus an unpublishable entry.
|
|
27
|
+
// 3. Assert the nodes, their containment, and the resolved edges — and that the
|
|
28
|
+
// uncited address, the unknown kind, and the dangling parent produced none.
|
|
29
|
+
func TestArtifactsIndexWhatAPluginPublished(t *testing.T) {
|
|
30
|
+
root := t.TempDir()
|
|
31
|
+
writeFile(t, filepath.Join(root, "tsconfig.json"), `{
|
|
32
|
+
"compilerOptions": { "target": "ES2022", "module": "commonjs", "strict": true },
|
|
33
|
+
"files": ["src/main.ts"]
|
|
34
|
+
}
|
|
35
|
+
`)
|
|
36
|
+
writeFile(t, filepath.Join(root, "src", "main.ts"), `/** @evidence docs/sale.md#pricing States the rule. */
|
|
37
|
+
export function priced(): void {}
|
|
38
|
+
|
|
39
|
+
/** @evidence prisma:Sale.price The column it reads. */
|
|
40
|
+
export function column(): void {}
|
|
41
|
+
|
|
42
|
+
/** @evidence docs/sale.md#alias Reached by its other name. */
|
|
43
|
+
export function aliased(): void {}
|
|
44
|
+
|
|
45
|
+
/** @evidence docs/sale.md#nobody-published-this Still just text. */
|
|
46
|
+
export function unpublished(): void {}
|
|
47
|
+
`)
|
|
48
|
+
|
|
49
|
+
prog, _, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
|
|
50
|
+
if err != nil || prog == nil {
|
|
51
|
+
t.Fatalf("could not load the probe project: %v", err)
|
|
52
|
+
}
|
|
53
|
+
defer func() { _ = prog.Close() }()
|
|
54
|
+
|
|
55
|
+
g := Build(prog)
|
|
56
|
+
before := len(g.Nodes)
|
|
57
|
+
ApplyArtifacts(g, []Artifact{
|
|
58
|
+
{
|
|
59
|
+
Address: "docs/sale.md",
|
|
60
|
+
Kind: "markdown_document",
|
|
61
|
+
Readable: "Sale",
|
|
62
|
+
File: "docs/sale.md",
|
|
63
|
+
Line: 1,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
Address: "docs/sale.md#pricing",
|
|
67
|
+
Kind: "markdown_section",
|
|
68
|
+
Readable: "Pricing",
|
|
69
|
+
Parent: "docs/sale.md",
|
|
70
|
+
File: "docs/sale.md",
|
|
71
|
+
Line: 7,
|
|
72
|
+
Aliases: []string{"docs/sale.md#alias"},
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
Address: "prisma:Sale.price",
|
|
76
|
+
Kind: "prisma_column",
|
|
77
|
+
Readable: "Prisma column 'Sale.price'",
|
|
78
|
+
Parent: "prisma:Sale",
|
|
79
|
+
File: "prisma/schema.prisma",
|
|
80
|
+
Line: 12,
|
|
81
|
+
},
|
|
82
|
+
{Address: "docs/sale.md#unknown", Kind: "not_a_kind"},
|
|
83
|
+
// Its alias is another artifact's own address. An alias is an additional
|
|
84
|
+
// name for one artifact, never a rename of another, so this must not move
|
|
85
|
+
// `docs/sale.md#pricing` onto this node — a citation of that address would
|
|
86
|
+
// then resolve to the wrong section, which is a confident wrong answer.
|
|
87
|
+
{
|
|
88
|
+
Address: "docs/sale.md#shadow",
|
|
89
|
+
Kind: "markdown_section",
|
|
90
|
+
Readable: "Shadow",
|
|
91
|
+
Aliases: []string{"docs/sale.md#pricing"},
|
|
92
|
+
File: "docs/sale.md",
|
|
93
|
+
Line: 20,
|
|
94
|
+
},
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
if got := len(g.Nodes) - before; got != 4 {
|
|
98
|
+
t.Fatalf("added %d nodes, want 4; an unknown kind must contribute none", got)
|
|
99
|
+
}
|
|
100
|
+
if _, fabricated := g.Nodes["docs/sale.md#nobody-published-this"]; fabricated {
|
|
101
|
+
t.Fatal("an address nobody published became a node; the tag text carries it and the linter judges it")
|
|
102
|
+
}
|
|
103
|
+
if _, fabricated := g.Nodes["prisma:Sale"]; fabricated {
|
|
104
|
+
t.Fatal("a parent nobody published became a node")
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
section := g.Nodes["docs/sale.md#pricing"]
|
|
108
|
+
if section == nil {
|
|
109
|
+
t.Fatal("the cited section is not a node")
|
|
110
|
+
}
|
|
111
|
+
if section.Simple != "Pricing" {
|
|
112
|
+
t.Fatalf("the section carries %q, not the heading text an index exists to hold", section.Simple)
|
|
113
|
+
}
|
|
114
|
+
if section.ArtifactLine != 7 {
|
|
115
|
+
t.Fatalf("the section starts at line %d, want 7", section.ArtifactLine)
|
|
116
|
+
}
|
|
117
|
+
if section.ArtifactParent != "docs/sale.md" {
|
|
118
|
+
t.Fatalf("the section's parent is %q, want its document", section.ArtifactParent)
|
|
119
|
+
}
|
|
120
|
+
if parent := g.Nodes["prisma:Sale.price"].ArtifactParent; parent != "" {
|
|
121
|
+
t.Fatalf("a parent naming no published node survived as %q; it must be cleared, not fabricated", parent)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
cited := map[string]bool{}
|
|
125
|
+
for _, edge := range g.Edges {
|
|
126
|
+
if edge.Kind == EdgeDocRef {
|
|
127
|
+
cited[edge.To] = true
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
for _, address := range []string{"docs/sale.md#pricing", "prisma:Sale.price"} {
|
|
131
|
+
if !cited[address] {
|
|
132
|
+
t.Fatalf("no doc-ref edge resolved to %q", address)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if cited["docs/sale.md#alias"] {
|
|
136
|
+
t.Fatal("an alias became its own edge target; it has to resolve to the one node")
|
|
137
|
+
}
|
|
138
|
+
// The alias resolves to the same node, so the declaration citing it and the
|
|
139
|
+
// declaration citing the canonical address point at one artifact.
|
|
140
|
+
aliasEdges := 0
|
|
141
|
+
for _, edge := range g.Edges {
|
|
142
|
+
if edge.Kind == EdgeDocRef && edge.To == "docs/sale.md#pricing" {
|
|
143
|
+
aliasEdges++
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if aliasEdges != 2 {
|
|
147
|
+
t.Fatalf("%d declarations resolved to the section, want 2 (its address and its alias)", aliasEdges)
|
|
148
|
+
}
|
|
149
|
+
for _, edge := range g.Edges {
|
|
150
|
+
if edge.Kind == EdgeDocRef && edge.To == "docs/sale.md#shadow" {
|
|
151
|
+
t.Fatal("an alias claimed another artifact's address; a citation resolved to the wrong node")
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
package graph
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"path/filepath"
|
|
5
|
+
"testing"
|
|
6
|
+
|
|
7
|
+
"github.com/samchon/ttsc/packages/ttsc/driver"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// TestArtifactsSurviveAPartialRebuild verifies that a citation of an artifact is
|
|
11
|
+
// still a relation after the incremental path rebuilds the file that wrote it.
|
|
12
|
+
//
|
|
13
|
+
// A partial build re-resolves the outgoing facts of the files it selected, and a
|
|
14
|
+
// citation is one of them. The full snapshot applied the published artifacts and
|
|
15
|
+
// the incremental one did not, so editing a declaration that cites a document
|
|
16
|
+
// section rebuilt it with that edge missing — the client's graph silently lost a
|
|
17
|
+
// relation it had a moment earlier, and only a full reload brought it back.
|
|
18
|
+
//
|
|
19
|
+
// The nodes are re-added rather than assumed present because a partial starts
|
|
20
|
+
// with only its selected files' nodes. They are the same nodes under the same
|
|
21
|
+
// ids, which is why the store can replace like with like.
|
|
22
|
+
//
|
|
23
|
+
// 1. Build the complete graph with artifacts applied, and confirm the edge.
|
|
24
|
+
// 2. Rebuild only the citing file, as the shard path does.
|
|
25
|
+
// 3. Assert the edge is there again, and that the artifact came with it.
|
|
26
|
+
func TestArtifactsSurviveAPartialRebuild(t *testing.T) {
|
|
27
|
+
root := t.TempDir()
|
|
28
|
+
writeFile(t, filepath.Join(root, "tsconfig.json"), `{
|
|
29
|
+
"compilerOptions": { "target": "ES2022", "module": "commonjs", "strict": true },
|
|
30
|
+
"files": ["src/main.ts"]
|
|
31
|
+
}
|
|
32
|
+
`)
|
|
33
|
+
source := filepath.Join(root, "src", "main.ts")
|
|
34
|
+
writeFile(t, source, `/** @evidence docs/sale.md#pricing States the rule. */
|
|
35
|
+
export function priced(): void {}
|
|
36
|
+
`)
|
|
37
|
+
|
|
38
|
+
prog, _, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
|
|
39
|
+
if err != nil || prog == nil {
|
|
40
|
+
t.Fatalf("could not load the probe project: %v", err)
|
|
41
|
+
}
|
|
42
|
+
defer func() { _ = prog.Close() }()
|
|
43
|
+
|
|
44
|
+
published := []Artifact{
|
|
45
|
+
{
|
|
46
|
+
Address: "docs/sale.md#pricing",
|
|
47
|
+
Kind: "markdown_section",
|
|
48
|
+
Readable: "Pricing",
|
|
49
|
+
File: "docs/sale.md",
|
|
50
|
+
Line: 7,
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
complete := Build(prog)
|
|
55
|
+
ApplyArtifacts(complete, published)
|
|
56
|
+
if !citesArtifact(complete) {
|
|
57
|
+
t.Fatal("the complete build did not resolve the citation")
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
var selected string
|
|
61
|
+
for _, node := range complete.Nodes {
|
|
62
|
+
if node.File != "" && node.Kind == NodeFunction {
|
|
63
|
+
selected = node.File
|
|
64
|
+
break
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if selected == "" {
|
|
68
|
+
t.Fatal("the probe project produced no declaration to reselect")
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
partial := BuildFiles(prog, []string{selected}, complete.Nodes)
|
|
72
|
+
ApplyArtifacts(partial, published)
|
|
73
|
+
if !citesArtifact(partial) {
|
|
74
|
+
t.Fatal("the partial rebuild dropped the citation edge the full build had")
|
|
75
|
+
}
|
|
76
|
+
if _, published := partial.Nodes["docs/sale.md#pricing"]; !published {
|
|
77
|
+
t.Fatal("the partial rebuild resolved an edge to a node it did not carry")
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// citesArtifact reports whether any doc-ref edge lands on the probe's artifact.
|
|
82
|
+
func citesArtifact(g *Graph) bool {
|
|
83
|
+
for _, edge := range g.Edges {
|
|
84
|
+
if edge.Kind == EdgeDocRef && edge.To == "docs/sale.md#pricing" {
|
|
85
|
+
return true
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return false
|
|
89
|
+
}
|
package/internal/graph/build.go
CHANGED
|
@@ -55,9 +55,39 @@ func BuildFiles(prog *driver.Program, selected []string, baseNodes map[string]*N
|
|
|
55
55
|
collectDeclarations(g, file)
|
|
56
56
|
}
|
|
57
57
|
g.addEdges(prog, selectedFiles, selected != nil)
|
|
58
|
+
g.releaseBuildState()
|
|
58
59
|
return g
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
// releaseBuildState drops the scratch the build needed, once the edge pass has
|
|
63
|
+
// finished with it.
|
|
64
|
+
//
|
|
65
|
+
// Every map cleared here is documented build-only, and each holds pointers into
|
|
66
|
+
// the compiler AST or into the preceding generation”'s nodes: resolved is keyed
|
|
67
|
+
// by *shimast.Node, docHosts holds one per documented declaration, and baseNodes
|
|
68
|
+
// is the whole prior endpoint index. A consumer that retains the returned Graph
|
|
69
|
+
// therefore pinned all of it for as long as it held the graph — and
|
|
70
|
+
// internal/graphsymbols retains one for the lifetime of an editor session
|
|
71
|
+
// between invalidations, closing the Program while the maps that reference its
|
|
72
|
+
// AST live on. Clearing them here rather than at that consumer is what keeps the
|
|
73
|
+
// decision with the producer, which is the only side that knows which fields are
|
|
74
|
+
// scratch.
|
|
75
|
+
//
|
|
76
|
+
// ExportedTargets and ImplementationSources are deliberately not cleared: the
|
|
77
|
+
// shard expansion path in cmd/ttscgraph/serve_shards.go reads both after this
|
|
78
|
+
// returns, and Nodes/Edges/Decorators/DocTags are the graph itself.
|
|
79
|
+
func (g *Graph) releaseBuildState() {
|
|
80
|
+
g.docTagPositions = nil
|
|
81
|
+
g.docHosts = nil
|
|
82
|
+
g.docHostPositions = nil
|
|
83
|
+
g.bodyNodes = nil
|
|
84
|
+
g.seen = nil
|
|
85
|
+
g.resolved = nil
|
|
86
|
+
g.edgeEvidenceFiles = nil
|
|
87
|
+
g.baseNodes = nil
|
|
88
|
+
g.selectedFiles = nil
|
|
89
|
+
}
|
|
90
|
+
|
|
61
91
|
// SourceTexts maps every program source to the resident checker text.
|
|
62
92
|
// Declaration and virtual bundled files are included because external graph
|
|
63
93
|
// leaves still carry facts and spans the source manifest must attest to.
|
|
@@ -454,6 +484,11 @@ func collectMembers(g *Graph, path string, statement *shimast.Node) {
|
|
|
454
484
|
|
|
455
485
|
func putDeclaredNode(g *Graph, path, name string, kind NodeKind, declaration *shimast.Node) {
|
|
456
486
|
id := nodeID(path, name, kind)
|
|
487
|
+
// Tags belong to the identity, not to whichever declaration of it wins the
|
|
488
|
+
// display span below. An overload run documented on its first signature and
|
|
489
|
+
// implemented underneath returns early from the node branch, so collecting
|
|
490
|
+
// there would drop exactly the declaration authors write documentation on.
|
|
491
|
+
collectDocTags(g, id, declaration)
|
|
457
492
|
hasBody := declarationHasImplementation(declaration, kind)
|
|
458
493
|
if _, exists := g.Nodes[id]; exists {
|
|
459
494
|
if !hasBody || g.bodyNodes[id] {
|