ttsc 0.20.1 → 0.22.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/ttscserver/main.go +79 -3
- package/cmd/ttscserver/run_lsp_uses_native_plugin_manifest_source_test.go +96 -16
- package/driver/lsp.go +4 -0
- package/internal/lspserver/lsp_document_diagnostics_omits_cached_project_when_current_result_has_none_test.go +48 -0
- package/internal/lspserver/lsp_executable_project_input_change_requests_launcher_restart_test.go +98 -0
- package/internal/lspserver/lsp_external_watched_change_retains_program_test.go +53 -0
- package/internal/lspserver/lsp_hints_refresh_scheduled_by_editor_events_test.go +9 -0
- package/internal/lspserver/lsp_native_plugin_source.go +171 -55
- package/internal/lspserver/lsp_native_resident.go +110 -9
- package/internal/lspserver/lsp_plugin.go +6 -0
- package/internal/lspserver/lsp_project_context_transport_identity_test.go +43 -0
- package/internal/lspserver/lsp_project_diagnostics.go +235 -0
- package/internal/lspserver/lsp_project_diagnostics_capability_is_independent_test.go +67 -0
- package/internal/lspserver/lsp_project_diagnostics_program_input_overlap_refreshes_all_producers_test.go +78 -0
- package/internal/lspserver/lsp_project_diagnostics_refresh_accumulates_owner_freshness_test.go +65 -0
- package/internal/lspserver/lsp_project_diagnostics_refresh_keeps_each_producer_last_good_test.go +90 -0
- package/internal/lspserver/lsp_project_diagnostics_refreshes_only_input_owners_test.go +62 -0
- package/internal/lspserver/lsp_project_diagnostics_resident_unsupported_falls_back_test.go +123 -0
- package/internal/lspserver/lsp_project_diagnostics_resume_preserves_generation_test.go +52 -0
- package/internal/lspserver/lsp_project_diagnostics_translates_relative_client_project_test.go +90 -0
- package/internal/lspserver/lsp_project_input_ownership_resolves_declared_spellings_test.go +95 -0
- package/internal/lspserver/lsp_project_input_registration.go +504 -0
- package/internal/lspserver/lsp_project_input_registration_rechecks_selection_baseline_test.go +71 -0
- package/internal/lspserver/lsp_project_input_registration_reconciles_rejected_pending_request_test.go +268 -0
- package/internal/lspserver/lsp_project_input_registration_shapes_patterns_test.go +94 -0
- package/internal/lspserver/lsp_project_input_watchers_anchor_missing_paths_test.go +48 -0
- package/internal/lspserver/lsp_project_inputs.go +944 -0
- package/internal/lspserver/lsp_project_inputs_match_missing_exact_and_glob_test.go +129 -0
- package/internal/lspserver/lsp_project_inputs_reject_invalid_and_foreign_snapshots_test.go +110 -0
- package/internal/lspserver/lsp_proxy.go +591 -28
- package/internal/lspserver/lsp_reload_directories_compare_immediate_topology_test.go +188 -0
- package/internal/lspserver/lsp_reload_file_fingerprint_preserves_raw_symlink_target_test.go +47 -0
- package/internal/lspserver/lsp_resident_falls_back_when_serve_unsupported_test.go +1 -1
- package/internal/lspserver/lsp_resident_invalidation_broadcasts_program_input_overlap_test.go +60 -0
- package/internal/lspserver/lsp_resident_invalidation_targets_project_input_owners_test.go +51 -0
- package/internal/lspserver/lsp_same_transport_entries_are_deduplicated_test.go +66 -0
- package/lib/compiler/internal/project/readProjectConfig.d.ts +5 -3
- package/lib/compiler/internal/project/readProjectConfig.js +46 -14
- package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
- package/lib/compiler/internal/residentCheckProcess.d.ts +46 -0
- package/lib/compiler/internal/residentCheckProcess.js +227 -0
- package/lib/compiler/internal/residentCheckProcess.js.map +1 -0
- package/lib/compiler/internal/resolveEmittedJavaScript.d.ts +2 -1
- package/lib/compiler/internal/resolveEmittedJavaScript.js +35 -18
- package/lib/compiler/internal/resolveEmittedJavaScript.js.map +1 -1
- package/lib/compiler/internal/runBuild.d.ts +64 -1
- package/lib/compiler/internal/runBuild.js +484 -7
- package/lib/compiler/internal/runBuild.js.map +1 -1
- package/lib/compiler/internal/runSingleFileEmit.js +1 -0
- package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
- package/lib/flags/parser.js +13 -17
- package/lib/flags/parser.js.map +1 -1
- package/lib/flags/schema.d.ts +10 -3
- package/lib/flags/schema.js +99 -0
- package/lib/flags/schema.js.map +1 -1
- package/lib/internal/projectInputPathIdentity.d.ts +23 -0
- package/lib/internal/projectInputPathIdentity.js +222 -0
- package/lib/internal/projectInputPathIdentity.js.map +1 -0
- package/lib/launcher/internal/runTtsc.d.ts +17 -0
- package/lib/launcher/internal/runTtsc.js +121 -88
- package/lib/launcher/internal/runTtsc.js.map +1 -1
- package/lib/launcher/internal/runTtscserver.d.ts +13 -1
- package/lib/launcher/internal/runTtscserver.js +415 -45
- package/lib/launcher/internal/runTtscserver.js.map +1 -1
- package/lib/launcher/internal/singleFileOutput.d.ts +16 -0
- package/lib/launcher/internal/singleFileOutput.js +103 -0
- package/lib/launcher/internal/singleFileOutput.js.map +1 -0
- package/lib/launcher/internal/watchTopology.d.ts +214 -2
- package/lib/launcher/internal/watchTopology.js +1560 -41
- package/lib/launcher/internal/watchTopology.js.map +1 -1
- package/lib/plugin/internal/loadProjectPlugins.d.ts +1 -0
- package/lib/plugin/internal/loadProjectPlugins.js +5 -0
- package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
- package/lib/structures/ITtscPluginCapabilities.d.ts +31 -0
- package/lib/structures/internal/ITtscProjectInputSnapshot.d.ts +29 -0
- package/lib/structures/internal/ITtscProjectInputSnapshot.js +3 -0
- package/lib/structures/internal/ITtscProjectInputSnapshot.js.map +1 -0
- package/lib/structures/internal/TtscSingleFileEmitOptions.d.ts +5 -0
- package/package.json +8 -8
- package/src/compiler/internal/project/readProjectConfig.ts +62 -8
- package/src/compiler/internal/residentCheckProcess.ts +279 -0
- package/src/compiler/internal/resolveEmittedJavaScript.ts +43 -21
- package/src/compiler/internal/runBuild.ts +728 -12
- package/src/compiler/internal/runSingleFileEmit.ts +1 -0
- package/src/flags/parser.ts +17 -17
- package/src/flags/schema.ts +111 -3
- package/src/internal/projectInputPathIdentity.ts +273 -0
- package/src/launcher/internal/runTtsc.ts +141 -117
- package/src/launcher/internal/runTtscserver.ts +561 -54
- package/src/launcher/internal/singleFileOutput.ts +133 -0
- package/src/launcher/internal/watchTopology.ts +2008 -59
- package/src/plugin/internal/loadProjectPlugins.ts +8 -0
- package/src/structures/ITtscPluginCapabilities.ts +34 -0
- package/src/structures/internal/ITtscProjectInputSnapshot.ts +29 -0
- package/src/structures/internal/TtscSingleFileEmitOptions.ts +5 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
package lspserver
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"os"
|
|
6
|
+
"path/filepath"
|
|
7
|
+
"strings"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// projectDiagnosticRecord is one producer's last successful project
|
|
11
|
+
// publication. A failed refresh never updates the record, so another producer's
|
|
12
|
+
// success cannot erase diagnostics that were still valid before the failure.
|
|
13
|
+
type projectDiagnosticRecord struct {
|
|
14
|
+
generation uint64
|
|
15
|
+
publication LSPProjectDiagnostics
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type projectDiagnosticsRefreshResult struct {
|
|
19
|
+
publication *LSPProjectDiagnostics
|
|
20
|
+
refreshed map[string]struct{}
|
|
21
|
+
complete bool
|
|
22
|
+
selected int
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ProjectDiagnostics evaluates project rules without requiring an open
|
|
26
|
+
// document and returns the latest successful publication from every capable
|
|
27
|
+
// sidecar.
|
|
28
|
+
func (s *NativePluginSource) ProjectDiagnostics() *LSPProjectDiagnostics {
|
|
29
|
+
return s.ProjectDiagnosticsForOwners(nil).publication
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ProjectDiagnosticsForOwners refreshes only the diagnostics-capable producers
|
|
33
|
+
// named by owners. A nil owner list preserves the legacy all-producer request;
|
|
34
|
+
// an empty non-nil list is a successful no-op.
|
|
35
|
+
func (s *NativePluginSource) ProjectDiagnosticsForOwners(
|
|
36
|
+
owners []string,
|
|
37
|
+
) projectDiagnosticsRefreshResult {
|
|
38
|
+
if s == nil {
|
|
39
|
+
return projectDiagnosticsRefreshResult{}
|
|
40
|
+
}
|
|
41
|
+
selectedOwners := map[string]struct{}{}
|
|
42
|
+
if owners != nil {
|
|
43
|
+
for _, owner := range owners {
|
|
44
|
+
selectedOwners[owner] = struct{}{}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
generation := s.projectDiagnosticsSequence.Add(1)
|
|
48
|
+
result := projectDiagnosticsRefreshResult{
|
|
49
|
+
complete: true,
|
|
50
|
+
refreshed: map[string]struct{}{},
|
|
51
|
+
}
|
|
52
|
+
for _, plugin := range selectPluginTransports(
|
|
53
|
+
s.plugins,
|
|
54
|
+
func(plugin NativeLSPPluginEntry) bool {
|
|
55
|
+
return plugin.ProjectDiagnostics
|
|
56
|
+
},
|
|
57
|
+
s.projectContextJSON,
|
|
58
|
+
) {
|
|
59
|
+
key := pluginKey(plugin, s.projectContextJSON)
|
|
60
|
+
if owners != nil {
|
|
61
|
+
if _, selected := selectedOwners[key]; !selected {
|
|
62
|
+
continue
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
result.selected++
|
|
66
|
+
body, err := s.run(plugin, serveVerbProjectDiagnostics)
|
|
67
|
+
if err != nil {
|
|
68
|
+
s.log("%v", err)
|
|
69
|
+
result.complete = false
|
|
70
|
+
continue
|
|
71
|
+
}
|
|
72
|
+
var publication *LSPProjectDiagnostics
|
|
73
|
+
if err := json.Unmarshal(body, &publication); err != nil {
|
|
74
|
+
s.log(
|
|
75
|
+
"ttscserver: %s lsp-project-diagnostics returned invalid JSON: %v",
|
|
76
|
+
pluginLabel(plugin),
|
|
77
|
+
err,
|
|
78
|
+
)
|
|
79
|
+
result.complete = false
|
|
80
|
+
continue
|
|
81
|
+
}
|
|
82
|
+
if publication == nil || publication.URI == "" {
|
|
83
|
+
result.complete = false
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
s.storeProjectDiagnostics(plugin, generation, publication)
|
|
87
|
+
result.refreshed[key] = struct{}{}
|
|
88
|
+
}
|
|
89
|
+
result.publication = s.projectDiagnosticsSnapshot()
|
|
90
|
+
return result
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// storeProjectDiagnostics replaces one producer's last-good publication. A
|
|
94
|
+
// successful empty diagnostics array clears that producer while leaving every
|
|
95
|
+
// other producer unchanged.
|
|
96
|
+
func (s *NativePluginSource) storeProjectDiagnostics(
|
|
97
|
+
plugin NativeLSPPluginEntry,
|
|
98
|
+
generation uint64,
|
|
99
|
+
publication *LSPProjectDiagnostics,
|
|
100
|
+
) {
|
|
101
|
+
if publication == nil || publication.URI == "" {
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
key := pluginKey(plugin, s.projectContextJSON)
|
|
105
|
+
copied := copyProjectDiagnostics(publication)
|
|
106
|
+
s.projectDiagnosticsMu.Lock()
|
|
107
|
+
defer s.projectDiagnosticsMu.Unlock()
|
|
108
|
+
if existing, ok := s.pluginProjectDiagnostics[key]; ok &&
|
|
109
|
+
generation < existing.generation {
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
if s.pluginProjectDiagnostics == nil {
|
|
113
|
+
s.pluginProjectDiagnostics = map[string]projectDiagnosticRecord{}
|
|
114
|
+
}
|
|
115
|
+
s.pluginProjectDiagnostics[key] = projectDiagnosticRecord{
|
|
116
|
+
generation: generation,
|
|
117
|
+
publication: *copied,
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// clientProjectURI restates a producer's project URI in the spelling the client
|
|
122
|
+
// opened the project under.
|
|
123
|
+
//
|
|
124
|
+
// A sidecar is deliberately given the project's physical paths, because that is
|
|
125
|
+
// what makes its compiler, its caches, and its own resolution agree with every
|
|
126
|
+
// other consumer. The editor never saw those paths. It opened the project under
|
|
127
|
+
// whatever spelling its workspace uses — through a symlink, a mapped drive, a
|
|
128
|
+
// case that differs — and a publication addressed to any other spelling is
|
|
129
|
+
// invisible to it no matter how correct the diagnostics inside are. So the one
|
|
130
|
+
// place the two worlds meet translates back.
|
|
131
|
+
func (s *NativePluginSource) clientProjectURI(uri string) string {
|
|
132
|
+
if s == nil || uri == "" {
|
|
133
|
+
return uri
|
|
134
|
+
}
|
|
135
|
+
client, key := s.clientProjectIdentity()
|
|
136
|
+
if client == "" {
|
|
137
|
+
return uri
|
|
138
|
+
}
|
|
139
|
+
location, ok := filePathFromURI(uri)
|
|
140
|
+
if !ok {
|
|
141
|
+
return uri
|
|
142
|
+
}
|
|
143
|
+
if projectInputPathKey(realProjectInputPath(location)) != key {
|
|
144
|
+
return uri
|
|
145
|
+
}
|
|
146
|
+
return projectInputFileURI(client)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// clientProjectIdentity is the project as the client named it, absolute, with
|
|
150
|
+
// the key a producer's URI is compared against. Empty when the client named no
|
|
151
|
+
// project at all, which is the one case where there is nothing to translate to.
|
|
152
|
+
func (s *NativePluginSource) clientProjectIdentity() (string, string) {
|
|
153
|
+
s.clientProjectOnce.Do(func() {
|
|
154
|
+
client := projectInputFilesystemPath(strings.TrimSpace(s.clientTsconfig))
|
|
155
|
+
if client == "" {
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
if !filepath.IsAbs(client) {
|
|
159
|
+
// The client names its project relative to the directory it asked this
|
|
160
|
+
// host to work in. When it named none, the host was started from that
|
|
161
|
+
// directory and inherited it, so the process directory is not a guess
|
|
162
|
+
// here — it is the same answer by another route.
|
|
163
|
+
base := projectInputFilesystemPath(strings.TrimSpace(s.clientCwd))
|
|
164
|
+
if base == "" {
|
|
165
|
+
working, err := os.Getwd()
|
|
166
|
+
if err != nil {
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
base = working
|
|
170
|
+
}
|
|
171
|
+
absolute, err := filepath.Abs(base)
|
|
172
|
+
if err != nil {
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
client = filepath.Join(absolute, client)
|
|
176
|
+
}
|
|
177
|
+
if !filepath.IsAbs(client) {
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
s.clientProject = client
|
|
181
|
+
s.clientProjectKey = projectInputPathKey(realProjectInputPath(client))
|
|
182
|
+
})
|
|
183
|
+
return s.clientProject, s.clientProjectKey
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// projectDiagnosticsSnapshot concatenates producer publications in manifest
|
|
187
|
+
// order. A project has one config URI, so a producer that reports a different
|
|
188
|
+
// URI is excluded and logged rather than replacing the other producers.
|
|
189
|
+
func (s *NativePluginSource) projectDiagnosticsSnapshot() *LSPProjectDiagnostics {
|
|
190
|
+
s.projectDiagnosticsMu.RLock()
|
|
191
|
+
defer s.projectDiagnosticsMu.RUnlock()
|
|
192
|
+
var out *LSPProjectDiagnostics
|
|
193
|
+
for _, plugin := range selectPluginTransports(
|
|
194
|
+
s.plugins,
|
|
195
|
+
nil,
|
|
196
|
+
s.projectContextJSON,
|
|
197
|
+
) {
|
|
198
|
+
key := pluginKey(plugin, s.projectContextJSON)
|
|
199
|
+
record, ok := s.pluginProjectDiagnostics[key]
|
|
200
|
+
if !ok {
|
|
201
|
+
continue
|
|
202
|
+
}
|
|
203
|
+
publication := record.publication
|
|
204
|
+
publication.URI = s.clientProjectURI(publication.URI)
|
|
205
|
+
if out == nil {
|
|
206
|
+
out = copyProjectDiagnostics(&publication)
|
|
207
|
+
continue
|
|
208
|
+
}
|
|
209
|
+
if out.URI != publication.URI {
|
|
210
|
+
s.log(
|
|
211
|
+
"ttscserver: %s lsp-project-diagnostics returned URI %q, want %q",
|
|
212
|
+
pluginLabel(plugin),
|
|
213
|
+
publication.URI,
|
|
214
|
+
out.URI,
|
|
215
|
+
)
|
|
216
|
+
continue
|
|
217
|
+
}
|
|
218
|
+
out.Diagnostics = append(out.Diagnostics, publication.Diagnostics...)
|
|
219
|
+
}
|
|
220
|
+
return out
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
func copyProjectDiagnostics(
|
|
224
|
+
publication *LSPProjectDiagnostics,
|
|
225
|
+
) *LSPProjectDiagnostics {
|
|
226
|
+
if publication == nil {
|
|
227
|
+
return nil
|
|
228
|
+
}
|
|
229
|
+
copied := *publication
|
|
230
|
+
copied.Diagnostics = append(
|
|
231
|
+
[]LSPDiagnostic(nil),
|
|
232
|
+
publication.Diagnostics...,
|
|
233
|
+
)
|
|
234
|
+
return &copied
|
|
235
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
package lspserver
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"strings"
|
|
6
|
+
"testing"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
// TestLSPProjectDiagnosticsCapabilityIsIndependent verifies the standalone
|
|
10
|
+
// diagnostic command is not inferred from project-input topology support.
|
|
11
|
+
//
|
|
12
|
+
// A third-party sidecar may implement `project-inputs` without implementing
|
|
13
|
+
// `lsp-project-diagnostics`, while a staged diagnostic sidecar may expose the
|
|
14
|
+
// inverse combination. Probing either command under the other capability makes
|
|
15
|
+
// strict sidecars fail an undocumented command.
|
|
16
|
+
//
|
|
17
|
+
// 1. Configure a topology-only plugin and a diagnostics-only plugin.
|
|
18
|
+
// 2. Seed the diagnostics producer, then make its next refresh fail to launch.
|
|
19
|
+
// 3. Assert only the explicitly capable producer was invoked.
|
|
20
|
+
// 4. Assert its last-good publication remains available after the failure.
|
|
21
|
+
func TestLSPProjectDiagnosticsCapabilityIsIndependent(t *testing.T) {
|
|
22
|
+
topologyOnly := NativeLSPPluginEntry{
|
|
23
|
+
Binary: "ttsc-no-such-topology-only-sidecar",
|
|
24
|
+
Name: "@ttsc/topology-only",
|
|
25
|
+
ProjectInputs: true,
|
|
26
|
+
}
|
|
27
|
+
diagnosticsOnly := NativeLSPPluginEntry{
|
|
28
|
+
Binary: "ttsc-no-such-diagnostics-only-sidecar",
|
|
29
|
+
Name: "@ttsc/diagnostics-only",
|
|
30
|
+
ProjectDiagnostics: true,
|
|
31
|
+
}
|
|
32
|
+
var log bytes.Buffer
|
|
33
|
+
source := &NativePluginSource{
|
|
34
|
+
err: &log,
|
|
35
|
+
plugins: []NativeLSPPluginEntry{topologyOnly, diagnosticsOnly},
|
|
36
|
+
}
|
|
37
|
+
source.storeProjectDiagnostics(
|
|
38
|
+
diagnosticsOnly,
|
|
39
|
+
1,
|
|
40
|
+
&LSPProjectDiagnostics{
|
|
41
|
+
URI: "file:///project/tsconfig.json",
|
|
42
|
+
Diagnostics: []LSPDiagnostic{{
|
|
43
|
+
Code: "last-good",
|
|
44
|
+
Message: "last-good",
|
|
45
|
+
}},
|
|
46
|
+
},
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
got := source.ProjectDiagnostics()
|
|
50
|
+
|
|
51
|
+
if strings.Contains(log.String(), topologyOnly.Name) {
|
|
52
|
+
t.Fatalf(
|
|
53
|
+
"projectInputs incorrectly enabled lsp-project-diagnostics:\n%s",
|
|
54
|
+
log.String(),
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
if !strings.Contains(log.String(), diagnosticsOnly.Name) {
|
|
58
|
+
t.Fatalf(
|
|
59
|
+
"projectDiagnostics did not enable its direct command:\n%s",
|
|
60
|
+
log.String(),
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
if got == nil || len(got.Diagnostics) != 1 ||
|
|
64
|
+
got.Diagnostics[0].Code != "last-good" {
|
|
65
|
+
t.Fatalf("failed capable producer lost its last-good publication: %#v", got)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
package lspserver
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"strings"
|
|
7
|
+
"testing"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// TestLSPProjectDiagnosticsProgramInputOverlapRefreshesAllProducers verifies a
|
|
11
|
+
// declared input that can also belong to the Program widens diagnostic scope.
|
|
12
|
+
//
|
|
13
|
+
// Only the first producer declares the path, but both project-rule producers
|
|
14
|
+
// observe the shared Program. TypeScript and resolveJsonModule JSON edits must
|
|
15
|
+
// therefore refresh both, while a path that can only be data keeps the
|
|
16
|
+
// owner-scoped behavior pinned by
|
|
17
|
+
// lsp_project_diagnostics_refreshes_only_input_owners_test.go.
|
|
18
|
+
//
|
|
19
|
+
// 1. Declare one shared-Program path for the first producer only.
|
|
20
|
+
// 2. Resolve its owner scope and widen it as a watched Program input.
|
|
21
|
+
// 3. Assert the scope became all-producer.
|
|
22
|
+
// 4. Refresh and assert both producers were invoked.
|
|
23
|
+
func TestLSPProjectDiagnosticsProgramInputOverlapRefreshesAllProducers(
|
|
24
|
+
t *testing.T,
|
|
25
|
+
) {
|
|
26
|
+
for _, name := range []string{"shared.ts", "shared.json"} {
|
|
27
|
+
t.Run(name, func(t *testing.T) {
|
|
28
|
+
root := t.TempDir()
|
|
29
|
+
first := NativeLSPPluginEntry{
|
|
30
|
+
Binary: "ttsc-no-such-first-program-sidecar",
|
|
31
|
+
Name: "@ttsc/first-program",
|
|
32
|
+
ProjectDiagnostics: true,
|
|
33
|
+
ProjectInputs: true,
|
|
34
|
+
}
|
|
35
|
+
second := NativeLSPPluginEntry{
|
|
36
|
+
Binary: "ttsc-no-such-second-program-sidecar",
|
|
37
|
+
Name: "@ttsc/second-program",
|
|
38
|
+
ProjectDiagnostics: true,
|
|
39
|
+
ProjectInputs: true,
|
|
40
|
+
}
|
|
41
|
+
var log bytes.Buffer
|
|
42
|
+
source := &NativePluginSource{
|
|
43
|
+
err: &log,
|
|
44
|
+
plugins: []NativeLSPPluginEntry{first, second},
|
|
45
|
+
}
|
|
46
|
+
input := filepath.Join(root, "src", name)
|
|
47
|
+
source.storeProjectInputs(first, 1, LSPProjectInputSnapshot{
|
|
48
|
+
Root: filepath.ToSlash(root),
|
|
49
|
+
Files: []string{filepath.ToSlash(input)},
|
|
50
|
+
})
|
|
51
|
+
source.storeProjectInputs(second, 1, LSPProjectInputSnapshot{
|
|
52
|
+
Root: filepath.ToSlash(root),
|
|
53
|
+
Files: []string{
|
|
54
|
+
filepath.ToSlash(filepath.Join(root, "docs", "second.md")),
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
uri := testFileURI(input)
|
|
58
|
+
scope, matched := (&Proxy{source: source}).projectInputOwnerScope(uri)
|
|
59
|
+
if !matched || scope.all || len(scope.owners) != 1 {
|
|
60
|
+
t.Fatalf("declared owner scope = %#v, matched %v", scope, matched)
|
|
61
|
+
}
|
|
62
|
+
scope = projectDiagnosticScopeForWatchedInput(uri, scope)
|
|
63
|
+
if !scope.all {
|
|
64
|
+
t.Fatalf("Program input scope remained owner-only: %#v", scope)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
result := source.ProjectDiagnosticsForOwners(nil)
|
|
68
|
+
|
|
69
|
+
if result.selected != 2 || result.complete {
|
|
70
|
+
t.Fatalf("all-producer failed refresh = %#v", result)
|
|
71
|
+
}
|
|
72
|
+
if !strings.Contains(log.String(), first.Name) ||
|
|
73
|
+
!strings.Contains(log.String(), second.Name) {
|
|
74
|
+
t.Fatalf("Program input did not invoke both producers:\n%s", log.String())
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
}
|
package/internal/lspserver/lsp_project_diagnostics_refresh_accumulates_owner_freshness_test.go
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
package lspserver
|
|
2
|
+
|
|
3
|
+
import "testing"
|
|
4
|
+
|
|
5
|
+
// TestProjectDiagnosticsRefreshAccumulatesOwnerFreshness verifies separate
|
|
6
|
+
// successful refreshes can jointly satisfy one affected-producer generation.
|
|
7
|
+
func TestProjectDiagnosticsRefreshAccumulatesOwnerFreshness(t *testing.T) {
|
|
8
|
+
proxy := &Proxy{}
|
|
9
|
+
proxy.projectDiagnosticRefreshPending = true
|
|
10
|
+
proxy.pendingProjectDiagnosticGeneration = 2
|
|
11
|
+
proxy.pendingProjectDiagnosticOwners = map[string]struct{}{
|
|
12
|
+
"alpha": {},
|
|
13
|
+
"beta": {},
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if proxy.recordPendingProjectDiagnosticOwnersRefreshed(
|
|
17
|
+
1,
|
|
18
|
+
map[string]struct{}{"beta": {}},
|
|
19
|
+
) {
|
|
20
|
+
t.Fatal("stale producer refresh completed the newer generation")
|
|
21
|
+
}
|
|
22
|
+
if _, pending := proxy.pendingProjectDiagnosticOwners["beta"]; !pending {
|
|
23
|
+
t.Fatal("stale producer refresh removed a newer pending owner")
|
|
24
|
+
}
|
|
25
|
+
if proxy.recordPendingProjectDiagnosticOwnersRefreshed(
|
|
26
|
+
2,
|
|
27
|
+
map[string]struct{}{"alpha": {}},
|
|
28
|
+
) {
|
|
29
|
+
t.Fatal("one producer completed a two-producer refresh")
|
|
30
|
+
}
|
|
31
|
+
if _, pending := proxy.pendingProjectDiagnosticOwners["alpha"]; pending {
|
|
32
|
+
t.Fatal("successful producer remained pending")
|
|
33
|
+
}
|
|
34
|
+
if _, pending := proxy.pendingProjectDiagnosticOwners["beta"]; !pending {
|
|
35
|
+
t.Fatal("failed producer was removed from pending scope")
|
|
36
|
+
}
|
|
37
|
+
if !proxy.recordPendingProjectDiagnosticOwnersRefreshed(
|
|
38
|
+
2,
|
|
39
|
+
map[string]struct{}{"beta": {}},
|
|
40
|
+
) {
|
|
41
|
+
t.Fatal("separate producer successes did not complete the generation")
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// A newer watched event can arrive after record reports completion but
|
|
45
|
+
// before its caller clears the old generation. The newer scope must survive
|
|
46
|
+
// that rejected completion unchanged.
|
|
47
|
+
proxy.projectDiagnosticGeneration = 3
|
|
48
|
+
proxy.pendingProjectDiagnosticGeneration = 3
|
|
49
|
+
proxy.projectDiagnosticRefreshPending = true
|
|
50
|
+
proxy.pendingProjectDiagnosticOwners = map[string]struct{}{"alpha": {}}
|
|
51
|
+
proxy.completePendingProjectDiagnosticRefresh(2)
|
|
52
|
+
proxy.projectRefreshMu.Lock()
|
|
53
|
+
defer proxy.projectRefreshMu.Unlock()
|
|
54
|
+
if !proxy.projectDiagnosticRefreshPending ||
|
|
55
|
+
proxy.pendingProjectDiagnosticGeneration != 3 {
|
|
56
|
+
t.Fatalf(
|
|
57
|
+
"newer pending generation was cleared: pending %v, generation %d",
|
|
58
|
+
proxy.projectDiagnosticRefreshPending,
|
|
59
|
+
proxy.pendingProjectDiagnosticGeneration,
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
if _, pending := proxy.pendingProjectDiagnosticOwners["alpha"]; !pending {
|
|
63
|
+
t.Fatal("newer pending owner was cleared by the prior generation")
|
|
64
|
+
}
|
|
65
|
+
}
|
package/internal/lspserver/lsp_project_diagnostics_refresh_keeps_each_producer_last_good_test.go
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
package lspserver
|
|
2
|
+
|
|
3
|
+
import "testing"
|
|
4
|
+
|
|
5
|
+
// TestLSPProjectDiagnosticsRefreshKeepsEachProducerLastGood verifies partial
|
|
6
|
+
// refreshes update only the producers that answered successfully.
|
|
7
|
+
//
|
|
8
|
+
// Project diagnostics are one merged config-URI publication, but their
|
|
9
|
+
// sidecars fail independently. Rebuilding that publication from only the
|
|
10
|
+
// current call's successes would erase a failed producer's prior findings. An
|
|
11
|
+
// empty successful answer is different: it deliberately clears that producer.
|
|
12
|
+
//
|
|
13
|
+
// 1. Seed two producers with successful publications.
|
|
14
|
+
// 2. Refresh only the first and assert the second producer's last-good remains.
|
|
15
|
+
// 3. Clear the second with a successful empty publication.
|
|
16
|
+
// 4. Recover the second and assert manifest-order aggregation is restored.
|
|
17
|
+
func TestLSPProjectDiagnosticsRefreshKeepsEachProducerLastGood(t *testing.T) {
|
|
18
|
+
first := NativeLSPPluginEntry{Binary: "first", Name: "@ttsc/first"}
|
|
19
|
+
second := NativeLSPPluginEntry{Binary: "second", Name: "@ttsc/second"}
|
|
20
|
+
source := &NativePluginSource{
|
|
21
|
+
plugins: []NativeLSPPluginEntry{first, second},
|
|
22
|
+
}
|
|
23
|
+
publication := func(codes ...string) *LSPProjectDiagnostics {
|
|
24
|
+
diagnostics := make([]LSPDiagnostic, 0, len(codes))
|
|
25
|
+
for _, code := range codes {
|
|
26
|
+
diagnostics = append(diagnostics, LSPDiagnostic{
|
|
27
|
+
Code: code,
|
|
28
|
+
Message: code,
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
return &LSPProjectDiagnostics{
|
|
32
|
+
URI: "file:///project/tsconfig.json",
|
|
33
|
+
Diagnostics: diagnostics,
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
codes := func(got *LSPProjectDiagnostics) []string {
|
|
37
|
+
if got == nil {
|
|
38
|
+
return nil
|
|
39
|
+
}
|
|
40
|
+
out := make([]string, 0, len(got.Diagnostics))
|
|
41
|
+
for _, diagnostic := range got.Diagnostics {
|
|
42
|
+
out = append(out, diagnostic.Code.(string))
|
|
43
|
+
}
|
|
44
|
+
return out
|
|
45
|
+
}
|
|
46
|
+
assertCodes := func(label string, got *LSPProjectDiagnostics, want ...string) {
|
|
47
|
+
t.Helper()
|
|
48
|
+
actual := codes(got)
|
|
49
|
+
if len(actual) != len(want) {
|
|
50
|
+
t.Fatalf("%s codes = %v, want %v", label, actual, want)
|
|
51
|
+
}
|
|
52
|
+
for index := range want {
|
|
53
|
+
if actual[index] != want[index] {
|
|
54
|
+
t.Fatalf("%s codes = %v, want %v", label, actual, want)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
source.storeProjectDiagnostics(first, 1, publication("first-old"))
|
|
60
|
+
source.storeProjectDiagnostics(second, 1, publication("second-old"))
|
|
61
|
+
assertCodes(
|
|
62
|
+
"initial",
|
|
63
|
+
source.projectDiagnosticsSnapshot(),
|
|
64
|
+
"first-old",
|
|
65
|
+
"second-old",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
source.storeProjectDiagnostics(first, 2, publication("first-new"))
|
|
69
|
+
assertCodes(
|
|
70
|
+
"partial failure",
|
|
71
|
+
source.projectDiagnosticsSnapshot(),
|
|
72
|
+
"first-new",
|
|
73
|
+
"second-old",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
source.storeProjectDiagnostics(second, 3, publication())
|
|
77
|
+
assertCodes(
|
|
78
|
+
"successful clear",
|
|
79
|
+
source.projectDiagnosticsSnapshot(),
|
|
80
|
+
"first-new",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
source.storeProjectDiagnostics(second, 4, publication("second-new"))
|
|
84
|
+
assertCodes(
|
|
85
|
+
"recovery",
|
|
86
|
+
source.projectDiagnosticsSnapshot(),
|
|
87
|
+
"first-new",
|
|
88
|
+
"second-new",
|
|
89
|
+
)
|
|
90
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
package lspserver
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"strings"
|
|
7
|
+
"testing"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// TestLSPProjectDiagnosticsRefreshesOnlyInputOwners verifies flattened client
|
|
11
|
+
// watch registration does not erase contributor ownership.
|
|
12
|
+
//
|
|
13
|
+
// 1. Give two diagnostics-capable producers disjoint exact input snapshots.
|
|
14
|
+
// 2. Resolve the first input's owner from the merged source.
|
|
15
|
+
// 3. Refresh that owner and assert the unrelated producer is never invoked.
|
|
16
|
+
func TestLSPProjectDiagnosticsRefreshesOnlyInputOwners(t *testing.T) {
|
|
17
|
+
root := t.TempDir()
|
|
18
|
+
first := NativeLSPPluginEntry{
|
|
19
|
+
Binary: "ttsc-no-such-first-owner-sidecar",
|
|
20
|
+
Name: "@ttsc/first",
|
|
21
|
+
ProjectDiagnostics: true,
|
|
22
|
+
ProjectInputs: true,
|
|
23
|
+
}
|
|
24
|
+
second := NativeLSPPluginEntry{
|
|
25
|
+
Binary: "ttsc-no-such-second-owner-sidecar",
|
|
26
|
+
Name: "@ttsc/second",
|
|
27
|
+
ProjectDiagnostics: true,
|
|
28
|
+
ProjectInputs: true,
|
|
29
|
+
}
|
|
30
|
+
var log bytes.Buffer
|
|
31
|
+
source := &NativePluginSource{
|
|
32
|
+
err: &log,
|
|
33
|
+
plugins: []NativeLSPPluginEntry{first, second},
|
|
34
|
+
}
|
|
35
|
+
firstInput := filepath.Join(root, "docs", "first.md")
|
|
36
|
+
source.storeProjectInputs(first, 1, LSPProjectInputSnapshot{
|
|
37
|
+
Root: filepath.ToSlash(root),
|
|
38
|
+
Files: []string{filepath.ToSlash(firstInput)},
|
|
39
|
+
})
|
|
40
|
+
source.storeProjectInputs(second, 1, LSPProjectInputSnapshot{
|
|
41
|
+
Root: filepath.ToSlash(root),
|
|
42
|
+
Files: []string{
|
|
43
|
+
filepath.ToSlash(filepath.Join(root, "docs", "second.md")),
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
owners := source.ProjectInputOwnersForURI(testFileURI(firstInput))
|
|
47
|
+
if len(owners) != 1 || owners[0] != pluginKey(first) {
|
|
48
|
+
t.Fatalf("first input owners = %#v", owners)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
result := source.ProjectDiagnosticsForOwners(owners)
|
|
52
|
+
|
|
53
|
+
if result.selected != 1 || result.complete {
|
|
54
|
+
t.Fatalf("scoped failed refresh = %#v", result)
|
|
55
|
+
}
|
|
56
|
+
if !strings.Contains(log.String(), first.Name) {
|
|
57
|
+
t.Fatalf("owned producer was not invoked:\n%s", log.String())
|
|
58
|
+
}
|
|
59
|
+
if strings.Contains(log.String(), second.Name) {
|
|
60
|
+
t.Fatalf("unrelated producer was invoked:\n%s", log.String())
|
|
61
|
+
}
|
|
62
|
+
}
|