ttsc 0.23.0 → 0.25.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/LICENSE +21 -21
- package/README.md +66 -63
- package/cmd/ttscgraph/serve.go +264 -48
- package/cmd/ttscgraph/serve_answers_a_project_that_implements_its_own_ambient_declaration_test.go +69 -0
- package/cmd/ttscgraph/serve_early_error_encode_failure_is_observable_test.go +40 -0
- package/cmd/ttscgraph/serve_graph_snapshot_version_matches_typescript_client_test.go +32 -0
- package/cmd/ttscgraph/serve_phase_trace_accounts_for_load_failure_test.go +45 -0
- package/cmd/ttscgraph/serve_phase_trace_is_opt_in_and_payload_free_test.go +52 -0
- package/cmd/ttscgraph/serve_protocol_negotiates_incremental_shards_test.go +44 -0
- package/cmd/ttscgraph/serve_session_reloads_commonjs_package_entry_for_esm_test.go +2 -2
- package/cmd/ttscgraph/serve_session_reloads_superseding_module_candidates_test.go +46 -20
- package/cmd/ttscgraph/serve_session_retries_failed_dump_test.go +9 -9
- package/cmd/ttscgraph/serve_shards.go +1390 -0
- package/cmd/ttscgraph/serve_shards_abandoned_generation_converges_on_retry_test.go +92 -0
- package/cmd/ttscgraph/serve_shards_delete_removed_source_test.go +57 -0
- package/cmd/ttscgraph/serve_shards_external_endpoints_test.go +53 -0
- package/cmd/ttscgraph/serve_shards_keep_source_distributed_dependencies_at_the_boundary_test.go +108 -0
- package/cmd/ttscgraph/serve_shards_match_full_dump_oracle_test.go +88 -0
- package/cmd/ttscgraph/serve_shards_merge_pending_and_new_edits_test.go +50 -0
- package/cmd/ttscgraph/serve_shards_prune_unreferenced_external_endpoints_test.go +65 -0
- package/cmd/ttscgraph/serve_shards_publish_created_source_test.go +92 -0
- package/cmd/ttscgraph/serve_shards_rebuild_declaration_edit_test.go +43 -0
- package/cmd/ttscgraph/serve_shards_refresh_cross_file_implementation_test.go +63 -0
- package/cmd/ttscgraph/serve_shards_refresh_dependent_diagnostics_test.go +80 -0
- package/cmd/ttscgraph/serve_shards_rename_moves_source_identity_test.go +95 -0
- package/cmd/ttscgraph/serve_shards_retry_preserves_committed_generation_test.go +48 -0
- package/cmd/ttscgraph/serve_shards_reuse_unaffected_sources_test.go +121 -0
- package/driver/declaration_shape.go +85 -0
- package/driver/program.go +39 -10
- package/go.sum +22 -22
- package/internal/graph/assigned_implementations_of_declaration_file_symbols_leave_no_edge_source_test.go +84 -0
- package/internal/graph/assigned_implementations_of_node_modules_symbols_leave_no_edge_source_test.go +85 -0
- package/internal/graph/build.go +62 -10
- package/internal/graph/build_files_reuses_committed_cross_file_endpoints_test.go +87 -0
- package/internal/graph/build_keeps_source_distributed_dependencies_at_the_external_boundary_test.go +74 -0
- package/internal/graph/dump.go +84 -43
- package/internal/graph/dump_path_mapper_caches_alias_resolution_test.go +29 -0
- package/internal/graph/dump_paths.go +134 -11
- package/internal/graph/dump_paths_canonicalize_windows_short_root_test.go +39 -0
- package/internal/graph/dump_rejects_provenance_only_path_errors_test.go +32 -0
- package/internal/graph/dump_serializes_the_full_graph_test.go +2 -2
- package/internal/graph/dump_symlink_project_publishes_matching_base_test.go +82 -0
- package/internal/graph/edges.go +73 -17
- package/internal/graph/exports.go +9 -2
- package/internal/graph/graph.go +47 -7
- package/internal/graph/graph_kind_contracts_match_their_producers_test.go +121 -0
- package/internal/graph/node_modifiers_emit_union_strings_test.go +1 -1
- package/internal/graph/provenance.go +11 -1
- package/internal/graph/resolve.go +1 -4
- package/internal/graph/source.go +23 -0
- package/internal/graph/wire_node_ids_share_alias_resolution_test.go +29 -0
- package/internal/lspserver/lsp_native_plugin_source.go +5 -6
- package/internal/lspserver/lsp_native_resident.go +17 -24
- package/lib/TtscService.d.ts +1 -6
- package/lib/TtscService.js +2 -2
- package/lib/TtscService.js.map +1 -1
- package/lib/compiler/internal/captureProcessOutput.d.ts +26 -0
- package/lib/compiler/internal/captureProcessOutput.js +92 -0
- package/lib/compiler/internal/captureProcessOutput.js.map +1 -0
- package/lib/compiler/internal/compileProjectInMemory.js +4 -7
- package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
- package/lib/compiler/internal/residentCheckProcess.d.ts +0 -2
- package/lib/compiler/internal/residentCheckProcess.js +1 -15
- package/lib/compiler/internal/residentCheckProcess.js.map +1 -1
- package/lib/compiler/internal/residentTransformProcess.d.ts +0 -6
- package/lib/compiler/internal/residentTransformProcess.js +0 -25
- package/lib/compiler/internal/residentTransformProcess.js.map +1 -1
- package/lib/compiler/internal/spawnNative.d.ts +14 -6
- package/lib/compiler/internal/spawnNative.js +32 -11
- package/lib/compiler/internal/spawnNative.js.map +1 -1
- package/lib/compiler/internal/startResidentTransform.d.ts +1 -5
- package/lib/compiler/internal/startResidentTransform.js +1 -3
- package/lib/compiler/internal/startResidentTransform.js.map +1 -1
- package/lib/launcher/internal/prepareExecution.d.ts +2 -1
- package/lib/launcher/internal/prepareExecution.js +269 -23
- package/lib/launcher/internal/prepareExecution.js.map +1 -1
- package/lib/launcher/internal/registerRuntimeHooks.js +6 -1
- package/lib/launcher/internal/registerRuntimeHooks.js.map +1 -1
- package/lib/launcher/internal/runTtscserver.js +4 -9
- package/lib/launcher/internal/runTtscserver.js.map +1 -1
- package/lib/launcher/internal/runTtsx.js +2 -55
- package/lib/launcher/internal/runTtsx.js.map +1 -1
- package/lib/launcher/internal/runtimeHooks.d.ts +25 -2
- package/lib/launcher/internal/runtimeHooks.js +344 -69
- package/lib/launcher/internal/runtimeHooks.js.map +1 -1
- package/lib/plugin/internal/buildSourcePlugin.js +31 -3
- package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
- package/lib/plugin/internal/descriptorProcessFailure.d.ts +23 -19
- package/lib/plugin/internal/descriptorProcessFailure.js +40 -51
- package/lib/plugin/internal/descriptorProcessFailure.js.map +1 -1
- package/lib/plugin/internal/loadProjectPlugins.d.ts +12 -0
- package/lib/plugin/internal/loadProjectPlugins.js +77 -28
- package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
- package/package.json +8 -8
- package/shim/ast/extra-shim.json +4 -4
- package/shim/ast/go.mod +14 -14
- package/shim/ast/go.sum +20 -20
- package/shim/astnav/go.mod +14 -14
- package/shim/astnav/go.sum +20 -20
- package/shim/bundled/extra-shim.json +1 -1
- package/shim/bundled/go.mod +5 -5
- package/shim/checker/extra-shim.json +31 -31
- package/shim/checker/go.mod +5 -5
- package/shim/compiler/extra-shim.json +4 -4
- package/shim/compiler/go.mod +14 -14
- package/shim/compiler/go.sum +22 -22
- package/shim/core/extra-shim.json +4 -4
- package/shim/core/go.mod +5 -5
- package/shim/diagnosticwriter/go.mod +5 -5
- package/shim/lsp/extra-shim.json +4 -4
- package/shim/lsp/go.mod +16 -16
- package/shim/lsp/go.sum +26 -26
- package/shim/parser/extra-shim.json +4 -4
- package/shim/parser/go.mod +5 -5
- package/shim/printer/go.mod +14 -14
- package/shim/printer/go.sum +22 -22
- package/shim/scanner/extra-shim.json +4 -4
- package/shim/scanner/go.mod +5 -5
- package/shim/transformers/go.mod +14 -14
- package/shim/transformers/go.sum +20 -20
- package/shim/tsoptions/extra-shim.json +4 -4
- package/shim/tsoptions/go.mod +5 -5
- package/shim/tspath/extra-shim.json +4 -4
- package/shim/tspath/go.mod +5 -5
- package/shim/vfs/cachedvfs/extra-shim.json +4 -4
- package/shim/vfs/cachedvfs/go.mod +5 -5
- package/shim/vfs/extra-shim.json +4 -4
- package/shim/vfs/go.mod +5 -5
- package/shim/vfs/osvfs/extra-shim.json +4 -4
- package/shim/vfs/osvfs/go.mod +5 -5
- package/src/TtscService.ts +8 -20
- package/src/compiler/internal/captureProcessOutput.ts +96 -0
- package/src/compiler/internal/compileProjectInMemory.ts +7 -7
- package/src/compiler/internal/residentCheckProcess.ts +1 -21
- package/src/compiler/internal/residentTransformProcess.ts +0 -42
- package/src/compiler/internal/spawnNative.ts +39 -18
- package/src/compiler/internal/startResidentTransform.ts +1 -12
- package/src/launcher/internal/prepareExecution.ts +297 -26
- package/src/launcher/internal/registerRuntimeHooks.ts +11 -2
- package/src/launcher/internal/runTtscserver.ts +4 -9
- package/src/launcher/internal/runTtsx.ts +2 -78
- package/src/launcher/internal/runtimeHooks.ts +385 -82
- package/src/plugin/internal/buildSourcePlugin.ts +36 -3
- package/src/plugin/internal/descriptorProcessFailure.ts +34 -65
- package/src/plugin/internal/loadProjectPlugins.ts +78 -39
- package/tools/gen_shims/go.mod +14 -14
- package/tools/gen_shims/go.sum +12 -12
- package/tools/shim_audit/go.mod +10 -10
- package/tools/shim_audit/go.sum +8 -8
|
@@ -2,6 +2,8 @@ package graph
|
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
4
|
"fmt"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"runtime"
|
|
5
7
|
"strings"
|
|
6
8
|
|
|
7
9
|
shimtspath "github.com/microsoft/typescript-go/shim/tspath"
|
|
@@ -12,19 +14,26 @@ import (
|
|
|
12
14
|
// reject both an unportable filesystem root and a non-injective projection
|
|
13
15
|
// before any JSON is written.
|
|
14
16
|
type dumpPathMapper struct {
|
|
17
|
+
rawProject string
|
|
15
18
|
project string
|
|
16
19
|
caseSensitive bool
|
|
20
|
+
canonicalize func(string) string
|
|
17
21
|
|
|
22
|
+
rawToWire map[string]string
|
|
18
23
|
physicalToWire map[string]string
|
|
19
24
|
wireToPhysical map[string]string
|
|
20
25
|
mappingErr error
|
|
21
26
|
}
|
|
22
27
|
|
|
23
28
|
func newDumpPathMapper(project string) *dumpPathMapper {
|
|
24
|
-
|
|
29
|
+
raw := shimtspath.NormalizePath(shimtspath.NormalizeSlashes(project))
|
|
30
|
+
normalized := canonicalDumpPath(raw)
|
|
25
31
|
mapper := &dumpPathMapper{
|
|
32
|
+
rawProject: raw,
|
|
26
33
|
project: normalized,
|
|
27
34
|
caseSensitive: dumpPathRootIsCaseSensitive(normalized),
|
|
35
|
+
canonicalize: canonicalDumpPath,
|
|
36
|
+
rawToWire: map[string]string{},
|
|
28
37
|
physicalToWire: map[string]string{},
|
|
29
38
|
wireToPhysical: map[string]string{},
|
|
30
39
|
}
|
|
@@ -34,6 +43,62 @@ func newDumpPathMapper(project string) *dumpPathMapper {
|
|
|
34
43
|
return mapper
|
|
35
44
|
}
|
|
36
45
|
|
|
46
|
+
// WireProject returns the canonical filesystem base that owns every relative
|
|
47
|
+
// wire path emitted by this package.
|
|
48
|
+
func WireProject(project string) (string, error) {
|
|
49
|
+
mapper := newDumpPathMapper(project)
|
|
50
|
+
return mapper.project, mapper.err()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// WirePath maps one compiler path into the portable schema-v6 vocabulary used
|
|
54
|
+
// by dumps and resident graph shards. Callers that project a complete graph
|
|
55
|
+
// keep one dumpPathMapper so collision detection spans every path; callers
|
|
56
|
+
// shaping a single source/config coordinate use this helper and receive the
|
|
57
|
+
// same filesystem-alias and cross-root behavior.
|
|
58
|
+
func WirePath(project, file string) (string, error) {
|
|
59
|
+
mapper := newDumpPathMapper(project)
|
|
60
|
+
wire := mapper.mapPath(file)
|
|
61
|
+
return wire, mapper.err()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// WireNodeID maps the filesystem-bearing portions of one internal node ID into
|
|
65
|
+
// the same portable vocabulary as NewDumpFacts. Resident stores use it when a
|
|
66
|
+
// wire-level external reference count must be reconciled with the immutable
|
|
67
|
+
// graph.Node cache, whose keys retain compiler-physical paths.
|
|
68
|
+
func WireNodeID(project, id string) (string, error) {
|
|
69
|
+
ids, err := WireNodeIDs(project, []string{id})
|
|
70
|
+
return ids[id], err
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// WireNodeIDs maps a set of internal node IDs through one path mapper. Sharing
|
|
74
|
+
// the mapper preserves cross-ID collision detection and resolves each repeated
|
|
75
|
+
// filesystem alias only once per snapshot generation.
|
|
76
|
+
func WireNodeIDs(project string, ids []string) (map[string]string, error) {
|
|
77
|
+
mapper := newDumpPathMapper(project)
|
|
78
|
+
wireIDs := make(map[string]string, len(ids))
|
|
79
|
+
for _, id := range ids {
|
|
80
|
+
wire, err := wireNodeID(mapper, id)
|
|
81
|
+
if err != nil {
|
|
82
|
+
return nil, err
|
|
83
|
+
}
|
|
84
|
+
wireIDs[id] = wire
|
|
85
|
+
}
|
|
86
|
+
return wireIDs, mapper.err()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
func wireNodeID(mapper *dumpPathMapper, id string) (string, error) {
|
|
90
|
+
parts, ok := parseNodeID(id)
|
|
91
|
+
if !ok {
|
|
92
|
+
return "", fmt.Errorf("ttscgraph: invalid internal graph node id %q", id)
|
|
93
|
+
}
|
|
94
|
+
name := parts.name
|
|
95
|
+
if parts.kind == NodeModule {
|
|
96
|
+
name = mapper.mapPath(name)
|
|
97
|
+
}
|
|
98
|
+
wire := nodeID(mapper.mapPath(parts.path), name, parts.kind)
|
|
99
|
+
return wire, nil
|
|
100
|
+
}
|
|
101
|
+
|
|
37
102
|
// mapPath returns one portable, slash-normalized coordinate:
|
|
38
103
|
//
|
|
39
104
|
// - project files stay project-relative;
|
|
@@ -49,26 +114,30 @@ func (m *dumpPathMapper) mapPath(file string) string {
|
|
|
49
114
|
if file == "" {
|
|
50
115
|
return ""
|
|
51
116
|
}
|
|
52
|
-
normalized := shimtspath.NormalizeSlashes(file)
|
|
117
|
+
normalized := shimtspath.NormalizePath(shimtspath.NormalizeSlashes(file))
|
|
53
118
|
if strings.HasPrefix(normalized, "bundled:///") {
|
|
54
119
|
return m.claim(normalized, normalized)
|
|
55
120
|
}
|
|
56
|
-
normalized = shimtspath.NormalizePath(normalized)
|
|
57
121
|
if m.project == "" || shimtspath.GetRootLength(m.project) == 0 {
|
|
58
122
|
return normalized
|
|
59
123
|
}
|
|
60
124
|
|
|
61
|
-
|
|
62
|
-
if shimtspath.GetRootLength(
|
|
63
|
-
|
|
125
|
+
rawPhysical := normalized
|
|
126
|
+
if shimtspath.GetRootLength(rawPhysical) == 0 {
|
|
127
|
+
rawPhysical = shimtspath.GetNormalizedAbsolutePath(rawPhysical, m.rawProject)
|
|
64
128
|
}
|
|
129
|
+
rawKey := m.pathKey(rawPhysical)
|
|
130
|
+
if wire, ok := m.rawToWire[rawKey]; ok {
|
|
131
|
+
return wire
|
|
132
|
+
}
|
|
133
|
+
physical := m.canonicalize(rawPhysical)
|
|
65
134
|
if !dumpPathRootsEqual(m.project, physical, m.caseSensitive) {
|
|
66
135
|
m.fail(fmt.Errorf(
|
|
67
136
|
"ttscgraph: source path %q cannot be represented relative to project %q because they are on different filesystem roots",
|
|
68
|
-
|
|
137
|
+
rawPhysical,
|
|
69
138
|
m.project,
|
|
70
139
|
))
|
|
71
|
-
return
|
|
140
|
+
return rawPhysical
|
|
72
141
|
}
|
|
73
142
|
options := shimtspath.ComparePathsOptions{
|
|
74
143
|
CurrentDirectory: m.project,
|
|
@@ -78,12 +147,21 @@ func (m *dumpPathMapper) mapPath(file string) string {
|
|
|
78
147
|
if shimtspath.GetRootLength(wire) != 0 {
|
|
79
148
|
m.fail(fmt.Errorf(
|
|
80
149
|
"ttscgraph: source path %q cannot be represented relative to project %q because they are on different filesystem roots",
|
|
81
|
-
|
|
150
|
+
rawPhysical,
|
|
82
151
|
m.project,
|
|
83
152
|
))
|
|
84
|
-
return
|
|
153
|
+
return rawPhysical
|
|
85
154
|
}
|
|
86
|
-
|
|
155
|
+
wire = m.claim(physical, wire)
|
|
156
|
+
m.rawToWire[rawKey] = wire
|
|
157
|
+
return wire
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
func (m *dumpPathMapper) pathKey(path string) string {
|
|
161
|
+
if !m.caseSensitive {
|
|
162
|
+
return strings.ToLower(path)
|
|
163
|
+
}
|
|
164
|
+
return path
|
|
87
165
|
}
|
|
88
166
|
|
|
89
167
|
// claim records both directions of the projection. The reverse map is the
|
|
@@ -127,6 +205,51 @@ func (m *dumpPathMapper) fail(err error) {
|
|
|
127
205
|
|
|
128
206
|
func (m *dumpPathMapper) err() error { return m.mappingErr }
|
|
129
207
|
|
|
208
|
+
// canonicalDumpPath collapses filesystem aliases for an existing path before
|
|
209
|
+
// it enters the wire-coordinate mapper. TypeScript reports physical source
|
|
210
|
+
// names, while a caller may select the same project through a symlink or a
|
|
211
|
+
// Windows 8.3 spelling; comparing those raw strings would leak a producer-local
|
|
212
|
+
// absolute path into an otherwise portable snapshot.
|
|
213
|
+
//
|
|
214
|
+
// Synthetic paths in mapper unit tests and missing paths retain their lexical
|
|
215
|
+
// spelling. Real graph inputs exist by construction, so the best-effort branch
|
|
216
|
+
// covers their physical identity without weakening the mapper's explicit
|
|
217
|
+
// cross-root and collision checks.
|
|
218
|
+
func canonicalDumpPath(location string) string {
|
|
219
|
+
normalized := shimtspath.NormalizePath(shimtspath.NormalizeSlashes(location))
|
|
220
|
+
if normalized == "" || shimtspath.GetRootLength(normalized) == 0 {
|
|
221
|
+
return normalized
|
|
222
|
+
}
|
|
223
|
+
if !dumpPathUsesHostFilesystem(normalized) {
|
|
224
|
+
return normalized
|
|
225
|
+
}
|
|
226
|
+
candidate := filepath.Clean(filepath.FromSlash(normalized))
|
|
227
|
+
suffix := []string{}
|
|
228
|
+
for {
|
|
229
|
+
physical, err := filepath.EvalSymlinks(candidate)
|
|
230
|
+
if err == nil {
|
|
231
|
+
for index := len(suffix) - 1; index >= 0; index-- {
|
|
232
|
+
physical = filepath.Join(physical, suffix[index])
|
|
233
|
+
}
|
|
234
|
+
return shimtspath.NormalizePath(shimtspath.NormalizeSlashes(physical))
|
|
235
|
+
}
|
|
236
|
+
parent := filepath.Dir(candidate)
|
|
237
|
+
if parent == candidate {
|
|
238
|
+
break
|
|
239
|
+
}
|
|
240
|
+
suffix = append(suffix, filepath.Base(candidate))
|
|
241
|
+
candidate = parent
|
|
242
|
+
}
|
|
243
|
+
return normalized
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
func dumpPathUsesHostFilesystem(path string) bool {
|
|
247
|
+
if runtime.GOOS == "windows" {
|
|
248
|
+
return strings.HasPrefix(path, "//") || (len(path) >= 2 && path[1] == ':')
|
|
249
|
+
}
|
|
250
|
+
return strings.HasPrefix(path, "/") && !strings.HasPrefix(path, "//")
|
|
251
|
+
}
|
|
252
|
+
|
|
130
253
|
// Windows drive and UNC roots use case-insensitive path comparison. POSIX
|
|
131
254
|
// roots remain case-sensitive. This decision follows the path's own grammar so
|
|
132
255
|
// synthetic Windows/UNC fixtures behave the same on every CI host.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
//go:build windows
|
|
2
|
+
|
|
3
|
+
package graph
|
|
4
|
+
|
|
5
|
+
import (
|
|
6
|
+
"path/filepath"
|
|
7
|
+
"testing"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// TestDumpPathMapperCanonicalizesWindowsShortRoot verifies that a project
|
|
11
|
+
// selected through an 8.3 path still owns checker sources reported through the
|
|
12
|
+
// expanded physical spelling.
|
|
13
|
+
//
|
|
14
|
+
// 1. Obtain the environment's temp spelling and its physical spelling.
|
|
15
|
+
// 2. Skip hosts where Windows exposes only one spelling.
|
|
16
|
+
// 3. Require a physical child source to retain a project-relative identity.
|
|
17
|
+
func TestDumpPathMapperCanonicalizesWindowsShortRoot(t *testing.T) {
|
|
18
|
+
project := t.TempDir()
|
|
19
|
+
physical, err := filepath.EvalSymlinks(project)
|
|
20
|
+
if err != nil {
|
|
21
|
+
t.Fatal(err)
|
|
22
|
+
}
|
|
23
|
+
if filepath.Clean(project) == filepath.Clean(physical) {
|
|
24
|
+
t.Skip("temporary directory has no alternate Windows path spelling")
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
mapper := newDumpPathMapper(project)
|
|
28
|
+
source := filepath.Join(physical, "src", "main.ts")
|
|
29
|
+
if got := mapper.mapPath(source); got != "src/main.ts" {
|
|
30
|
+
t.Fatalf("mapPath(%q) = %q, want project-relative identity", source, got)
|
|
31
|
+
}
|
|
32
|
+
missing := filepath.Join(project, "future", "main.ts")
|
|
33
|
+
if got := mapper.mapPath(missing); got != "future/main.ts" {
|
|
34
|
+
t.Fatalf("mapPath(%q) = %q, want stable missing-root identity", missing, got)
|
|
35
|
+
}
|
|
36
|
+
if err := mapper.err(); err != nil {
|
|
37
|
+
t.Fatal(err)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
package graph
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"strings"
|
|
5
|
+
"testing"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
// TestNewDumpRejectsProvenanceOnlyPathErrors verifies paths introduced after
|
|
9
|
+
// fact projection still pass through the dump context's fail-closed boundary.
|
|
10
|
+
//
|
|
11
|
+
// Provenance inputs are mapped after graph facts. Without a final path-error
|
|
12
|
+
// check, a cross-drive config could survive into otherwise valid JSON.
|
|
13
|
+
//
|
|
14
|
+
// 1. Build an empty graph under one synthetic Windows drive.
|
|
15
|
+
// 2. Add a provenance-only config input from another drive.
|
|
16
|
+
// 3. Require NewDump to reject the unrepresentable coordinate.
|
|
17
|
+
func TestNewDumpRejectsProvenanceOnlyPathErrors(t *testing.T) {
|
|
18
|
+
_, err := NewDump(
|
|
19
|
+
&Graph{Nodes: map[string]*Node{}},
|
|
20
|
+
"C:/checkout/app",
|
|
21
|
+
"tsconfig.json",
|
|
22
|
+
nil,
|
|
23
|
+
nil,
|
|
24
|
+
DumpOrigin{Provenance: Provenance{Universe: Universe{Configs: []FileDigest{{
|
|
25
|
+
File: "D:/shared/tsconfig.json",
|
|
26
|
+
Digest: "config-digest",
|
|
27
|
+
}}}}},
|
|
28
|
+
)
|
|
29
|
+
if err == nil || !strings.Contains(err.Error(), "different filesystem roots") {
|
|
30
|
+
t.Fatalf("provenance-only cross-root error = %v, want rejection before serialization", err)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -55,8 +55,8 @@ export function main(): void {
|
|
|
55
55
|
if err := json.Unmarshal(data, &dump); err != nil {
|
|
56
56
|
t.Fatalf("dump is not valid JSON: %v\n%s", err, data)
|
|
57
57
|
}
|
|
58
|
-
if dump.Project != root || dump.Tsconfig != "tsconfig.json" {
|
|
59
|
-
t.Fatalf("project/tsconfig not
|
|
58
|
+
if dump.Project != canonicalDumpPath(root) || dump.Tsconfig != "tsconfig.json" {
|
|
59
|
+
t.Fatalf("project/tsconfig coordinates are not canonical: %q / %q", dump.Project, dump.Tsconfig)
|
|
60
60
|
}
|
|
61
61
|
if len(dump.Nodes) != len(g.Nodes) {
|
|
62
62
|
t.Fatalf("dumped %d nodes, graph has %d", len(dump.Nodes), len(g.Nodes))
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
package graph
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"os"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"testing"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
// TestDumpSymlinkProjectPublishesMatchingBase verifies relative wire paths and
|
|
10
|
+
// the published project coordinate share one canonical filesystem base.
|
|
11
|
+
//
|
|
12
|
+
// A raw symlink cwd combined with canonical source paths makes `../` siblings,
|
|
13
|
+
// missing literal roots and a symlinked relative tsconfig resolve incorrectly
|
|
14
|
+
// unless project publication and path mapping use the same physical root.
|
|
15
|
+
//
|
|
16
|
+
// 1. Select a real project directory through a symlink and add sibling inputs.
|
|
17
|
+
// 2. Map an existing sibling, a missing child and a relative config symlink.
|
|
18
|
+
// 3. Require stable coordinates and a canonical published project base.
|
|
19
|
+
func TestDumpSymlinkProjectPublishesMatchingBase(t *testing.T) {
|
|
20
|
+
root := t.TempDir()
|
|
21
|
+
realRoot := filepath.Join(root, "real")
|
|
22
|
+
project := filepath.Join(realRoot, "pkg")
|
|
23
|
+
configDir := filepath.Join(realRoot, "config")
|
|
24
|
+
if err := os.MkdirAll(project, 0o755); err != nil {
|
|
25
|
+
t.Fatal(err)
|
|
26
|
+
}
|
|
27
|
+
if err := os.MkdirAll(configDir, 0o755); err != nil {
|
|
28
|
+
t.Fatal(err)
|
|
29
|
+
}
|
|
30
|
+
shared := filepath.Join(realRoot, "shared.ts")
|
|
31
|
+
config := filepath.Join(configDir, "tsconfig.json")
|
|
32
|
+
if err := os.WriteFile(shared, []byte("export const shared = true;\n"), 0o644); err != nil {
|
|
33
|
+
t.Fatal(err)
|
|
34
|
+
}
|
|
35
|
+
if err := os.WriteFile(config, []byte("{}\n"), 0o644); err != nil {
|
|
36
|
+
t.Fatal(err)
|
|
37
|
+
}
|
|
38
|
+
alias := filepath.Join(root, "project-link")
|
|
39
|
+
if err := os.Symlink(project, alias); err != nil {
|
|
40
|
+
t.Skipf("host cannot create directory symlink: %v", err)
|
|
41
|
+
}
|
|
42
|
+
if err := os.Symlink(config, filepath.Join(project, "tsconfig.json")); err != nil {
|
|
43
|
+
t.Skipf("host cannot create file symlink: %v", err)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
mapper := newDumpPathMapper(alias)
|
|
47
|
+
if wire := mapper.mapPath(shared); wire != "../shared.ts" {
|
|
48
|
+
t.Fatalf("sibling wire path = %q, want ../shared.ts", wire)
|
|
49
|
+
}
|
|
50
|
+
if wire := mapper.mapPath(filepath.Join(alias, "src", "new.ts")); wire != "src/new.ts" {
|
|
51
|
+
t.Fatalf("missing-root wire path = %q, want src/new.ts", wire)
|
|
52
|
+
}
|
|
53
|
+
if wire := mapper.mapPath("tsconfig.json"); wire != "../config/tsconfig.json" {
|
|
54
|
+
t.Fatalf("relative config wire path = %q, want ../config/tsconfig.json", wire)
|
|
55
|
+
}
|
|
56
|
+
dump, err := NewDump(&Graph{Nodes: map[string]*Node{}}, alias, "tsconfig.json", nil, nil, DumpOrigin{})
|
|
57
|
+
if err != nil {
|
|
58
|
+
t.Fatal(err)
|
|
59
|
+
}
|
|
60
|
+
physicalProject, err := filepath.EvalSymlinks(alias)
|
|
61
|
+
if err != nil {
|
|
62
|
+
t.Fatal(err)
|
|
63
|
+
}
|
|
64
|
+
if filepath.Clean(filepath.FromSlash(dump.Project)) != filepath.Clean(physicalProject) {
|
|
65
|
+
t.Fatalf("published project = %q, want %q", dump.Project, physicalProject)
|
|
66
|
+
}
|
|
67
|
+
resolvedSibling := filepath.Clean(filepath.Join(filepath.FromSlash(dump.Project), filepath.FromSlash("../shared.ts")))
|
|
68
|
+
resolvedSiblingInfo, err := os.Stat(resolvedSibling)
|
|
69
|
+
if err != nil {
|
|
70
|
+
t.Fatal(err)
|
|
71
|
+
}
|
|
72
|
+
sharedInfo, err := os.Stat(shared)
|
|
73
|
+
if err != nil {
|
|
74
|
+
t.Fatal(err)
|
|
75
|
+
}
|
|
76
|
+
if !os.SameFile(resolvedSiblingInfo, sharedInfo) {
|
|
77
|
+
t.Fatalf("published base resolves sibling to %q, want %q", resolvedSibling, shared)
|
|
78
|
+
}
|
|
79
|
+
if dump.Tsconfig != "../config/tsconfig.json" {
|
|
80
|
+
t.Fatalf("published tsconfig = %q, want ../config/tsconfig.json", dump.Tsconfig)
|
|
81
|
+
}
|
|
82
|
+
}
|
package/internal/graph/edges.go
CHANGED
|
@@ -18,9 +18,15 @@ import (
|
|
|
18
18
|
// it annotates, a literal value set on the type that admits it. Both need the
|
|
19
19
|
// same checker and the same walk, so they are resolved here and written to the
|
|
20
20
|
// node the build pass keyed.
|
|
21
|
-
func (g *Graph) addEdges(prog *driver.Program) {
|
|
21
|
+
func (g *Graph) addEdges(prog *driver.Program, selected map[string]bool, partial bool) {
|
|
22
22
|
checker := prog.Checker
|
|
23
23
|
for _, file := range prog.SourceFiles() {
|
|
24
|
+
if !IsWorkspaceSourceFile(file) {
|
|
25
|
+
continue
|
|
26
|
+
}
|
|
27
|
+
if partial && !selected[file.FileName()] {
|
|
28
|
+
continue
|
|
29
|
+
}
|
|
24
30
|
g.markExports(checker, file)
|
|
25
31
|
g.collectHeritage(checker, file)
|
|
26
32
|
g.collectCalls(checker, file)
|
|
@@ -54,7 +60,8 @@ func (g *Graph) addEdgeAt(from, to string, kind EdgeKind, origin string, pos, en
|
|
|
54
60
|
return
|
|
55
61
|
}
|
|
56
62
|
g.seen[key] = struct{}{}
|
|
57
|
-
|
|
63
|
+
file := g.edgeEvidenceFiles[from]
|
|
64
|
+
g.Edges = append(g.Edges, &Edge{From: from, To: to, Kind: kind, Origin: origin, File: file, Pos: pos, End: end})
|
|
58
65
|
}
|
|
59
66
|
|
|
60
67
|
// collectHeritage adds a heritage edge for every base of every class and
|
|
@@ -320,7 +327,7 @@ func graphMemberNodeID(g *Graph, member *shimast.Node) string {
|
|
|
320
327
|
return ""
|
|
321
328
|
}
|
|
322
329
|
id := nodeID(file.FileName(), name, kind)
|
|
323
|
-
stored, ok := g.
|
|
330
|
+
stored, ok := g.lookupNode(id)
|
|
324
331
|
if !ok {
|
|
325
332
|
return ""
|
|
326
333
|
}
|
|
@@ -576,8 +583,10 @@ func (g *Graph) callsWithin(checker *shimchecker.Checker, from string, node *shi
|
|
|
576
583
|
}
|
|
577
584
|
case shimast.KindBinaryExpression:
|
|
578
585
|
if target := g.assignedFunctionTarget(checker, from, child); target != "" {
|
|
579
|
-
g.recordImplementation(target, child)
|
|
580
|
-
g.
|
|
586
|
+
file := g.recordImplementation(target, child)
|
|
587
|
+
g.withEdgeEvidenceFile(target, file, func() {
|
|
588
|
+
g.callsWithin(checker, target, child.AsBinaryExpression().Right)
|
|
589
|
+
})
|
|
581
590
|
}
|
|
582
591
|
case shimast.KindJsxSelfClosingElement:
|
|
583
592
|
// `<Component />` is a use of the component; an intrinsic tag (`<div />`)
|
|
@@ -595,6 +604,20 @@ func (g *Graph) callsWithin(checker *shimchecker.Checker, from string, node *shi
|
|
|
595
604
|
})
|
|
596
605
|
}
|
|
597
606
|
|
|
607
|
+
// assignedFunctionTarget returns the graph node an `x.y = function` assignment
|
|
608
|
+
// implements, or "" when the assignment implements no node this graph owns.
|
|
609
|
+
//
|
|
610
|
+
// A declaration outside the workspace is not such a node. `globalThis.assert =
|
|
611
|
+
// …` against a `declare global { function assert(…) }`, or a patched member of
|
|
612
|
+
// a dependency's `@types`, resolves to a symbol whose only declaration is in a
|
|
613
|
+
// declaration file — which has no body, holds no facts, and enters the graph as
|
|
614
|
+
// an external boundary leaf. Re-attributing the assigned body to it moved the
|
|
615
|
+
// implementation's calls and type references onto a node that never ran them,
|
|
616
|
+
// under offsets read against the wrong file: `recordImplementation` already
|
|
617
|
+
// refuses an external node, so the evidence file stayed unset and the spans
|
|
618
|
+
// pointed at neither the declaration nor the implementation. And the edge
|
|
619
|
+
// itself was unassemblable, so every request against such a project failed at
|
|
620
|
+
// shard partition.
|
|
598
621
|
func (g *Graph) assignedFunctionTarget(checker *shimchecker.Checker, from string, node *shimast.Node) string {
|
|
599
622
|
binary := node.AsBinaryExpression()
|
|
600
623
|
if binary == nil ||
|
|
@@ -605,7 +628,7 @@ func (g *Graph) assignedFunctionTarget(checker *shimchecker.Checker, from string
|
|
|
605
628
|
return ""
|
|
606
629
|
}
|
|
607
630
|
target := g.resolve(checker, binary.Left)
|
|
608
|
-
if target == nil || target.Symbol == nil {
|
|
631
|
+
if target == nil || target.Symbol == nil || target.External {
|
|
609
632
|
return ""
|
|
610
633
|
}
|
|
611
634
|
to := g.ensureTargetNode(target)
|
|
@@ -615,18 +638,45 @@ func (g *Graph) assignedFunctionTarget(checker *shimchecker.Checker, from string
|
|
|
615
638
|
return to
|
|
616
639
|
}
|
|
617
640
|
|
|
618
|
-
func (g *Graph) recordImplementation(id string, assignment *shimast.Node) {
|
|
619
|
-
node := g.
|
|
620
|
-
if
|
|
621
|
-
return
|
|
641
|
+
func (g *Graph) recordImplementation(id string, assignment *shimast.Node) string {
|
|
642
|
+
node, ok := g.lookupNode(id)
|
|
643
|
+
if !ok || node.External {
|
|
644
|
+
return ""
|
|
622
645
|
}
|
|
623
646
|
file := shimast.GetSourceFileOfNode(assignment)
|
|
624
647
|
if file == nil {
|
|
648
|
+
return ""
|
|
649
|
+
}
|
|
650
|
+
sources := g.ImplementationSources[id]
|
|
651
|
+
if sources == nil {
|
|
652
|
+
sources = map[string]bool{}
|
|
653
|
+
g.ImplementationSources[id] = sources
|
|
654
|
+
}
|
|
655
|
+
sources[file.FileName()] = true
|
|
656
|
+
current := g.Nodes[id]
|
|
657
|
+
if current != nil && current.ImplementationFile == "" {
|
|
658
|
+
current.ImplementationFile = file.FileName()
|
|
659
|
+
current.ImplementationPos = assignment.Pos()
|
|
660
|
+
current.ImplementationEnd = assignment.End()
|
|
661
|
+
}
|
|
662
|
+
return file.FileName()
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
func (g *Graph) withEdgeEvidenceFile(from, file string, visit func()) {
|
|
666
|
+
if file == "" {
|
|
667
|
+
visit()
|
|
625
668
|
return
|
|
626
669
|
}
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
670
|
+
previous, existed := g.edgeEvidenceFiles[from]
|
|
671
|
+
g.edgeEvidenceFiles[from] = file
|
|
672
|
+
defer func() {
|
|
673
|
+
if existed {
|
|
674
|
+
g.edgeEvidenceFiles[from] = previous
|
|
675
|
+
} else {
|
|
676
|
+
delete(g.edgeEvidenceFiles, from)
|
|
677
|
+
}
|
|
678
|
+
}()
|
|
679
|
+
visit()
|
|
630
680
|
}
|
|
631
681
|
|
|
632
682
|
func isInvokedAccess(access *shimast.Node) bool {
|
|
@@ -745,7 +795,13 @@ func (g *Graph) typeRefsWithin(checker *shimchecker.Checker, from string, node *
|
|
|
745
795
|
}
|
|
746
796
|
case shimast.KindBinaryExpression:
|
|
747
797
|
if target := g.assignedFunctionTarget(checker, from, child); target != "" {
|
|
748
|
-
|
|
798
|
+
file := ""
|
|
799
|
+
if source := shimast.GetSourceFileOfNode(child); source != nil {
|
|
800
|
+
file = source.FileName()
|
|
801
|
+
}
|
|
802
|
+
g.withEdgeEvidenceFile(target, file, func() {
|
|
803
|
+
g.typeRefsWithin(checker, target, child.AsBinaryExpression().Right)
|
|
804
|
+
})
|
|
749
805
|
}
|
|
750
806
|
}
|
|
751
807
|
g.typeRefsWithin(checker, from, child)
|
|
@@ -795,7 +851,7 @@ func (g *Graph) ensureTargetNode(target *Target) string {
|
|
|
795
851
|
return ""
|
|
796
852
|
}
|
|
797
853
|
id := nodeID(target.File, name, NodeMethod)
|
|
798
|
-
if _, exists := g.
|
|
854
|
+
if _, exists := g.lookupNode(id); exists {
|
|
799
855
|
return id
|
|
800
856
|
}
|
|
801
857
|
return ""
|
|
@@ -805,7 +861,7 @@ func (g *Graph) ensureTargetNode(target *Target) string {
|
|
|
805
861
|
}
|
|
806
862
|
name := qualifiedName(target.Symbol)
|
|
807
863
|
id := nodeID(target.File, name, kind)
|
|
808
|
-
if _, exists := g.
|
|
864
|
+
if _, exists := g.lookupNode(id); exists {
|
|
809
865
|
return id
|
|
810
866
|
}
|
|
811
867
|
if !target.External {
|
|
@@ -851,7 +907,7 @@ func (g *Graph) closureTargetID(target *Target) string {
|
|
|
851
907
|
kind = NodeVariable
|
|
852
908
|
}
|
|
853
909
|
id := nodeID(target.File, name, kind)
|
|
854
|
-
if _, exists := g.
|
|
910
|
+
if _, exists := g.lookupNode(id); exists {
|
|
855
911
|
return id
|
|
856
912
|
}
|
|
857
913
|
}
|
|
@@ -108,10 +108,17 @@ func (g *Graph) markExportedSymbol(symbol *shimast.Symbol) (string, bool) {
|
|
|
108
108
|
if name == "" {
|
|
109
109
|
return "", false
|
|
110
110
|
}
|
|
111
|
-
node, ok := g.
|
|
111
|
+
node, ok := g.lookupNode(nodeID(file.FileName(), name, kind))
|
|
112
112
|
if !ok {
|
|
113
113
|
return "", false
|
|
114
114
|
}
|
|
115
|
-
node.
|
|
115
|
+
g.ExportedTargets[node.ID] = true
|
|
116
|
+
// A complete build and an invalidation transaction that includes the
|
|
117
|
+
// declaration owner both hold a fresh node and may stamp it directly. Never
|
|
118
|
+
// mutate a base node: it belongs to the last committed generation until the
|
|
119
|
+
// caller has assembled and validated every replacement shard.
|
|
120
|
+
if current, exists := g.Nodes[node.ID]; exists {
|
|
121
|
+
current.Exported = true
|
|
122
|
+
}
|
|
116
123
|
return node.ID, true
|
|
117
124
|
}
|
package/internal/graph/graph.go
CHANGED
|
@@ -171,9 +171,11 @@ const (
|
|
|
171
171
|
)
|
|
172
172
|
|
|
173
173
|
// Edge is a directed, checker-resolved relationship from one node to another,
|
|
174
|
-
// both referenced by Node.ID. Pos and End bound the source expression
|
|
175
|
-
//
|
|
176
|
-
//
|
|
174
|
+
// both referenced by Node.ID. File, Pos, and End bound the source expression
|
|
175
|
+
// that produced the edge. File is empty when the From node names it and is set
|
|
176
|
+
// only when an assigned implementation attributes an expression in another
|
|
177
|
+
// file to the declaration node it implements. They are evidence, not identity;
|
|
178
|
+
// a duplicate relationship keeps the first source-order span.
|
|
177
179
|
type Edge struct {
|
|
178
180
|
From string
|
|
179
181
|
To string
|
|
@@ -186,6 +188,7 @@ type Edge struct {
|
|
|
186
188
|
// "jsx", or "tagged"; for EdgeHeritage it is "extends" or "implements";
|
|
187
189
|
// for EdgeMemberRelation it is "implements" or "overrides".
|
|
188
190
|
Origin string
|
|
191
|
+
File string
|
|
189
192
|
Pos int
|
|
190
193
|
End int
|
|
191
194
|
}
|
|
@@ -196,10 +199,10 @@ type Graph struct {
|
|
|
196
199
|
Nodes map[string]*Node
|
|
197
200
|
Edges []*Edge
|
|
198
201
|
// Decorators holds the decorators written on the workspace's declarations,
|
|
199
|
-
// captured syntactically so the JSON dump can
|
|
200
|
-
// consumer can interpret `@Controller`/`@Get` conventions without
|
|
201
|
-
// source. It is dump-only metadata, separate from Edges so the
|
|
202
|
-
// checker-resolved relationships are untouched.
|
|
202
|
+
// captured syntactically so the JSON dump can attach raw facts to each target
|
|
203
|
+
// node and a consumer can interpret `@Controller`/`@Get` conventions without
|
|
204
|
+
// re-parsing source. It is dump-only metadata, separate from Edges so the
|
|
205
|
+
// existing checker-resolved relationships are untouched.
|
|
203
206
|
Decorators []*Decorator
|
|
204
207
|
// bodyNodes tracks whether a callable node's display span is the overload
|
|
205
208
|
// implementation rather than an overload signature. It is build-only metadata
|
|
@@ -213,6 +216,43 @@ type Graph struct {
|
|
|
213
216
|
// build: the edge pass visits the same node several times, and a resolution
|
|
214
217
|
// cannot change while the program is fixed.
|
|
215
218
|
resolved map[*shimast.Node]*Target
|
|
219
|
+
|
|
220
|
+
// edgeEvidenceFiles temporarily overrides a declaration node's source while
|
|
221
|
+
// an assigned implementation in another file is walked. Builds are
|
|
222
|
+
// single-threaded, and the helper that installs an override restores it
|
|
223
|
+
// before the traversal returns.
|
|
224
|
+
edgeEvidenceFiles map[string]string
|
|
225
|
+
|
|
226
|
+
// baseNodes is the immutable endpoint index of the preceding committed
|
|
227
|
+
// generation during a partial build. Nodes owned by selectedFiles are never
|
|
228
|
+
// read through it: a changed file may delete or re-key a declaration, so its
|
|
229
|
+
// old nodes must disappear before the new checker walk begins.
|
|
230
|
+
baseNodes map[string]*Node
|
|
231
|
+
selectedFiles map[string]bool
|
|
232
|
+
|
|
233
|
+
// ExportedTargets records every declaration reached through a selected
|
|
234
|
+
// module's checker export table. A partial caller uses it to add re-exported
|
|
235
|
+
// owner files to the same invalidation transaction before publishing.
|
|
236
|
+
ExportedTargets map[string]bool
|
|
237
|
+
|
|
238
|
+
// ImplementationSources records every source that assigns a function body
|
|
239
|
+
// to a modeled declaration, including candidates after the first assignment
|
|
240
|
+
// that supplied the node's display span. A partial caller uses the complete
|
|
241
|
+
// set to rebuild a declaration owner and all competing assignments together.
|
|
242
|
+
ImplementationSources map[string]map[string]bool
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// lookupNode returns a node from this build, then from the preceding committed
|
|
246
|
+
// endpoint index when that node's file is not being replaced.
|
|
247
|
+
func (g *Graph) lookupNode(id string) (*Node, bool) {
|
|
248
|
+
if node, ok := g.Nodes[id]; ok {
|
|
249
|
+
return node, true
|
|
250
|
+
}
|
|
251
|
+
node, ok := g.baseNodes[id]
|
|
252
|
+
if !ok || g.selectedFiles[node.File] {
|
|
253
|
+
return nil, false
|
|
254
|
+
}
|
|
255
|
+
return node, true
|
|
216
256
|
}
|
|
217
257
|
|
|
218
258
|
// edgeKey identifies an edge by its two ends and the wire kind it will surface
|