ttsc 0.21.0 → 0.23.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/go.mod +1 -1
- 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_launcher_reload_directory_fingerprint_parity_windows_test.go +94 -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_glob_path_other.go +16 -0
- package/internal/lspserver/lsp_project_input_glob_path_windows.go +36 -0
- package/internal/lspserver/lsp_project_input_ownership_resolves_declared_spellings_test.go +95 -0
- package/internal/lspserver/lsp_project_input_path_key_other.go +17 -0
- package/internal/lspserver/lsp_project_input_path_key_windows.go +228 -0
- package/internal/lspserver/lsp_project_input_path_key_windows_test.go +460 -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 +1012 -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_directories_separate_glob_territory_test.go +181 -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 +31 -0
- package/lib/internal/projectInputPathIdentity.js +276 -0
- package/lib/internal/projectInputPathIdentity.js.map +1 -0
- package/lib/internal/windowsDirectoryCaseSensitivity.d.ts +10 -0
- package/lib/internal/windowsDirectoryCaseSensitivity.js +34 -0
- package/lib/internal/windowsDirectoryCaseSensitivity.js.map +1 -0
- package/lib/launcher/internal/runTtsc.d.ts +17 -0
- package/lib/launcher/internal/runTtsc.js +126 -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 +468 -47
- package/lib/launcher/internal/runTtscserver.js.map +1 -1
- package/lib/launcher/internal/runTtsx.js +61 -7
- package/lib/launcher/internal/runTtsx.js.map +1 -1
- package/lib/launcher/internal/runtimeHooks.d.ts +6 -5
- package/lib/launcher/internal/runtimeHooks.js +7 -15
- package/lib/launcher/internal/runtimeHooks.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 +288 -2
- package/lib/launcher/internal/watchTopology.js +1921 -54
- package/lib/launcher/internal/watchTopology.js.map +1 -1
- package/lib/plugin/internal/descriptorProcessFailure.d.ts +32 -0
- package/lib/plugin/internal/descriptorProcessFailure.js +73 -0
- package/lib/plugin/internal/descriptorProcessFailure.js.map +1 -0
- package/lib/plugin/internal/loadProjectPlugins.d.ts +1 -0
- package/lib/plugin/internal/loadProjectPlugins.js +34 -8
- 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 +12 -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 +112 -3
- package/src/internal/projectInputPathIdentity.ts +377 -0
- package/src/internal/windowsDirectoryCaseSensitivity.ts +33 -0
- package/src/launcher/internal/runTtsc.ts +148 -117
- package/src/launcher/internal/runTtscserver.ts +633 -56
- package/src/launcher/internal/runTtsx.ts +87 -10
- package/src/launcher/internal/runtimeHooks.ts +16 -16
- package/src/launcher/internal/singleFileOutput.ts +133 -0
- package/src/launcher/internal/watchTopology.ts +2494 -74
- package/src/plugin/internal/descriptorProcessFailure.ts +100 -0
- package/src/plugin/internal/loadProjectPlugins.ts +49 -8
- package/src/structures/ITtscPluginCapabilities.ts +34 -0
- package/src/structures/internal/ITtscProjectInputSnapshot.ts +29 -0
- package/src/structures/internal/TtscSingleFileEmitOptions.ts +5 -0
- package/tools/shim_audit/baseline.json +0 -9
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
package lspserver
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"os"
|
|
6
|
+
"os/exec"
|
|
7
|
+
"path/filepath"
|
|
8
|
+
"runtime"
|
|
9
|
+
"strings"
|
|
10
|
+
"testing"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
// TestLSPProjectDiagnosticsResidentUnsupportedFallsBack verifies a staged
|
|
14
|
+
// sidecar can serve its advertised direct command through an older daemon.
|
|
15
|
+
//
|
|
16
|
+
// `lsp-serve` and its accepted request verbs can ship at different times. Once
|
|
17
|
+
// a resident answers another verb, rejecting `lsp-project-diagnostics` is not a
|
|
18
|
+
// transport failure and used to suppress the working one-shot command.
|
|
19
|
+
//
|
|
20
|
+
// 1. Build a sidecar whose daemon rejects only project diagnostics.
|
|
21
|
+
// 2. Advertise the direct project-diagnostic capability.
|
|
22
|
+
// 3. Request a publication and assert the one-shot command answers it.
|
|
23
|
+
// 4. Assert both resident rejection and direct fallback occurred once.
|
|
24
|
+
func TestLSPProjectDiagnosticsResidentUnsupportedFallsBack(t *testing.T) {
|
|
25
|
+
dir := t.TempDir()
|
|
26
|
+
sidecarSource := filepath.Join(dir, "sidecar.go")
|
|
27
|
+
if err := os.WriteFile(
|
|
28
|
+
sidecarSource,
|
|
29
|
+
[]byte(projectDiagnosticsFallbackSidecarSource),
|
|
30
|
+
0644,
|
|
31
|
+
); err != nil {
|
|
32
|
+
t.Fatal(err)
|
|
33
|
+
}
|
|
34
|
+
binary := filepath.Join(dir, "sidecar")
|
|
35
|
+
if runtime.GOOS == "windows" {
|
|
36
|
+
binary += ".exe"
|
|
37
|
+
}
|
|
38
|
+
build := exec.Command("go", "build", "-o", binary, sidecarSource)
|
|
39
|
+
if output, err := build.CombinedOutput(); err != nil {
|
|
40
|
+
t.Fatalf("go build sidecar failed: %v\n%s", err, output)
|
|
41
|
+
}
|
|
42
|
+
logPath := filepath.Join(dir, "calls.log")
|
|
43
|
+
t.Setenv("TTSC_PROJECT_DIAGNOSTICS_FALLBACK_LOG", logPath)
|
|
44
|
+
plugin := NativeLSPPluginEntry{
|
|
45
|
+
Binary: binary,
|
|
46
|
+
Name: "@ttsc/staged",
|
|
47
|
+
ProjectDiagnostics: true,
|
|
48
|
+
}
|
|
49
|
+
source := &NativePluginSource{
|
|
50
|
+
cwd: dir,
|
|
51
|
+
err: &bytes.Buffer{},
|
|
52
|
+
plugins: []NativeLSPPluginEntry{plugin},
|
|
53
|
+
pluginsJSON: "[]",
|
|
54
|
+
tsconfig: filepath.Join(dir, "tsconfig.json"),
|
|
55
|
+
}
|
|
56
|
+
t.Cleanup(source.shutdownResidents)
|
|
57
|
+
|
|
58
|
+
got := source.ProjectDiagnostics()
|
|
59
|
+
|
|
60
|
+
if got == nil || got.URI != "file:///project/tsconfig.json" ||
|
|
61
|
+
len(got.Diagnostics) != 1 ||
|
|
62
|
+
got.Diagnostics[0].Code != "direct" {
|
|
63
|
+
t.Fatalf("direct fallback publication = %#v", got)
|
|
64
|
+
}
|
|
65
|
+
calls, err := os.ReadFile(logPath)
|
|
66
|
+
if err != nil {
|
|
67
|
+
t.Fatal(err)
|
|
68
|
+
}
|
|
69
|
+
log := string(calls)
|
|
70
|
+
if strings.Count(log, "resident lsp-project-diagnostics") != 1 {
|
|
71
|
+
t.Fatalf("resident attempt count is wrong:\n%s", log)
|
|
72
|
+
}
|
|
73
|
+
if strings.Count(log, "direct lsp-project-diagnostics") != 1 {
|
|
74
|
+
t.Fatalf("direct fallback count is wrong:\n%s", log)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const projectDiagnosticsFallbackSidecarSource = `package main
|
|
79
|
+
|
|
80
|
+
import (
|
|
81
|
+
"bufio"
|
|
82
|
+
"encoding/json"
|
|
83
|
+
"fmt"
|
|
84
|
+
"os"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
type request struct {
|
|
88
|
+
Verb string ` + "`json:\"verb\"`" + `
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
func logCall(message string) {
|
|
92
|
+
path := os.Getenv("TTSC_PROJECT_DIAGNOSTICS_FALLBACK_LOG")
|
|
93
|
+
file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
|
94
|
+
if err == nil {
|
|
95
|
+
_, _ = fmt.Fprintln(file, message)
|
|
96
|
+
_ = file.Close()
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
func main() {
|
|
101
|
+
if len(os.Args) < 2 {
|
|
102
|
+
os.Exit(2)
|
|
103
|
+
}
|
|
104
|
+
if os.Args[1] == "lsp-serve" {
|
|
105
|
+
scanner := bufio.NewScanner(os.Stdin)
|
|
106
|
+
for scanner.Scan() {
|
|
107
|
+
var req request
|
|
108
|
+
if json.Unmarshal(scanner.Bytes(), &req) != nil {
|
|
109
|
+
os.Exit(2)
|
|
110
|
+
}
|
|
111
|
+
logCall("resident " + req.Verb)
|
|
112
|
+
fmt.Println(` + "`" + `{"result":null,"code":2}` + "`" + `)
|
|
113
|
+
}
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
if os.Args[1] == "lsp-project-diagnostics" {
|
|
117
|
+
logCall("direct lsp-project-diagnostics")
|
|
118
|
+
fmt.Println(` + "`" + `{"uri":"file:///project/tsconfig.json","diagnostics":[{"range":{"start":{"line":0,"character":0},"end":{"line":0,"character":0}},"code":"direct","message":"direct"}]}` + "`" + `)
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
os.Exit(2)
|
|
122
|
+
}
|
|
123
|
+
`
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
package lspserver
|
|
2
|
+
|
|
3
|
+
import "testing"
|
|
4
|
+
|
|
5
|
+
type projectDiagnosticsResumeSource struct {
|
|
6
|
+
NullPluginSource
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
func (projectDiagnosticsResumeSource) ProjectDiagnostics() *LSPProjectDiagnostics {
|
|
10
|
+
return nil
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// TestResumePendingProjectDiagnosticRefreshDoesNotReviveCompletedWork verifies
|
|
14
|
+
// a save or close advances and rearms pending work atomically, while a refresh
|
|
15
|
+
// completed before that transition remains completed.
|
|
16
|
+
func TestResumePendingProjectDiagnosticRefreshDoesNotReviveCompletedWork(
|
|
17
|
+
t *testing.T,
|
|
18
|
+
) {
|
|
19
|
+
proxy := NewProxy(ProxyOptions{
|
|
20
|
+
Source: projectDiagnosticsResumeSource{},
|
|
21
|
+
})
|
|
22
|
+
defer proxy.stopProjectDiagnosticRefresh()
|
|
23
|
+
|
|
24
|
+
proxy.scheduleProjectDiagnosticRefresh(projectDiagnosticOwnerScope{all: true})
|
|
25
|
+
proxy.diagnosticsMu.Lock()
|
|
26
|
+
before := proxy.projectDiagnosticGeneration
|
|
27
|
+
proxy.diagnosticsMu.Unlock()
|
|
28
|
+
|
|
29
|
+
proxy.resumePendingProjectDiagnosticRefresh()
|
|
30
|
+
|
|
31
|
+
proxy.diagnosticsMu.Lock()
|
|
32
|
+
after := proxy.projectDiagnosticGeneration
|
|
33
|
+
proxy.diagnosticsMu.Unlock()
|
|
34
|
+
if after != before+1 {
|
|
35
|
+
t.Fatalf("resume advanced generation from %d to %d", before, after)
|
|
36
|
+
}
|
|
37
|
+
proxy.projectRefreshMu.Lock()
|
|
38
|
+
pending := proxy.projectDiagnosticRefreshPending
|
|
39
|
+
generation := proxy.pendingProjectDiagnosticGeneration
|
|
40
|
+
proxy.projectRefreshMu.Unlock()
|
|
41
|
+
if !pending {
|
|
42
|
+
t.Fatal("resume cleared the pending refresh")
|
|
43
|
+
}
|
|
44
|
+
proxy.completePendingProjectDiagnosticRefresh(generation)
|
|
45
|
+
proxy.resumePendingProjectDiagnosticRefresh()
|
|
46
|
+
proxy.diagnosticsMu.Lock()
|
|
47
|
+
final := proxy.projectDiagnosticGeneration
|
|
48
|
+
proxy.diagnosticsMu.Unlock()
|
|
49
|
+
if final != after {
|
|
50
|
+
t.Fatalf("completed refresh was revived at generation %d", final)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
package lspserver
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"os"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"testing"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
// TestLSPProjectDiagnosticsTranslatesRelativeClientProject verifies a project
|
|
10
|
+
// URI is restated in the client's spelling however the client named it.
|
|
11
|
+
//
|
|
12
|
+
// The editor is not required to pass an absolute `--tsconfig`; naming the
|
|
13
|
+
// project relative to the working directory it also passed is the ordinary
|
|
14
|
+
// spelling, and every ttsc CLI accepts it. The translator, however, needs an
|
|
15
|
+
// absolute path both to compare against the producer's URI and to build a URI
|
|
16
|
+
// from, so a relative spelling used to end the translation and let the sidecar's
|
|
17
|
+
// own spelling reach the editor unchanged — the precise failure this whole
|
|
18
|
+
// translation exists to prevent, surviving in the common case.
|
|
19
|
+
//
|
|
20
|
+
// A client that named no directory either is not a case to decline: this host
|
|
21
|
+
// was started from that directory and inherited it, so its own working
|
|
22
|
+
// directory is the same anchor reached another way.
|
|
23
|
+
//
|
|
24
|
+
// 1. Resolve a relative project against the directory the client named.
|
|
25
|
+
// 2. Resolve one against the directory the host inherited.
|
|
26
|
+
// 3. Leave an absolute one exactly as the previous behavior did.
|
|
27
|
+
func TestLSPProjectDiagnosticsTranslatesRelativeClientProject(t *testing.T) {
|
|
28
|
+
root := t.TempDir()
|
|
29
|
+
if err := os.WriteFile(
|
|
30
|
+
filepath.Join(root, "tsconfig.json"),
|
|
31
|
+
[]byte("{}"),
|
|
32
|
+
0o644,
|
|
33
|
+
); err != nil {
|
|
34
|
+
t.Fatalf("write tsconfig: %v", err)
|
|
35
|
+
}
|
|
36
|
+
if err := os.Mkdir(filepath.Join(root, "src"), 0o755); err != nil {
|
|
37
|
+
t.Fatalf("make src: %v", err)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Spelled through a directory and back out, so it addresses the same file the
|
|
41
|
+
// client does without being the same string.
|
|
42
|
+
detour := func(directory string) string {
|
|
43
|
+
return projectInputFileURI(
|
|
44
|
+
filepath.Join(directory, "src") +
|
|
45
|
+
string(os.PathSeparator) + ".." +
|
|
46
|
+
string(os.PathSeparator) + "tsconfig.json",
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
named := detour(root)
|
|
50
|
+
expected := projectInputFileURI(filepath.Join(root, "tsconfig.json"))
|
|
51
|
+
if named == expected {
|
|
52
|
+
t.Fatalf("the producer URI must differ from the client's to be a test")
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
source := &NativePluginSource{
|
|
56
|
+
clientTsconfig: "tsconfig.json",
|
|
57
|
+
clientCwd: root,
|
|
58
|
+
}
|
|
59
|
+
if got := source.clientProjectURI(named); got != expected {
|
|
60
|
+
t.Fatalf("relative project not translated: got %q, want %q", got, expected)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
working, err := os.Getwd()
|
|
64
|
+
if err != nil {
|
|
65
|
+
t.Fatalf("working directory: %v", err)
|
|
66
|
+
}
|
|
67
|
+
inherited := &NativePluginSource{clientTsconfig: "tsconfig.json"}
|
|
68
|
+
inheritedWant := projectInputFileURI(filepath.Join(working, "tsconfig.json"))
|
|
69
|
+
if got := inherited.clientProjectURI(detour(working)); got != inheritedWant {
|
|
70
|
+
t.Fatalf(
|
|
71
|
+
"inherited directory not used: got %q, want %q",
|
|
72
|
+
got,
|
|
73
|
+
inheritedWant,
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
absolute := &NativePluginSource{
|
|
78
|
+
clientTsconfig: filepath.Join(root, "tsconfig.json"),
|
|
79
|
+
}
|
|
80
|
+
if got := absolute.clientProjectURI(named); got != expected {
|
|
81
|
+
t.Fatalf("absolute project no longer translated: got %q, want %q", got, expected)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Nothing to translate to, so the producer keeps its own spelling rather than
|
|
85
|
+
// being addressed at a project the client never named.
|
|
86
|
+
unnamed := &NativePluginSource{}
|
|
87
|
+
if got := unnamed.clientProjectURI(named); got != named {
|
|
88
|
+
t.Fatalf("unnamed project was rewritten: got %q, want %q", got, named)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//go:build !windows
|
|
2
|
+
|
|
3
|
+
package lspserver
|
|
4
|
+
|
|
5
|
+
import "strings"
|
|
6
|
+
|
|
7
|
+
func projectInputGlobMatchesCandidate(
|
|
8
|
+
pattern string,
|
|
9
|
+
candidate string,
|
|
10
|
+
) bool {
|
|
11
|
+
return matchProjectInputGlob(
|
|
12
|
+
strings.Split(projectInputPathKey(realProjectInputPath(pattern)), "/"),
|
|
13
|
+
strings.Split(projectInputPathKey(realProjectInputPath(candidate)), "/"),
|
|
14
|
+
nil,
|
|
15
|
+
)
|
|
16
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
//go:build windows
|
|
2
|
+
|
|
3
|
+
package lspserver
|
|
4
|
+
|
|
5
|
+
import (
|
|
6
|
+
"path/filepath"
|
|
7
|
+
"strings"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
func projectInputGlobMatchesCandidate(
|
|
11
|
+
pattern string,
|
|
12
|
+
candidate string,
|
|
13
|
+
) bool {
|
|
14
|
+
resolvedPattern := resolveWindowsProjectInputPath(pattern)
|
|
15
|
+
resolvedCandidate := resolveWindowsProjectInputPath(candidate)
|
|
16
|
+
if !resolvedPattern.found || !resolvedCandidate.found {
|
|
17
|
+
return false
|
|
18
|
+
}
|
|
19
|
+
patternVolume := strings.ToLower(
|
|
20
|
+
filepath.ToSlash(filepath.VolumeName(resolvedPattern.physical)),
|
|
21
|
+
)
|
|
22
|
+
candidateVolume := strings.ToLower(
|
|
23
|
+
filepath.ToSlash(filepath.VolumeName(resolvedCandidate.physical)),
|
|
24
|
+
)
|
|
25
|
+
if patternVolume != candidateVolume {
|
|
26
|
+
return false
|
|
27
|
+
}
|
|
28
|
+
patternSegments, _ := windowsProjectInputSegments(resolvedPattern)
|
|
29
|
+
candidateSegments, candidateSensitivities :=
|
|
30
|
+
windowsProjectInputSegments(resolvedCandidate)
|
|
31
|
+
return matchProjectInputGlob(
|
|
32
|
+
patternSegments,
|
|
33
|
+
candidateSegments,
|
|
34
|
+
candidateSensitivities,
|
|
35
|
+
)
|
|
36
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
package lspserver
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"os"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"testing"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
// TestLSPProjectInputOwnershipResolvesDeclaredSpellings verifies a declaration
|
|
10
|
+
// is matched against an event through one filesystem identity.
|
|
11
|
+
//
|
|
12
|
+
// The candidate always arrives from an editor URI and is resolved physically,
|
|
13
|
+
// so comparing it against a declaration that was only cleaned lexically can
|
|
14
|
+
// never match wherever the two spellings differ. A Windows short (8.3)
|
|
15
|
+
// component, which the system temporary directory routinely carries, and a
|
|
16
|
+
// symlinked ancestor such as macOS `/var` both produce exactly that, and the
|
|
17
|
+
// contributor that declared the file then receives no refresh at all.
|
|
18
|
+
//
|
|
19
|
+
// 1. Declare an exact file and a glob through a directory alias.
|
|
20
|
+
// 2. Ask for the owners of the same paths spelled physically.
|
|
21
|
+
// 3. Assert the declaring producer owns both.
|
|
22
|
+
// 4. Assert two adjacent paths one property away are owned by nobody.
|
|
23
|
+
func TestLSPProjectInputOwnershipResolvesDeclaredSpellings(t *testing.T) {
|
|
24
|
+
physical := t.TempDir()
|
|
25
|
+
alias := filepath.Join(t.TempDir(), "project")
|
|
26
|
+
if err := os.Symlink(physical, alias); err != nil {
|
|
27
|
+
t.Skipf("filesystem cannot express a directory alias: %v", err)
|
|
28
|
+
}
|
|
29
|
+
for _, directory := range []string{
|
|
30
|
+
filepath.Join(physical, "docs"),
|
|
31
|
+
filepath.Join(physical, "api"),
|
|
32
|
+
} {
|
|
33
|
+
if err := os.MkdirAll(directory, 0o755); err != nil {
|
|
34
|
+
t.Fatal(err)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
exact := filepath.Join(physical, "docs", "spec.md")
|
|
38
|
+
matched := filepath.Join(physical, "api", "openapi.json")
|
|
39
|
+
for _, location := range []string{exact, matched} {
|
|
40
|
+
if err := os.WriteFile(location, []byte("{}\n"), 0o644); err != nil {
|
|
41
|
+
t.Fatal(err)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
plugin := NativeLSPPluginEntry{
|
|
46
|
+
Binary: "ttsc-no-such-alias-sidecar",
|
|
47
|
+
Name: "@ttsc/alias",
|
|
48
|
+
ProjectDiagnostics: true,
|
|
49
|
+
ProjectInputs: true,
|
|
50
|
+
}
|
|
51
|
+
source := &NativePluginSource{plugins: []NativeLSPPluginEntry{plugin}}
|
|
52
|
+
source.storeProjectInputs(plugin, 1, LSPProjectInputSnapshot{
|
|
53
|
+
Root: filepath.ToSlash(alias),
|
|
54
|
+
Files: []string{filepath.ToSlash(filepath.Join(alias, "docs", "spec.md"))},
|
|
55
|
+
Globs: []string{
|
|
56
|
+
filepath.ToSlash(filepath.Join(alias, "api", "**", "*.json")),
|
|
57
|
+
},
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
for _, owned := range []struct {
|
|
61
|
+
label string
|
|
62
|
+
location string
|
|
63
|
+
}{
|
|
64
|
+
{label: "exact declaration", location: exact},
|
|
65
|
+
{label: "glob member", location: matched},
|
|
66
|
+
} {
|
|
67
|
+
owners := source.ProjectInputOwnersForURI(testFileURI(owned.location))
|
|
68
|
+
if len(owners) != 1 || owners[0] != pluginKey(plugin) {
|
|
69
|
+
t.Fatalf("%s owners = %#v", owned.label, owners)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Resolving both sides collapses distinct spellings onto one physical path,
|
|
74
|
+
// so over-matching is the failure this lane can produce. Each negative sits
|
|
75
|
+
// one property away from an owned path: the same directory with another
|
|
76
|
+
// extension, and the same extension outside every declaration.
|
|
77
|
+
for _, unowned := range []struct {
|
|
78
|
+
label string
|
|
79
|
+
location string
|
|
80
|
+
}{
|
|
81
|
+
{
|
|
82
|
+
label: "a sibling the glob does not select",
|
|
83
|
+
location: filepath.Join(physical, "api", "openapi.txt"),
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
label: "a path outside every declaration",
|
|
87
|
+
location: filepath.Join(physical, "docs", "other.json"),
|
|
88
|
+
},
|
|
89
|
+
} {
|
|
90
|
+
owners := source.ProjectInputOwnersForURI(testFileURI(unowned.location))
|
|
91
|
+
if len(owners) != 0 {
|
|
92
|
+
t.Fatalf("%s owners = %#v", unowned.label, owners)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//go:build !windows
|
|
2
|
+
|
|
3
|
+
package lspserver
|
|
4
|
+
|
|
5
|
+
import "path/filepath"
|
|
6
|
+
|
|
7
|
+
func physicalProjectInputPath(location string) (string, error) {
|
|
8
|
+
return filepath.EvalSymlinks(location)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
func projectInputPhysicalPathKey(location string) string {
|
|
12
|
+
return filepath.ToSlash(realProjectInputPath(location))
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
func projectInputPathKey(location string) string {
|
|
16
|
+
return filepath.ToSlash(filepath.Clean(filepath.FromSlash(location)))
|
|
17
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
//go:build windows
|
|
2
|
+
|
|
3
|
+
package lspserver
|
|
4
|
+
|
|
5
|
+
import (
|
|
6
|
+
"path/filepath"
|
|
7
|
+
"strings"
|
|
8
|
+
"unsafe"
|
|
9
|
+
|
|
10
|
+
"golang.org/x/sys/windows"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
type windowsProjectInputPath struct {
|
|
14
|
+
found bool
|
|
15
|
+
missing []string
|
|
16
|
+
physical string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// projectInputPathKey resolves existing spelling through the filesystem and
|
|
20
|
+
// applies case folding to each segment according to the directory that owns
|
|
21
|
+
// that name. Windows case semantics are per directory, not per volume or OS.
|
|
22
|
+
func projectInputPathKey(location string) string {
|
|
23
|
+
normalized := filepath.Clean(projectInputFilesystemPath(location))
|
|
24
|
+
if !filepath.IsAbs(normalized) {
|
|
25
|
+
return strings.ToLower(filepath.ToSlash(normalized))
|
|
26
|
+
}
|
|
27
|
+
volume := filepath.VolumeName(normalized)
|
|
28
|
+
segments := splitWindowsProjectInputSegments(
|
|
29
|
+
strings.TrimPrefix(normalized, volume),
|
|
30
|
+
)
|
|
31
|
+
current := windowsProjectInputVolumeRoot(volume)
|
|
32
|
+
sensitive := false
|
|
33
|
+
for index, segment := range segments {
|
|
34
|
+
if currentSensitivity, ok :=
|
|
35
|
+
tryQueryProjectInputDirectoryCaseSensitivity(current); ok {
|
|
36
|
+
sensitive = currentSensitivity
|
|
37
|
+
}
|
|
38
|
+
if !sensitive {
|
|
39
|
+
segments[index] = strings.ToLower(segments[index])
|
|
40
|
+
}
|
|
41
|
+
current = filepath.Join(current, segment)
|
|
42
|
+
}
|
|
43
|
+
return windowsProjectInputKey(normalized, segments)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
func resolveWindowsProjectInputPath(location string) windowsProjectInputPath {
|
|
47
|
+
normalized := filepath.Clean(projectInputFilesystemPath(location))
|
|
48
|
+
if !filepath.IsAbs(normalized) {
|
|
49
|
+
return windowsProjectInputPath{physical: normalized}
|
|
50
|
+
}
|
|
51
|
+
probe := normalized
|
|
52
|
+
missing := []string{}
|
|
53
|
+
for {
|
|
54
|
+
physical, err := physicalProjectInputPath(probe)
|
|
55
|
+
if err == nil {
|
|
56
|
+
return windowsProjectInputPath{
|
|
57
|
+
found: true,
|
|
58
|
+
missing: reverseWindowsProjectInputSegments(missing),
|
|
59
|
+
physical: filepath.Clean(physical),
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
parent := filepath.Dir(probe)
|
|
63
|
+
if parent == probe {
|
|
64
|
+
return windowsProjectInputPath{physical: normalized}
|
|
65
|
+
}
|
|
66
|
+
missing = append(missing, filepath.Base(probe))
|
|
67
|
+
probe = parent
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
func physicalProjectInputPath(location string) (string, error) {
|
|
72
|
+
pointer, err := windows.UTF16PtrFromString(location)
|
|
73
|
+
if err != nil {
|
|
74
|
+
return "", err
|
|
75
|
+
}
|
|
76
|
+
handle, err := windows.CreateFile(
|
|
77
|
+
pointer,
|
|
78
|
+
windows.FILE_READ_ATTRIBUTES,
|
|
79
|
+
windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE,
|
|
80
|
+
nil,
|
|
81
|
+
windows.OPEN_EXISTING,
|
|
82
|
+
windows.FILE_FLAG_BACKUP_SEMANTICS,
|
|
83
|
+
0,
|
|
84
|
+
)
|
|
85
|
+
if err != nil {
|
|
86
|
+
return "", err
|
|
87
|
+
}
|
|
88
|
+
defer windows.CloseHandle(handle)
|
|
89
|
+
|
|
90
|
+
buffer := make([]uint16, windows.MAX_PATH)
|
|
91
|
+
for {
|
|
92
|
+
length, err := windows.GetFinalPathNameByHandle(
|
|
93
|
+
handle,
|
|
94
|
+
&buffer[0],
|
|
95
|
+
uint32(len(buffer)),
|
|
96
|
+
0,
|
|
97
|
+
)
|
|
98
|
+
if err != nil {
|
|
99
|
+
return "", err
|
|
100
|
+
}
|
|
101
|
+
if length < uint32(len(buffer)) {
|
|
102
|
+
return filepath.Clean(
|
|
103
|
+
projectInputFilesystemPath(
|
|
104
|
+
windows.UTF16ToString(buffer[:length]),
|
|
105
|
+
),
|
|
106
|
+
), nil
|
|
107
|
+
}
|
|
108
|
+
buffer = make([]uint16, length+1)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
func projectInputPhysicalPathKey(location string) string {
|
|
113
|
+
resolved := resolveWindowsProjectInputPath(location)
|
|
114
|
+
volume := filepath.VolumeName(resolved.physical)
|
|
115
|
+
segments := splitWindowsProjectInputSegments(
|
|
116
|
+
strings.TrimPrefix(resolved.physical, volume),
|
|
117
|
+
)
|
|
118
|
+
if len(resolved.missing) != 0 {
|
|
119
|
+
missing := append([]string{}, resolved.missing...)
|
|
120
|
+
if !queryProjectInputDirectoryCaseSensitivity(resolved.physical) {
|
|
121
|
+
for index := range missing {
|
|
122
|
+
missing[index] = strings.ToLower(missing[index])
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
segments = append(segments, missing...)
|
|
126
|
+
}
|
|
127
|
+
prefix := filepath.ToSlash(volume)
|
|
128
|
+
if len(segments) == 0 {
|
|
129
|
+
return prefix + "/"
|
|
130
|
+
}
|
|
131
|
+
return prefix + "/" + strings.Join(segments, "/")
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
func windowsProjectInputSegments(
|
|
135
|
+
resolved windowsProjectInputPath,
|
|
136
|
+
) ([]string, []bool) {
|
|
137
|
+
volume := filepath.VolumeName(resolved.physical)
|
|
138
|
+
existing := splitWindowsProjectInputSegments(
|
|
139
|
+
strings.TrimPrefix(resolved.physical, volume),
|
|
140
|
+
)
|
|
141
|
+
segments := append(append([]string{}, existing...), resolved.missing...)
|
|
142
|
+
sensitivities := make([]bool, 0, len(segments))
|
|
143
|
+
current := windowsProjectInputVolumeRoot(volume)
|
|
144
|
+
for _, segment := range existing {
|
|
145
|
+
sensitivities = append(
|
|
146
|
+
sensitivities,
|
|
147
|
+
queryProjectInputDirectoryCaseSensitivity(current),
|
|
148
|
+
)
|
|
149
|
+
current = filepath.Join(current, segment)
|
|
150
|
+
}
|
|
151
|
+
if len(resolved.missing) != 0 {
|
|
152
|
+
missingSensitivity := queryProjectInputDirectoryCaseSensitivity(
|
|
153
|
+
resolved.physical,
|
|
154
|
+
)
|
|
155
|
+
for range resolved.missing {
|
|
156
|
+
sensitivities = append(sensitivities, missingSensitivity)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return segments, sensitivities
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
func windowsProjectInputKey(location string, segments []string) string {
|
|
163
|
+
volume := filepath.VolumeName(location)
|
|
164
|
+
prefix := strings.ToLower(filepath.ToSlash(volume))
|
|
165
|
+
if len(segments) == 0 {
|
|
166
|
+
return prefix + "/"
|
|
167
|
+
}
|
|
168
|
+
return prefix + "/" + strings.Join(segments, "/")
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
func windowsProjectInputVolumeRoot(volume string) string {
|
|
172
|
+
if strings.HasSuffix(volume, `\`) || strings.HasSuffix(volume, "/") {
|
|
173
|
+
return volume
|
|
174
|
+
}
|
|
175
|
+
return volume + `\`
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
func splitWindowsProjectInputSegments(location string) []string {
|
|
179
|
+
return strings.FieldsFunc(location, func(character rune) bool {
|
|
180
|
+
return character == '/' || character == '\\'
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
func reverseWindowsProjectInputSegments(segments []string) []string {
|
|
185
|
+
reversed := make([]string, len(segments))
|
|
186
|
+
for index := range segments {
|
|
187
|
+
reversed[len(segments)-index-1] = segments[index]
|
|
188
|
+
}
|
|
189
|
+
return reversed
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
func queryProjectInputDirectoryCaseSensitivity(directory string) bool {
|
|
193
|
+
sensitive, _ := tryQueryProjectInputDirectoryCaseSensitivity(directory)
|
|
194
|
+
return sensitive
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
func tryQueryProjectInputDirectoryCaseSensitivity(
|
|
198
|
+
directory string,
|
|
199
|
+
) (bool, bool) {
|
|
200
|
+
pointer, err := windows.UTF16PtrFromString(directory)
|
|
201
|
+
if err != nil {
|
|
202
|
+
return false, false
|
|
203
|
+
}
|
|
204
|
+
handle, err := windows.CreateFile(
|
|
205
|
+
pointer,
|
|
206
|
+
windows.FILE_READ_ATTRIBUTES,
|
|
207
|
+
windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE,
|
|
208
|
+
nil,
|
|
209
|
+
windows.OPEN_EXISTING,
|
|
210
|
+
windows.FILE_FLAG_BACKUP_SEMANTICS,
|
|
211
|
+
0,
|
|
212
|
+
)
|
|
213
|
+
if err != nil {
|
|
214
|
+
return false, false
|
|
215
|
+
}
|
|
216
|
+
defer windows.CloseHandle(handle)
|
|
217
|
+
|
|
218
|
+
var flags uint32
|
|
219
|
+
if err := windows.GetFileInformationByHandleEx(
|
|
220
|
+
handle,
|
|
221
|
+
windows.FileCaseSensitiveInfo,
|
|
222
|
+
(*byte)(unsafe.Pointer(&flags)),
|
|
223
|
+
uint32(unsafe.Sizeof(flags)),
|
|
224
|
+
); err != nil {
|
|
225
|
+
return false, false
|
|
226
|
+
}
|
|
227
|
+
return flags&windows.FILE_CS_FLAG_CASE_SENSITIVE_DIR != 0, true
|
|
228
|
+
}
|