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