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,944 @@
|
|
|
1
|
+
package lspserver
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"crypto/sha256"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"fmt"
|
|
7
|
+
"os"
|
|
8
|
+
"path"
|
|
9
|
+
"path/filepath"
|
|
10
|
+
"runtime"
|
|
11
|
+
"sort"
|
|
12
|
+
"strings"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
// LSPProjectInputSnapshot is the normalized external filesystem topology
|
|
16
|
+
// published by project-rule contributors.
|
|
17
|
+
type LSPProjectInputSnapshot struct {
|
|
18
|
+
Root string `json:"root"`
|
|
19
|
+
Files []string `json:"files"`
|
|
20
|
+
Globs []string `json:"globs"`
|
|
21
|
+
ReloadFiles []string `json:"reloadFiles,omitempty"`
|
|
22
|
+
ReloadDirectories []string `json:"reloadDirectories,omitempty"`
|
|
23
|
+
ReloadFileDigests map[string]string `json:"reloadFileDigests,omitempty"`
|
|
24
|
+
ReloadDirectoryDigests map[string]string `json:"reloadDirectoryDigests,omitempty"`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type projectInputRecord struct {
|
|
28
|
+
generation uint64
|
|
29
|
+
snapshot LSPProjectInputSnapshot
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ProjectInputs returns a stable copy of the current merged dependency
|
|
33
|
+
// snapshot.
|
|
34
|
+
func (s *NativePluginSource) ProjectInputs() LSPProjectInputSnapshot {
|
|
35
|
+
if s == nil {
|
|
36
|
+
return LSPProjectInputSnapshot{}
|
|
37
|
+
}
|
|
38
|
+
s.projectInputsMu.RLock()
|
|
39
|
+
defer s.projectInputsMu.RUnlock()
|
|
40
|
+
return copyProjectInputSnapshot(s.projectInputs)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ProjectInputReloadFingerprintsAreCurrent reports whether the retained
|
|
44
|
+
// selection-time baseline still matches the filesystem. The proxy checks this
|
|
45
|
+
// after the client confirms dynamic watcher registration, closing the interval
|
|
46
|
+
// between construction-time validation and active event delivery.
|
|
47
|
+
func (s *NativePluginSource) ProjectInputReloadFingerprintsAreCurrent() bool {
|
|
48
|
+
if s == nil {
|
|
49
|
+
return true
|
|
50
|
+
}
|
|
51
|
+
s.projectInputsMu.RLock()
|
|
52
|
+
snapshot := copyProjectInputSnapshot(s.projectInputs)
|
|
53
|
+
s.projectInputsMu.RUnlock()
|
|
54
|
+
return projectInputReloadFingerprintsAreCurrent(snapshot)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ProjectInputMatchesURI reports whether a watched-file URI belongs to a
|
|
58
|
+
// declared exact dependency or glob population.
|
|
59
|
+
func (s *NativePluginSource) ProjectInputMatchesURI(uri string) bool {
|
|
60
|
+
if s == nil {
|
|
61
|
+
return false
|
|
62
|
+
}
|
|
63
|
+
location, ok := filePathFromURI(uri)
|
|
64
|
+
if !ok {
|
|
65
|
+
return false
|
|
66
|
+
}
|
|
67
|
+
candidate := projectInputPathKey(realProjectInputPath(location))
|
|
68
|
+
s.projectInputsMu.RLock()
|
|
69
|
+
defer s.projectInputsMu.RUnlock()
|
|
70
|
+
return projectInputSnapshotMatchesCandidate(s.projectInputs, candidate)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ProjectInputReloadMatchesURI reports whether uri names an exact reload file,
|
|
74
|
+
// a reload directory, or one of that directory's immediate entries. Callers
|
|
75
|
+
// with an LSP change event should use ProjectInputReloadMatchesChange so an
|
|
76
|
+
// ordinary content edit inside a topology directory does not force a restart.
|
|
77
|
+
func (s *NativePluginSource) ProjectInputReloadMatchesURI(uri string) bool {
|
|
78
|
+
if s == nil {
|
|
79
|
+
return false
|
|
80
|
+
}
|
|
81
|
+
location, ok := filePathFromURI(uri)
|
|
82
|
+
if !ok {
|
|
83
|
+
return false
|
|
84
|
+
}
|
|
85
|
+
candidate := realProjectInputPath(location)
|
|
86
|
+
candidateEntry := realProjectInputEntryPath(location)
|
|
87
|
+
candidateKey := projectInputPathKey(candidate)
|
|
88
|
+
candidateEntryKey := projectInputPathKey(candidateEntry)
|
|
89
|
+
s.projectInputsMu.RLock()
|
|
90
|
+
defer s.projectInputsMu.RUnlock()
|
|
91
|
+
for _, file := range s.projectInputs.ReloadFiles {
|
|
92
|
+
// The candidate is resolved physically, so the declaration has to be too;
|
|
93
|
+
// a Windows short component or a symlinked ancestor otherwise never matches.
|
|
94
|
+
fileKey := projectInputPathKey(realProjectInputPath(file))
|
|
95
|
+
if fileKey == candidateKey || fileKey == candidateEntryKey {
|
|
96
|
+
return true
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
for _, directory := range s.projectInputs.ReloadDirectories {
|
|
100
|
+
if projectInputDirectoryContainsImmediate(directory, candidateEntry) {
|
|
101
|
+
return true
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return false
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ProjectInputReloadMatchesChange reports whether an LSP filesystem change
|
|
108
|
+
// invalidates executable plugin selection. Exact reload files are content
|
|
109
|
+
// inputs. Reload directories are topology inputs, so only a change to the
|
|
110
|
+
// directory itself or an immediate entry can qualify, and it qualifies only
|
|
111
|
+
// when the current name/type/symlink-target digest differs from the snapshot.
|
|
112
|
+
func (s *NativePluginSource) ProjectInputReloadMatchesChange(
|
|
113
|
+
uri string,
|
|
114
|
+
changeType *int,
|
|
115
|
+
) bool {
|
|
116
|
+
if s == nil {
|
|
117
|
+
return false
|
|
118
|
+
}
|
|
119
|
+
location, ok := filePathFromURI(uri)
|
|
120
|
+
if !ok {
|
|
121
|
+
return false
|
|
122
|
+
}
|
|
123
|
+
candidate := realProjectInputPath(location)
|
|
124
|
+
candidateEntry := realProjectInputEntryPath(location)
|
|
125
|
+
candidateKey := projectInputPathKey(candidate)
|
|
126
|
+
candidateEntryKey := projectInputPathKey(candidateEntry)
|
|
127
|
+
s.projectInputsMu.RLock()
|
|
128
|
+
snapshot := copyProjectInputSnapshot(s.projectInputs)
|
|
129
|
+
s.projectInputsMu.RUnlock()
|
|
130
|
+
for _, file := range snapshot.ReloadFiles {
|
|
131
|
+
// The candidate is resolved physically, so the declaration has to be too;
|
|
132
|
+
// a Windows short component or a symlinked ancestor otherwise never matches.
|
|
133
|
+
fileKey := projectInputPathKey(realProjectInputPath(file))
|
|
134
|
+
if fileKey == candidateKey || fileKey == candidateEntryKey {
|
|
135
|
+
return true
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
matched := false
|
|
139
|
+
for _, directory := range snapshot.ReloadDirectories {
|
|
140
|
+
if !projectInputDirectoryContainsImmediate(directory, candidateEntry) {
|
|
141
|
+
continue
|
|
142
|
+
}
|
|
143
|
+
key := projectInputPathKey(directory)
|
|
144
|
+
// The digest map is keyed by the declared spelling, so that lookup keeps
|
|
145
|
+
// it. The identity comparison is against a physically resolved candidate
|
|
146
|
+
// and has to resolve too, or a directory recreated with identical topology
|
|
147
|
+
// under an aliased spelling would fall through to the digest compare and
|
|
148
|
+
// report no change at all.
|
|
149
|
+
if projectInputPathKey(realProjectInputPath(directory)) ==
|
|
150
|
+
candidateEntryKey &&
|
|
151
|
+
(changeType == nil || *changeType != fileChangeTypeChanged) {
|
|
152
|
+
return true
|
|
153
|
+
}
|
|
154
|
+
if projectInputReloadDirectoryDigest(directory) !=
|
|
155
|
+
snapshot.ReloadDirectoryDigests[key] {
|
|
156
|
+
matched = true
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return matched
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ProjectInputOwnersForURI returns the stable plugin keys whose latest
|
|
163
|
+
// successful snapshots match uri. Ownership is retained past the flattened
|
|
164
|
+
// client registration so an external edit refreshes only the contributors that
|
|
165
|
+
// declared it.
|
|
166
|
+
func (s *NativePluginSource) ProjectInputOwnersForURI(uri string) []string {
|
|
167
|
+
if s == nil {
|
|
168
|
+
return nil
|
|
169
|
+
}
|
|
170
|
+
location, ok := filePathFromURI(uri)
|
|
171
|
+
if !ok {
|
|
172
|
+
return nil
|
|
173
|
+
}
|
|
174
|
+
candidate := projectInputPathKey(realProjectInputPath(location))
|
|
175
|
+
s.projectInputsMu.RLock()
|
|
176
|
+
defer s.projectInputsMu.RUnlock()
|
|
177
|
+
owners := []string{}
|
|
178
|
+
for _, plugin := range selectPluginTransports(
|
|
179
|
+
s.plugins,
|
|
180
|
+
nil,
|
|
181
|
+
s.projectContextJSON,
|
|
182
|
+
) {
|
|
183
|
+
key := pluginKey(plugin, s.projectContextJSON)
|
|
184
|
+
record, ok := s.pluginProjectInputs[key]
|
|
185
|
+
if !ok || !projectInputSnapshotMatchesCandidate(record.snapshot, candidate) {
|
|
186
|
+
continue
|
|
187
|
+
}
|
|
188
|
+
owners = append(owners, key)
|
|
189
|
+
}
|
|
190
|
+
return owners
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// projectInputSnapshotMatchesCandidate compares a declaration against a
|
|
194
|
+
// candidate both sides of which have been resolved to one filesystem identity.
|
|
195
|
+
//
|
|
196
|
+
// The candidate arrives from an editor URI and is resolved physically, so a
|
|
197
|
+
// declaration compared lexically can never match it wherever the two spellings
|
|
198
|
+
// differ: a Windows short (8.3) component, which `%TEMP%` routinely carries, or
|
|
199
|
+
// a symlinked ancestor such as macOS `/var`. Normalization already resolves
|
|
200
|
+
// every lane, so this resolution is idempotent for any snapshot that arrived
|
|
201
|
+
// through it; it is what makes a snapshot stored directly, as the unit cases
|
|
202
|
+
// do, answer on the same terms as one the ingestion boundary normalized.
|
|
203
|
+
func projectInputSnapshotMatchesCandidate(
|
|
204
|
+
snapshot LSPProjectInputSnapshot,
|
|
205
|
+
candidate string,
|
|
206
|
+
) bool {
|
|
207
|
+
for _, file := range snapshot.Files {
|
|
208
|
+
if projectInputPathKey(realProjectInputPath(file)) == candidate {
|
|
209
|
+
return true
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
for _, pattern := range snapshot.Globs {
|
|
213
|
+
// A pattern has no filesystem object of its own; realProjectInputPath
|
|
214
|
+
// resolves its longest existing prefix and rejoins the wildcards.
|
|
215
|
+
if matchProjectInputGlob(
|
|
216
|
+
strings.Split(projectInputPathKey(realProjectInputPath(pattern)), "/"),
|
|
217
|
+
strings.Split(candidate, "/"),
|
|
218
|
+
) {
|
|
219
|
+
return true
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return false
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// RefreshProjectInputs schedules a coalesced dependency rediscovery after a
|
|
226
|
+
// configuration input changes.
|
|
227
|
+
func (s *NativePluginSource) RefreshProjectInputs() {
|
|
228
|
+
if s == nil {
|
|
229
|
+
return
|
|
230
|
+
}
|
|
231
|
+
s.projectInputsRefresh.schedule(s.discoverProjectInputs)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// SetProjectInputsObserver registers the proxy callback that replaces the
|
|
235
|
+
// client's dynamic watched-file registration after a successful topology
|
|
236
|
+
// change.
|
|
237
|
+
func (s *NativePluginSource) SetProjectInputsObserver(observer func()) {
|
|
238
|
+
if s == nil {
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
s.projectInputsMu.Lock()
|
|
242
|
+
s.projectInputsObserver = observer
|
|
243
|
+
s.projectInputsMu.Unlock()
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
func (s *NativePluginSource) discoverProjectInputs(generation uint64) {
|
|
247
|
+
changed := false
|
|
248
|
+
for _, plugin := range selectPluginTransports(
|
|
249
|
+
s.plugins,
|
|
250
|
+
func(plugin NativeLSPPluginEntry) bool {
|
|
251
|
+
return plugin.ProjectInputs
|
|
252
|
+
},
|
|
253
|
+
s.projectContextJSON,
|
|
254
|
+
) {
|
|
255
|
+
body, err := s.run(plugin, "project-inputs")
|
|
256
|
+
if err != nil {
|
|
257
|
+
s.log("%v", err)
|
|
258
|
+
continue
|
|
259
|
+
}
|
|
260
|
+
var snapshot LSPProjectInputSnapshot
|
|
261
|
+
if err := json.Unmarshal(body, &snapshot); err != nil {
|
|
262
|
+
s.log(
|
|
263
|
+
"ttscserver: %s project-inputs returned invalid JSON: %v",
|
|
264
|
+
pluginLabel(plugin),
|
|
265
|
+
err,
|
|
266
|
+
)
|
|
267
|
+
continue
|
|
268
|
+
}
|
|
269
|
+
snapshot, err = normalizeLSPProjectInputSnapshot(snapshot, s.cwd)
|
|
270
|
+
if err != nil {
|
|
271
|
+
s.log(
|
|
272
|
+
"ttscserver: %s project-inputs returned an invalid snapshot: %v",
|
|
273
|
+
pluginLabel(plugin),
|
|
274
|
+
err,
|
|
275
|
+
)
|
|
276
|
+
continue
|
|
277
|
+
}
|
|
278
|
+
if s.storeProjectInputs(plugin, generation, snapshot) {
|
|
279
|
+
changed = true
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if changed {
|
|
283
|
+
s.projectInputsMu.RLock()
|
|
284
|
+
observer := s.projectInputsObserver
|
|
285
|
+
s.projectInputsMu.RUnlock()
|
|
286
|
+
if observer != nil {
|
|
287
|
+
observer()
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
func (s *NativePluginSource) storeProjectInputs(
|
|
293
|
+
plugin NativeLSPPluginEntry,
|
|
294
|
+
generation uint64,
|
|
295
|
+
snapshot LSPProjectInputSnapshot,
|
|
296
|
+
) bool {
|
|
297
|
+
key := pluginKey(plugin, s.projectContextJSON)
|
|
298
|
+
s.projectInputsMu.Lock()
|
|
299
|
+
defer s.projectInputsMu.Unlock()
|
|
300
|
+
if existing, ok := s.pluginProjectInputs[key]; ok &&
|
|
301
|
+
generation < existing.generation {
|
|
302
|
+
return false
|
|
303
|
+
}
|
|
304
|
+
if s.pluginProjectInputs == nil {
|
|
305
|
+
s.pluginProjectInputs = map[string]projectInputRecord{}
|
|
306
|
+
}
|
|
307
|
+
existing, existed := s.pluginProjectInputs[key]
|
|
308
|
+
if existed {
|
|
309
|
+
preserveProjectInputReloadFingerprints(existing.snapshot, &snapshot)
|
|
310
|
+
}
|
|
311
|
+
s.pluginProjectInputs[key] = projectInputRecord{
|
|
312
|
+
generation: generation,
|
|
313
|
+
snapshot: snapshot,
|
|
314
|
+
}
|
|
315
|
+
if existed && projectInputSnapshotsEqual(existing.snapshot, snapshot) {
|
|
316
|
+
return false
|
|
317
|
+
}
|
|
318
|
+
s.projectInputs = s.flattenProjectInputsLocked()
|
|
319
|
+
return true
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
func (s *NativePluginSource) flattenProjectInputsLocked() LSPProjectInputSnapshot {
|
|
323
|
+
files := map[string]string{}
|
|
324
|
+
globs := map[string]string{}
|
|
325
|
+
reloadFiles := map[string]string{}
|
|
326
|
+
reloadDirectories := map[string]string{}
|
|
327
|
+
reloadFileDigests := map[string]string{}
|
|
328
|
+
reloadDirectoryDigests := map[string]string{}
|
|
329
|
+
root := ""
|
|
330
|
+
for _, plugin := range selectPluginTransports(
|
|
331
|
+
s.plugins,
|
|
332
|
+
nil,
|
|
333
|
+
s.projectContextJSON,
|
|
334
|
+
) {
|
|
335
|
+
key := pluginKey(plugin, s.projectContextJSON)
|
|
336
|
+
snapshot := s.pluginProjectInputs[key].snapshot
|
|
337
|
+
if root == "" && snapshot.Root != "" {
|
|
338
|
+
root = snapshot.Root
|
|
339
|
+
}
|
|
340
|
+
for _, file := range snapshot.Files {
|
|
341
|
+
files[projectInputPathKey(file)] = file
|
|
342
|
+
}
|
|
343
|
+
for _, pattern := range snapshot.Globs {
|
|
344
|
+
globs[projectInputPathKey(pattern)] = pattern
|
|
345
|
+
}
|
|
346
|
+
for _, file := range snapshot.ReloadFiles {
|
|
347
|
+
fileKey := projectInputPathKey(file)
|
|
348
|
+
reloadFiles[fileKey] = file
|
|
349
|
+
reloadFileDigests[fileKey] = snapshot.ReloadFileDigests[fileKey]
|
|
350
|
+
}
|
|
351
|
+
for _, directory := range snapshot.ReloadDirectories {
|
|
352
|
+
directoryKey := projectInputPathKey(directory)
|
|
353
|
+
reloadDirectories[directoryKey] = directory
|
|
354
|
+
reloadDirectoryDigests[directoryKey] =
|
|
355
|
+
snapshot.ReloadDirectoryDigests[directoryKey]
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
out := LSPProjectInputSnapshot{
|
|
359
|
+
Root: root,
|
|
360
|
+
ReloadFileDigests: reloadFileDigests,
|
|
361
|
+
ReloadDirectoryDigests: reloadDirectoryDigests,
|
|
362
|
+
}
|
|
363
|
+
for _, file := range files {
|
|
364
|
+
out.Files = append(out.Files, file)
|
|
365
|
+
}
|
|
366
|
+
for _, pattern := range globs {
|
|
367
|
+
out.Globs = append(out.Globs, pattern)
|
|
368
|
+
}
|
|
369
|
+
for _, file := range reloadFiles {
|
|
370
|
+
out.ReloadFiles = append(out.ReloadFiles, file)
|
|
371
|
+
}
|
|
372
|
+
for _, directory := range reloadDirectories {
|
|
373
|
+
out.ReloadDirectories = append(out.ReloadDirectories, directory)
|
|
374
|
+
}
|
|
375
|
+
sort.Strings(out.Files)
|
|
376
|
+
sort.Strings(out.Globs)
|
|
377
|
+
sort.Strings(out.ReloadFiles)
|
|
378
|
+
sort.Strings(out.ReloadDirectories)
|
|
379
|
+
return out
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
func normalizeLSPProjectInputSnapshot(
|
|
383
|
+
snapshot LSPProjectInputSnapshot,
|
|
384
|
+
expectedRoot string,
|
|
385
|
+
) (LSPProjectInputSnapshot, error) {
|
|
386
|
+
if strings.TrimSpace(snapshot.Root) == "" ||
|
|
387
|
+
!isAbsoluteLocalLSPProjectInputPath(snapshot.Root, runtime.GOOS) {
|
|
388
|
+
return LSPProjectInputSnapshot{}, fmt.Errorf(
|
|
389
|
+
"root %q is not an absolute local path",
|
|
390
|
+
snapshot.Root,
|
|
391
|
+
)
|
|
392
|
+
}
|
|
393
|
+
root := filepath.ToSlash(realProjectInputPath(snapshot.Root))
|
|
394
|
+
if strings.TrimSpace(expectedRoot) != "" &&
|
|
395
|
+
projectInputPathKey(root) !=
|
|
396
|
+
projectInputPathKey(realProjectInputPath(expectedRoot)) {
|
|
397
|
+
return LSPProjectInputSnapshot{}, fmt.Errorf(
|
|
398
|
+
"root %q differs from selected project root %q",
|
|
399
|
+
root,
|
|
400
|
+
filepath.ToSlash(realProjectInputPath(expectedRoot)),
|
|
401
|
+
)
|
|
402
|
+
}
|
|
403
|
+
files := map[string]string{}
|
|
404
|
+
for _, file := range snapshot.Files {
|
|
405
|
+
if strings.TrimSpace(file) == "" ||
|
|
406
|
+
!isAbsoluteLocalLSPProjectInputPath(file, runtime.GOOS) {
|
|
407
|
+
return LSPProjectInputSnapshot{}, fmt.Errorf(
|
|
408
|
+
"file %q is not an absolute local path",
|
|
409
|
+
file,
|
|
410
|
+
)
|
|
411
|
+
}
|
|
412
|
+
normalized := filepath.ToSlash(realProjectInputPath(file))
|
|
413
|
+
files[projectInputPathKey(normalized)] = normalized
|
|
414
|
+
}
|
|
415
|
+
reloadFiles := map[string]string{}
|
|
416
|
+
for _, file := range snapshot.ReloadFiles {
|
|
417
|
+
if strings.TrimSpace(file) == "" ||
|
|
418
|
+
!isAbsoluteLocalLSPProjectInputPath(file, runtime.GOOS) {
|
|
419
|
+
return LSPProjectInputSnapshot{}, fmt.Errorf(
|
|
420
|
+
"reload file %q is not an absolute local path",
|
|
421
|
+
file,
|
|
422
|
+
)
|
|
423
|
+
}
|
|
424
|
+
normalized := filepath.ToSlash(realProjectInputEntryPath(file))
|
|
425
|
+
reloadFiles[projectInputPathKey(normalized)] = normalized
|
|
426
|
+
}
|
|
427
|
+
reloadDirectories := map[string]string{}
|
|
428
|
+
for _, directory := range snapshot.ReloadDirectories {
|
|
429
|
+
if strings.TrimSpace(directory) == "" ||
|
|
430
|
+
!isAbsoluteLocalLSPProjectInputPath(directory, runtime.GOOS) {
|
|
431
|
+
return LSPProjectInputSnapshot{}, fmt.Errorf(
|
|
432
|
+
"reload directory %q is not an absolute local path",
|
|
433
|
+
directory,
|
|
434
|
+
)
|
|
435
|
+
}
|
|
436
|
+
normalized := filepath.ToSlash(realProjectInputPath(directory))
|
|
437
|
+
reloadDirectories[projectInputPathKey(normalized)] = normalized
|
|
438
|
+
}
|
|
439
|
+
globs := map[string]string{}
|
|
440
|
+
for _, pattern := range snapshot.Globs {
|
|
441
|
+
if strings.TrimSpace(pattern) == "" ||
|
|
442
|
+
!isAbsoluteLocalLSPProjectInputPath(pattern, runtime.GOOS) {
|
|
443
|
+
return LSPProjectInputSnapshot{}, fmt.Errorf(
|
|
444
|
+
"glob %q is not an absolute local path",
|
|
445
|
+
pattern,
|
|
446
|
+
)
|
|
447
|
+
}
|
|
448
|
+
// Every other lane in this function resolves its declaration physically,
|
|
449
|
+
// and a glob has to as well or its population is compared against events
|
|
450
|
+
// that were. A pattern has no filesystem object of its own, so resolution
|
|
451
|
+
// takes its longest existing prefix and rejoins the wildcards, which leaves
|
|
452
|
+
// the pattern's shape untouched while its root gains one identity.
|
|
453
|
+
normalized := filepath.ToSlash(realProjectInputPath(pattern))
|
|
454
|
+
globs[projectInputPathKey(normalized)] = normalized
|
|
455
|
+
}
|
|
456
|
+
out := LSPProjectInputSnapshot{
|
|
457
|
+
Root: root,
|
|
458
|
+
ReloadFileDigests: map[string]string{},
|
|
459
|
+
ReloadDirectoryDigests: map[string]string{},
|
|
460
|
+
}
|
|
461
|
+
for _, file := range files {
|
|
462
|
+
out.Files = append(out.Files, file)
|
|
463
|
+
}
|
|
464
|
+
for _, pattern := range globs {
|
|
465
|
+
out.Globs = append(out.Globs, pattern)
|
|
466
|
+
}
|
|
467
|
+
for _, file := range reloadFiles {
|
|
468
|
+
out.ReloadFiles = append(out.ReloadFiles, file)
|
|
469
|
+
key := projectInputPathKey(file)
|
|
470
|
+
digest, ok := projectInputSnapshotDigest(snapshot.ReloadFileDigests, file)
|
|
471
|
+
if !ok {
|
|
472
|
+
if snapshot.ReloadFileDigests != nil {
|
|
473
|
+
return LSPProjectInputSnapshot{}, fmt.Errorf(
|
|
474
|
+
"reload file %q is missing its selection fingerprint",
|
|
475
|
+
file,
|
|
476
|
+
)
|
|
477
|
+
}
|
|
478
|
+
digest = projectInputReloadFileDigest(file)
|
|
479
|
+
}
|
|
480
|
+
if !validProjectInputFingerprint(digest) {
|
|
481
|
+
return LSPProjectInputSnapshot{}, fmt.Errorf(
|
|
482
|
+
"reload file %q has an invalid fingerprint",
|
|
483
|
+
file,
|
|
484
|
+
)
|
|
485
|
+
}
|
|
486
|
+
out.ReloadFileDigests[key] = digest
|
|
487
|
+
}
|
|
488
|
+
for _, directory := range reloadDirectories {
|
|
489
|
+
out.ReloadDirectories = append(out.ReloadDirectories, directory)
|
|
490
|
+
key := projectInputPathKey(directory)
|
|
491
|
+
digest, ok := projectInputSnapshotDigest(
|
|
492
|
+
snapshot.ReloadDirectoryDigests,
|
|
493
|
+
directory,
|
|
494
|
+
)
|
|
495
|
+
if !ok {
|
|
496
|
+
if snapshot.ReloadDirectoryDigests != nil {
|
|
497
|
+
return LSPProjectInputSnapshot{}, fmt.Errorf(
|
|
498
|
+
"reload directory %q is missing its selection fingerprint",
|
|
499
|
+
directory,
|
|
500
|
+
)
|
|
501
|
+
}
|
|
502
|
+
digest = projectInputReloadDirectoryDigest(directory)
|
|
503
|
+
}
|
|
504
|
+
if !validProjectInputFingerprint(digest) {
|
|
505
|
+
return LSPProjectInputSnapshot{}, fmt.Errorf(
|
|
506
|
+
"reload directory %q has an invalid fingerprint",
|
|
507
|
+
directory,
|
|
508
|
+
)
|
|
509
|
+
}
|
|
510
|
+
out.ReloadDirectoryDigests[key] = digest
|
|
511
|
+
}
|
|
512
|
+
sort.Strings(out.Files)
|
|
513
|
+
sort.Strings(out.Globs)
|
|
514
|
+
sort.Strings(out.ReloadFiles)
|
|
515
|
+
sort.Strings(out.ReloadDirectories)
|
|
516
|
+
return out, nil
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
func isAbsoluteLocalLSPProjectInputPath(
|
|
520
|
+
location string,
|
|
521
|
+
goos string,
|
|
522
|
+
) bool {
|
|
523
|
+
if strings.ContainsRune(location, '\x00') {
|
|
524
|
+
return false
|
|
525
|
+
}
|
|
526
|
+
if goos != "windows" {
|
|
527
|
+
return path.IsAbs(location)
|
|
528
|
+
}
|
|
529
|
+
normalized := strings.ReplaceAll(location, "/", `\`)
|
|
530
|
+
if strings.HasPrefix(normalized, `\\?\`) {
|
|
531
|
+
extended := strings.TrimPrefix(normalized, `\\?\`)
|
|
532
|
+
if isWindowsDrivePath(extended) {
|
|
533
|
+
return true
|
|
534
|
+
}
|
|
535
|
+
if strings.HasPrefix(strings.ToLower(extended), `unc\`) {
|
|
536
|
+
return isWindowsUNCPath(extended[4:])
|
|
537
|
+
}
|
|
538
|
+
return false
|
|
539
|
+
}
|
|
540
|
+
if strings.HasPrefix(normalized, `\\.\`) {
|
|
541
|
+
return false
|
|
542
|
+
}
|
|
543
|
+
return isWindowsDrivePath(normalized) ||
|
|
544
|
+
(strings.HasPrefix(normalized, `\\`) &&
|
|
545
|
+
isWindowsUNCPath(strings.TrimPrefix(normalized, `\\`)))
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
func isWindowsDrivePath(location string) bool {
|
|
549
|
+
return len(location) >= 3 &&
|
|
550
|
+
((location[0] >= 'A' && location[0] <= 'Z') ||
|
|
551
|
+
(location[0] >= 'a' && location[0] <= 'z')) &&
|
|
552
|
+
location[1] == ':' &&
|
|
553
|
+
location[2] == '\\'
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
func isWindowsUNCPath(location string) bool {
|
|
557
|
+
components := strings.Split(location, `\`)
|
|
558
|
+
return len(components) >= 2 &&
|
|
559
|
+
isWindowsUNCVolumeSegment(components[0]) &&
|
|
560
|
+
isWindowsUNCVolumeSegment(components[1])
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
func isWindowsUNCVolumeSegment(segment string) bool {
|
|
564
|
+
return segment != "" &&
|
|
565
|
+
segment != "." &&
|
|
566
|
+
segment != ".." &&
|
|
567
|
+
!strings.ContainsAny(segment, "\x00<>:\"/\\|?*")
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
func copyProjectInputSnapshot(
|
|
571
|
+
snapshot LSPProjectInputSnapshot,
|
|
572
|
+
) LSPProjectInputSnapshot {
|
|
573
|
+
copied := LSPProjectInputSnapshot{
|
|
574
|
+
Root: snapshot.Root,
|
|
575
|
+
Files: append([]string(nil), snapshot.Files...),
|
|
576
|
+
Globs: append([]string(nil), snapshot.Globs...),
|
|
577
|
+
ReloadFiles: append([]string(nil), snapshot.ReloadFiles...),
|
|
578
|
+
ReloadDirectories: append([]string(nil), snapshot.ReloadDirectories...),
|
|
579
|
+
}
|
|
580
|
+
if snapshot.ReloadFileDigests != nil {
|
|
581
|
+
copied.ReloadFileDigests = make(
|
|
582
|
+
map[string]string,
|
|
583
|
+
len(snapshot.ReloadFileDigests),
|
|
584
|
+
)
|
|
585
|
+
for key, digest := range snapshot.ReloadFileDigests {
|
|
586
|
+
copied.ReloadFileDigests[key] = digest
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
if snapshot.ReloadDirectoryDigests != nil {
|
|
590
|
+
copied.ReloadDirectoryDigests = make(
|
|
591
|
+
map[string]string,
|
|
592
|
+
len(snapshot.ReloadDirectoryDigests),
|
|
593
|
+
)
|
|
594
|
+
for key, digest := range snapshot.ReloadDirectoryDigests {
|
|
595
|
+
copied.ReloadDirectoryDigests[key] = digest
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return copied
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
func projectInputSnapshotsEqual(
|
|
602
|
+
left LSPProjectInputSnapshot,
|
|
603
|
+
right LSPProjectInputSnapshot,
|
|
604
|
+
) bool {
|
|
605
|
+
if projectInputPathKey(left.Root) != projectInputPathKey(right.Root) ||
|
|
606
|
+
len(left.Files) != len(right.Files) ||
|
|
607
|
+
len(left.Globs) != len(right.Globs) ||
|
|
608
|
+
len(left.ReloadFiles) != len(right.ReloadFiles) ||
|
|
609
|
+
len(left.ReloadDirectories) != len(right.ReloadDirectories) {
|
|
610
|
+
return false
|
|
611
|
+
}
|
|
612
|
+
for index := range left.Files {
|
|
613
|
+
if projectInputPathKey(left.Files[index]) !=
|
|
614
|
+
projectInputPathKey(right.Files[index]) {
|
|
615
|
+
return false
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
for index := range left.Globs {
|
|
619
|
+
if projectInputPathKey(left.Globs[index]) !=
|
|
620
|
+
projectInputPathKey(right.Globs[index]) {
|
|
621
|
+
return false
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
for index := range left.ReloadFiles {
|
|
625
|
+
if projectInputPathKey(left.ReloadFiles[index]) !=
|
|
626
|
+
projectInputPathKey(right.ReloadFiles[index]) {
|
|
627
|
+
return false
|
|
628
|
+
}
|
|
629
|
+
key := projectInputPathKey(left.ReloadFiles[index])
|
|
630
|
+
if left.ReloadFileDigests[key] != right.ReloadFileDigests[key] {
|
|
631
|
+
return false
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
for index := range left.ReloadDirectories {
|
|
635
|
+
if projectInputPathKey(left.ReloadDirectories[index]) !=
|
|
636
|
+
projectInputPathKey(right.ReloadDirectories[index]) {
|
|
637
|
+
return false
|
|
638
|
+
}
|
|
639
|
+
key := projectInputPathKey(left.ReloadDirectories[index])
|
|
640
|
+
if left.ReloadDirectoryDigests[key] != right.ReloadDirectoryDigests[key] {
|
|
641
|
+
return false
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return true
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
func projectInputDirectoryContainsImmediate(
|
|
648
|
+
directory string,
|
|
649
|
+
candidate string,
|
|
650
|
+
) bool {
|
|
651
|
+
// Both operands are resolved physically before they are compared. Callers
|
|
652
|
+
// pass a candidate that already went through that resolution, so leaving the
|
|
653
|
+
// declared directory lexical would never match it under a Windows short
|
|
654
|
+
// component or a symlinked ancestor.
|
|
655
|
+
resolvedDirectory := realProjectInputPath(directory)
|
|
656
|
+
relative, err := filepath.Rel(
|
|
657
|
+
projectInputFilesystemPath(resolvedDirectory),
|
|
658
|
+
projectInputFilesystemPath(candidate),
|
|
659
|
+
)
|
|
660
|
+
if err != nil || filepath.IsAbs(relative) {
|
|
661
|
+
return false
|
|
662
|
+
}
|
|
663
|
+
relativeKey := projectInputPathKey(relative)
|
|
664
|
+
if relativeKey == "." {
|
|
665
|
+
return projectInputPathKey(resolvedDirectory) ==
|
|
666
|
+
projectInputPathKey(candidate)
|
|
667
|
+
}
|
|
668
|
+
return relativeKey != ".." &&
|
|
669
|
+
!strings.HasPrefix(relativeKey, "../") &&
|
|
670
|
+
!strings.Contains(relativeKey, "/")
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
func preserveProjectInputReloadFingerprints(
|
|
674
|
+
baseline LSPProjectInputSnapshot,
|
|
675
|
+
current *LSPProjectInputSnapshot,
|
|
676
|
+
) {
|
|
677
|
+
if current == nil {
|
|
678
|
+
return
|
|
679
|
+
}
|
|
680
|
+
for _, file := range current.ReloadFiles {
|
|
681
|
+
key := projectInputPathKey(file)
|
|
682
|
+
if digest := baseline.ReloadFileDigests[key]; digest != "" {
|
|
683
|
+
current.ReloadFileDigests[key] = digest
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
for _, directory := range current.ReloadDirectories {
|
|
687
|
+
key := projectInputPathKey(directory)
|
|
688
|
+
if digest := baseline.ReloadDirectoryDigests[key]; digest != "" {
|
|
689
|
+
current.ReloadDirectoryDigests[key] = digest
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
func projectInputReloadFingerprintsAreCurrent(
|
|
695
|
+
snapshot LSPProjectInputSnapshot,
|
|
696
|
+
) bool {
|
|
697
|
+
for _, file := range snapshot.ReloadFiles {
|
|
698
|
+
key := projectInputPathKey(file)
|
|
699
|
+
if snapshot.ReloadFileDigests[key] != projectInputReloadFileDigest(file) {
|
|
700
|
+
return false
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
for _, directory := range snapshot.ReloadDirectories {
|
|
704
|
+
key := projectInputPathKey(directory)
|
|
705
|
+
if snapshot.ReloadDirectoryDigests[key] !=
|
|
706
|
+
projectInputReloadDirectoryDigest(directory) {
|
|
707
|
+
return false
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
return true
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
func projectInputSnapshotDigest(
|
|
714
|
+
fingerprints map[string]string,
|
|
715
|
+
location string,
|
|
716
|
+
) (string, bool) {
|
|
717
|
+
if fingerprints == nil {
|
|
718
|
+
return "", false
|
|
719
|
+
}
|
|
720
|
+
if digest, ok := fingerprints[location]; ok {
|
|
721
|
+
return digest, true
|
|
722
|
+
}
|
|
723
|
+
wanted := projectInputPathKey(location)
|
|
724
|
+
for candidate, digest := range fingerprints {
|
|
725
|
+
if projectInputPathKey(realProjectInputPath(candidate)) == wanted ||
|
|
726
|
+
projectInputPathKey(realProjectInputEntryPath(candidate)) == wanted {
|
|
727
|
+
return digest, true
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
return "", false
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
func validProjectInputFingerprint(digest string) bool {
|
|
734
|
+
if len(digest) != sha256.Size*2 || strings.ToLower(digest) != digest {
|
|
735
|
+
return false
|
|
736
|
+
}
|
|
737
|
+
for _, char := range digest {
|
|
738
|
+
if (char < '0' || char > '9') && (char < 'a' || char > 'f') {
|
|
739
|
+
return false
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return true
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
func projectInputReloadDirectoryDigest(directory string) string {
|
|
746
|
+
entries, err := os.ReadDir(projectInputFilesystemPath(directory))
|
|
747
|
+
if err != nil {
|
|
748
|
+
missing := sha256.Sum256([]byte("missing\x00"))
|
|
749
|
+
return fmt.Sprintf("%x", missing[:])
|
|
750
|
+
}
|
|
751
|
+
digest := sha256.New()
|
|
752
|
+
for index, entry := range entries {
|
|
753
|
+
kind := "other"
|
|
754
|
+
info, err := entry.Info()
|
|
755
|
+
if err != nil {
|
|
756
|
+
missing := sha256.Sum256([]byte("missing\x00"))
|
|
757
|
+
return fmt.Sprintf("%x", missing[:])
|
|
758
|
+
}
|
|
759
|
+
switch {
|
|
760
|
+
case info.Mode()&os.ModeSymlink != 0:
|
|
761
|
+
kind = "symlink"
|
|
762
|
+
case info.IsDir():
|
|
763
|
+
kind = "directory"
|
|
764
|
+
case info.Mode().IsRegular():
|
|
765
|
+
kind = "file"
|
|
766
|
+
}
|
|
767
|
+
target := ""
|
|
768
|
+
if kind == "symlink" {
|
|
769
|
+
target, err = os.Readlink(
|
|
770
|
+
filepath.Join(projectInputFilesystemPath(directory), entry.Name()),
|
|
771
|
+
)
|
|
772
|
+
if err != nil {
|
|
773
|
+
target = "<unreadable>"
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
digest.Write([]byte(entry.Name()))
|
|
777
|
+
digest.Write([]byte{0})
|
|
778
|
+
digest.Write([]byte(kind))
|
|
779
|
+
digest.Write([]byte{0})
|
|
780
|
+
digest.Write([]byte(target))
|
|
781
|
+
if index+1 != len(entries) {
|
|
782
|
+
digest.Write([]byte{0})
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
return fmt.Sprintf("%x", digest.Sum(nil))
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
func projectInputReloadFileDigest(location string) string {
|
|
789
|
+
native := projectInputFilesystemPath(location)
|
|
790
|
+
info, err := os.Lstat(native)
|
|
791
|
+
if err != nil {
|
|
792
|
+
missing := sha256.Sum256([]byte("missing\x00"))
|
|
793
|
+
return fmt.Sprintf("%x", missing[:])
|
|
794
|
+
}
|
|
795
|
+
if info.Mode()&os.ModeSymlink != 0 {
|
|
796
|
+
target, err := os.Readlink(native)
|
|
797
|
+
if err != nil {
|
|
798
|
+
target = "<unreadable>"
|
|
799
|
+
}
|
|
800
|
+
content := []byte("missing\x00")
|
|
801
|
+
if body, readErr := os.ReadFile(native); readErr == nil {
|
|
802
|
+
content = append([]byte("file\x00"), body...)
|
|
803
|
+
}
|
|
804
|
+
digest := sha256.New()
|
|
805
|
+
digest.Write([]byte("symlink\x00"))
|
|
806
|
+
digest.Write([]byte(target))
|
|
807
|
+
digest.Write([]byte{0})
|
|
808
|
+
digest.Write(content)
|
|
809
|
+
return fmt.Sprintf("%x", digest.Sum(nil))
|
|
810
|
+
}
|
|
811
|
+
if info.Mode().IsRegular() {
|
|
812
|
+
body, err := os.ReadFile(native)
|
|
813
|
+
if err != nil {
|
|
814
|
+
missing := sha256.Sum256([]byte("missing\x00"))
|
|
815
|
+
return fmt.Sprintf("%x", missing[:])
|
|
816
|
+
}
|
|
817
|
+
digest := sha256.New()
|
|
818
|
+
digest.Write([]byte("file\x00"))
|
|
819
|
+
digest.Write(body)
|
|
820
|
+
return fmt.Sprintf("%x", digest.Sum(nil))
|
|
821
|
+
}
|
|
822
|
+
other := sha256.Sum256([]byte("other\x00"))
|
|
823
|
+
return fmt.Sprintf("%x", other[:])
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
func realProjectInputPath(location string) string {
|
|
827
|
+
absolute, err := filepath.Abs(projectInputFilesystemPath(location))
|
|
828
|
+
if err != nil {
|
|
829
|
+
return filepath.Clean(filepath.FromSlash(location))
|
|
830
|
+
}
|
|
831
|
+
probe := absolute
|
|
832
|
+
suffix := []string{}
|
|
833
|
+
for {
|
|
834
|
+
resolved, err := filepath.EvalSymlinks(probe)
|
|
835
|
+
if err == nil {
|
|
836
|
+
for index := len(suffix) - 1; index >= 0; index-- {
|
|
837
|
+
resolved = filepath.Join(resolved, suffix[index])
|
|
838
|
+
}
|
|
839
|
+
return filepath.Clean(resolved)
|
|
840
|
+
}
|
|
841
|
+
parent := filepath.Dir(probe)
|
|
842
|
+
if parent == probe {
|
|
843
|
+
return filepath.Clean(absolute)
|
|
844
|
+
}
|
|
845
|
+
suffix = append(suffix, filepath.Base(probe))
|
|
846
|
+
probe = parent
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
func realProjectInputEntryPath(location string) string {
|
|
851
|
+
native := projectInputFilesystemPath(location)
|
|
852
|
+
return filepath.Join(
|
|
853
|
+
realProjectInputPath(filepath.Dir(native)),
|
|
854
|
+
filepath.Base(native),
|
|
855
|
+
)
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
func projectInputFilesystemPath(location string) string {
|
|
859
|
+
if runtime.GOOS != "windows" {
|
|
860
|
+
return filepath.FromSlash(location)
|
|
861
|
+
}
|
|
862
|
+
normalized := strings.ReplaceAll(location, "/", `\`)
|
|
863
|
+
if strings.HasPrefix(strings.ToLower(normalized), `\\?\unc\`) {
|
|
864
|
+
return `\\` + normalized[8:]
|
|
865
|
+
}
|
|
866
|
+
if strings.HasPrefix(normalized, `\\?\`) &&
|
|
867
|
+
isWindowsDrivePath(normalized[4:]) {
|
|
868
|
+
return normalized[4:]
|
|
869
|
+
}
|
|
870
|
+
return normalized
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
func projectInputPathKey(location string) string {
|
|
874
|
+
key := filepath.ToSlash(filepath.Clean(filepath.FromSlash(location)))
|
|
875
|
+
if runtime.GOOS == "windows" {
|
|
876
|
+
key = strings.ToLower(key)
|
|
877
|
+
}
|
|
878
|
+
return key
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
func matchProjectInputGlob(pattern []string, candidate []string) bool {
|
|
882
|
+
type position struct {
|
|
883
|
+
pattern int
|
|
884
|
+
candidate int
|
|
885
|
+
}
|
|
886
|
+
memo := map[position]bool{}
|
|
887
|
+
visited := map[position]bool{}
|
|
888
|
+
var visit func(int, int) bool
|
|
889
|
+
visit = func(patternIndex int, candidateIndex int) bool {
|
|
890
|
+
key := position{pattern: patternIndex, candidate: candidateIndex}
|
|
891
|
+
if visited[key] {
|
|
892
|
+
return memo[key]
|
|
893
|
+
}
|
|
894
|
+
visited[key] = true
|
|
895
|
+
var matched bool
|
|
896
|
+
switch {
|
|
897
|
+
case patternIndex == len(pattern):
|
|
898
|
+
matched = candidateIndex == len(candidate)
|
|
899
|
+
case pattern[patternIndex] == "**":
|
|
900
|
+
matched = visit(patternIndex+1, candidateIndex) ||
|
|
901
|
+
(candidateIndex != len(candidate) &&
|
|
902
|
+
visit(patternIndex, candidateIndex+1))
|
|
903
|
+
case candidateIndex != len(candidate):
|
|
904
|
+
matched = matchProjectInputGlobSegment(
|
|
905
|
+
pattern[patternIndex],
|
|
906
|
+
candidate[candidateIndex],
|
|
907
|
+
) && visit(patternIndex+1, candidateIndex+1)
|
|
908
|
+
}
|
|
909
|
+
memo[key] = matched
|
|
910
|
+
return matched
|
|
911
|
+
}
|
|
912
|
+
return visit(0, 0)
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
func matchProjectInputGlobSegment(pattern string, candidate string) bool {
|
|
916
|
+
expression := []rune(pattern)
|
|
917
|
+
input := []rune(candidate)
|
|
918
|
+
matches := make([][]bool, len(expression)+1)
|
|
919
|
+
for index := range matches {
|
|
920
|
+
matches[index] = make([]bool, len(input)+1)
|
|
921
|
+
}
|
|
922
|
+
matches[0][0] = true
|
|
923
|
+
for patternIndex, char := range expression {
|
|
924
|
+
if char == '*' {
|
|
925
|
+
matches[patternIndex+1][0] = matches[patternIndex][0]
|
|
926
|
+
}
|
|
927
|
+
for inputIndex := range input {
|
|
928
|
+
switch char {
|
|
929
|
+
case '*':
|
|
930
|
+
matches[patternIndex+1][inputIndex+1] =
|
|
931
|
+
matches[patternIndex][inputIndex+1] ||
|
|
932
|
+
matches[patternIndex+1][inputIndex]
|
|
933
|
+
case '?':
|
|
934
|
+
matches[patternIndex+1][inputIndex+1] =
|
|
935
|
+
matches[patternIndex][inputIndex]
|
|
936
|
+
default:
|
|
937
|
+
matches[patternIndex+1][inputIndex+1] =
|
|
938
|
+
char == input[inputIndex] &&
|
|
939
|
+
matches[patternIndex][inputIndex]
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
return matches[len(expression)][len(input)]
|
|
944
|
+
}
|