ttsc 0.19.3 → 0.20.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 (170) hide show
  1. package/README.md +7 -5
  2. package/cmd/graphdump/main.go +2 -4
  3. package/cmd/ttsc/filter.go +12 -4
  4. package/cmd/ttsc/filter_resolves_flag_names_case_insensitively_test.go +53 -0
  5. package/cmd/ttsc/flags_gen.go +3 -3
  6. package/cmd/ttscgraph/dump.go +5 -7
  7. package/cmd/ttscgraph/run_dump_emits_only_checker_verified_member_relations_test.go +118 -0
  8. package/cmd/ttscgraph/serve.go +58 -308
  9. package/cmd/ttscgraph/serve_session_keeps_base_url_fallback_unchanged_test.go +49 -0
  10. package/cmd/ttscgraph/serve_session_keeps_esm_directory_candidate_unchanged_test.go +61 -0
  11. package/cmd/ttscgraph/serve_session_keeps_exports_blocked_file_candidates_unchanged_test.go +64 -0
  12. package/cmd/ttscgraph/serve_session_keeps_inactive_conditional_export_unchanged_test.go +66 -0
  13. package/cmd/ttscgraph/serve_session_keeps_lower_priority_module_candidate_unchanged_test.go +67 -0
  14. package/cmd/ttscgraph/serve_session_keeps_lower_priority_paths_pattern_unchanged_test.go +58 -0
  15. package/cmd/ttscgraph/serve_session_keeps_nonlongest_root_dirs_candidate_unchanged_test.go +54 -0
  16. package/cmd/ttscgraph/serve_session_reloads_commonjs_package_entry_for_esm_test.go +50 -0
  17. package/cmd/ttscgraph/serve_session_reloads_module_suffix_candidate_test.go +41 -0
  18. package/cmd/ttscgraph/serve_session_reloads_superseding_module_candidates_test.go +216 -0
  19. package/cmd/ttscgraph/serve_session_retries_failed_dump_test.go +103 -0
  20. package/driver/graph.go +12 -6
  21. package/driver/lsp.go +30 -0
  22. package/driver/program.go +37 -1
  23. package/driver/resolution_candidates.go +819 -0
  24. package/driver/windowsjunction/windowsjunction.go +40 -0
  25. package/go.mod +2 -0
  26. package/internal/graph/build.go +45 -11
  27. package/internal/graph/dump.go +276 -100
  28. package/internal/graph/dump_checkout_paths_are_stable_test.go +210 -0
  29. package/internal/graph/dump_diagnostics_ride_the_generation_that_built_it_test.go +9 -3
  30. package/internal/graph/dump_paths.go +172 -0
  31. package/internal/graph/dump_paths_are_portable_and_injective_test.go +93 -0
  32. package/internal/graph/dump_uses_ecma_line_terminators_test.go +87 -0
  33. package/internal/graph/duplicate_variable_object_members_keep_first_declaration_test.go +61 -0
  34. package/internal/graph/edges.go +208 -0
  35. package/internal/graph/graph.go +143 -5
  36. package/internal/graph/line_model.go +122 -0
  37. package/internal/graph/member_relation_edges_follow_checker_validity_test.go +85 -0
  38. package/internal/graph/member_relation_edges_preserve_checker_boundaries_test.go +191 -0
  39. package/internal/graph/member_relation_edges_reject_checker_invalid_pairs_test.go +119 -0
  40. package/internal/graph/node_ids_preserve_hash_bearing_components_test.go +41 -0
  41. package/internal/graph/node_modifiers_emit_union_strings_test.go +4 -1
  42. package/internal/graph/object_literal_members_ride_on_the_snapshot_test.go +192 -0
  43. package/internal/graph/object_member_signatures_preserve_literal_whitespace_test.go +67 -0
  44. package/internal/graph/object_members.go +229 -0
  45. package/internal/graph/provenance.go +88 -30
  46. package/internal/graph/source_texts_cover_every_resident_program_source_test.go +73 -0
  47. package/internal/graphsymbols/position_helpers_follow_ecma_line_terminators_test.go +66 -0
  48. package/internal/graphsymbols/provider.go +26 -91
  49. package/internal/lspserver/lsp_clean_didopen_refreshes_compiler_state_test.go +144 -0
  50. package/internal/lspserver/lsp_completion.go +109 -0
  51. package/internal/lspserver/lsp_completion_gate_never_refuses_a_matching_line_test.go +50 -0
  52. package/internal/lspserver/lsp_completion_hints_longest_trigger_wins_test.go +149 -0
  53. package/internal/lspserver/lsp_completion_hints_nearest_trigger_wins_test.go +37 -0
  54. package/internal/lspserver/lsp_completion_merges_into_upstream_response_test.go +149 -0
  55. package/internal/lspserver/lsp_completion_nearest_hint_replaces_nearest_filter_test.go +43 -0
  56. package/internal/lspserver/lsp_completion_preserves_completion_list_extensions_test.go +99 -0
  57. package/internal/lspserver/lsp_completion_refuses_hints_inside_a_string_literal_test.go +69 -0
  58. package/internal/lspserver/lsp_completion_replaces_full_filter_in_rank_order_test.go +85 -0
  59. package/internal/lspserver/lsp_completion_request.go +455 -0
  60. package/internal/lspserver/lsp_completion_scope.go +530 -0
  61. package/internal/lspserver/lsp_completion_scope_bench_test.go +41 -0
  62. package/internal/lspserver/lsp_completion_scope_classifies_regex_statement_and_operator_boundaries_test.go +39 -0
  63. package/internal/lspserver/lsp_completion_scope_ignores_jsdoc_bytes_in_literals_test.go +108 -0
  64. package/internal/lspserver/lsp_completion_scope_refuses_regex_hints_test.go +52 -0
  65. package/internal/lspserver/lsp_completion_trigger_uses_last_rune_test.go +24 -0
  66. package/internal/lspserver/lsp_deleted_watched_file_withdraws_plugin_diagnostics_test.go +110 -0
  67. package/internal/lspserver/lsp_didsave_keeps_the_incremental_changed_uri_path_test.go +78 -0
  68. package/internal/lspserver/lsp_hints_absence_is_not_an_error_test.go +46 -0
  69. package/internal/lspserver/lsp_hints_refresh_coalesces_concurrent_requests_test.go +82 -0
  70. package/internal/lspserver/lsp_hints_refresh_ignores_a_superseded_generation_test.go +40 -0
  71. package/internal/lspserver/lsp_hints_refresh_keeps_each_producer_isolated_test.go +64 -0
  72. package/internal/lspserver/lsp_hints_refresh_replaces_the_session_snapshot_test.go +66 -0
  73. package/internal/lspserver/lsp_hints_refresh_scheduled_by_editor_events_test.go +90 -0
  74. package/internal/lspserver/lsp_hints_refresh_serves_completion_while_running_test.go +91 -0
  75. package/internal/lspserver/lsp_index_byte_from_bounds_its_search_start_test.go +38 -0
  76. package/internal/lspserver/lsp_initialize_constrains_position_encoding_to_utf16_test.go +205 -0
  77. package/internal/lspserver/lsp_late_completion_trigger_is_reported_once_test.go +105 -0
  78. package/internal/lspserver/lsp_native_completion_hints_decode_flat_lint_wire_test.go +66 -0
  79. package/internal/lspserver/lsp_native_completion_hints_grouped_compatibility_boundary_test.go +60 -0
  80. package/internal/lspserver/lsp_native_plugin_source.go +315 -2
  81. package/internal/lspserver/lsp_native_resident.go +304 -0
  82. package/internal/lspserver/lsp_plugin.go +85 -8
  83. package/internal/lspserver/lsp_plugin_completion_resolve_stays_local_test.go +82 -0
  84. package/internal/lspserver/lsp_position_conversions_span_astral_and_combining_runes_test.go +119 -0
  85. package/internal/lspserver/lsp_proxy.go +601 -9
  86. package/internal/lspserver/lsp_refresh.go +70 -0
  87. package/internal/lspserver/lsp_resident_falls_back_when_serve_unsupported_test.go +43 -0
  88. package/internal/lspserver/lsp_resident_maps_verb_args_test.go +34 -0
  89. package/internal/lspserver/lsp_server.go +13 -1
  90. package/internal/lspserver/lsp_watched_file_changes_reach_compiler_caches_test.go +172 -0
  91. package/lib/TtscCompiler.d.ts +3 -1
  92. package/lib/TtscCompiler.js +5 -1
  93. package/lib/TtscCompiler.js.map +1 -1
  94. package/lib/TtscService.d.ts +13 -3
  95. package/lib/TtscService.js +6 -6
  96. package/lib/TtscService.js.map +1 -1
  97. package/lib/compiler/internal/project/readConfigJson.d.ts +30 -0
  98. package/lib/compiler/internal/project/readConfigJson.js +208 -0
  99. package/lib/compiler/internal/project/readConfigJson.js.map +1 -0
  100. package/lib/compiler/internal/project/readProjectConfig.js +53 -149
  101. package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
  102. package/lib/compiler/internal/residentTransformProcess.d.ts +15 -2
  103. package/lib/compiler/internal/residentTransformProcess.js +159 -39
  104. package/lib/compiler/internal/residentTransformProcess.js.map +1 -1
  105. package/lib/compiler/internal/runBuild.d.ts +6 -0
  106. package/lib/compiler/internal/runBuild.js +143 -44
  107. package/lib/compiler/internal/runBuild.js.map +1 -1
  108. package/lib/compiler/internal/startResidentTransform.d.ts +5 -1
  109. package/lib/compiler/internal/startResidentTransform.js +3 -1
  110. package/lib/compiler/internal/startResidentTransform.js.map +1 -1
  111. package/lib/compiler/internal/transformProjectInMemory.js +12 -2
  112. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  113. package/lib/flags/parser.d.ts +19 -0
  114. package/lib/flags/parser.js +78 -36
  115. package/lib/flags/parser.js.map +1 -1
  116. package/lib/flags/schema.d.ts +49 -11
  117. package/lib/flags/schema.js +94 -22
  118. package/lib/flags/schema.js.map +1 -1
  119. package/lib/internal/assertSafeExplicitCacheDirectory.d.ts +5 -0
  120. package/lib/internal/assertSafeExplicitCacheDirectory.js +26 -0
  121. package/lib/internal/assertSafeExplicitCacheDirectory.js.map +1 -0
  122. package/lib/launcher/internal/runTtsc.js +107 -69
  123. package/lib/launcher/internal/runTtsc.js.map +1 -1
  124. package/lib/launcher/internal/runTtsx.js +28 -41
  125. package/lib/launcher/internal/runTtsx.js.map +1 -1
  126. package/lib/launcher/internal/runtimeHooks.d.ts +15 -0
  127. package/lib/launcher/internal/runtimeHooks.js +18 -1
  128. package/lib/launcher/internal/runtimeHooks.js.map +1 -1
  129. package/lib/launcher/internal/watchTopology.d.ts +43 -0
  130. package/lib/launcher/internal/watchTopology.js +360 -0
  131. package/lib/launcher/internal/watchTopology.js.map +1 -0
  132. package/lib/plugin/internal/buildSourcePlugin.d.ts +14 -3
  133. package/lib/plugin/internal/buildSourcePlugin.js +247 -61
  134. package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
  135. package/lib/plugin/internal/loadProjectPlugins.js +63 -2
  136. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  137. package/lib/structures/ITtscCompilerTransformation.d.ts +16 -0
  138. package/lib/structures/internal/ITtscParsedProjectConfig.d.ts +2 -0
  139. package/package.json +8 -8
  140. package/shim/ast/shim.go +48 -3
  141. package/shim/astnav/go.mod +14 -0
  142. package/shim/astnav/go.sum +20 -0
  143. package/shim/astnav/shim.go +15 -0
  144. package/shim/checker/shim.go +100 -0
  145. package/shim/core/shim.go +47 -0
  146. package/shim/diagnosticwriter/lint.go +50 -0
  147. package/src/TtscCompiler.ts +5 -1
  148. package/src/TtscService.ts +37 -10
  149. package/src/compiler/internal/project/readConfigJson.ts +209 -0
  150. package/src/compiler/internal/project/readProjectConfig.ts +62 -158
  151. package/src/compiler/internal/residentTransformProcess.ts +197 -39
  152. package/src/compiler/internal/runBuild.ts +169 -47
  153. package/src/compiler/internal/startResidentTransform.ts +12 -1
  154. package/src/compiler/internal/transformProjectInMemory.ts +12 -1
  155. package/src/flags/parser.ts +113 -42
  156. package/src/flags/schema.ts +113 -31
  157. package/src/internal/assertSafeExplicitCacheDirectory.ts +29 -0
  158. package/src/launcher/internal/runTtsc.ts +117 -68
  159. package/src/launcher/internal/runTtsx.ts +29 -44
  160. package/src/launcher/internal/runtimeHooks.ts +30 -2
  161. package/src/launcher/internal/watchTopology.ts +466 -0
  162. package/src/plugin/internal/buildSourcePlugin.ts +326 -64
  163. package/src/plugin/internal/loadProjectPlugins.ts +80 -2
  164. package/src/structures/ITtscCompilerTransformation.ts +17 -0
  165. package/src/structures/internal/ITtscParsedProjectConfig.ts +2 -0
  166. package/tools/shim_audit/baseline.json +0 -3
  167. package/tools/shim_audit/main.go +42 -14
  168. package/tools/shim_audit/shim_dir_coverage_test.go +25 -0
  169. package/utility/flags_gen.go +3 -3
  170. package/utility/host.go +9 -3
@@ -0,0 +1,530 @@
1
+ package lspserver
2
+
3
+ // lexicalScope names the kind of token a byte offset sits in.
4
+ //
5
+ // It is what a scanner can decide without a parse: comments, string and
6
+ // template literals, and regular expression literals all carry text that is not
7
+ // code, and every one of them can hold bytes that look like a comment
8
+ // delimiter.
9
+ type lexicalScope int
10
+
11
+ const (
12
+ lexicalScopeCode lexicalScope = iota
13
+ lexicalScopeLineComment
14
+ lexicalScopeBlockComment
15
+ lexicalScopeJSDoc
16
+ lexicalScopeString
17
+ lexicalScopeTemplate
18
+ lexicalScopeRegex
19
+ )
20
+
21
+ func (scope lexicalScope) String() string {
22
+ switch scope {
23
+ case lexicalScopeLineComment:
24
+ return "line comment"
25
+ case lexicalScopeBlockComment:
26
+ return "block comment"
27
+ case lexicalScopeJSDoc:
28
+ return "jsdoc"
29
+ case lexicalScopeString:
30
+ return "string"
31
+ case lexicalScopeTemplate:
32
+ return "template"
33
+ case lexicalScopeRegex:
34
+ return "regex"
35
+ default:
36
+ return "code"
37
+ }
38
+ }
39
+
40
+ // cursorInJSDoc reports whether an offset sits inside a `/** */` block.
41
+ //
42
+ // Line comments are not considered: `// @evidence` is not a doc comment and a
43
+ // tag there means nothing to the rule that published the hint.
44
+ func cursorInJSDoc(text string, offset int) bool {
45
+ if offset < 0 || offset > len(text) {
46
+ return false
47
+ }
48
+ return lexicalScopeAt(text, offset) == lexicalScopeJSDoc
49
+ }
50
+
51
+ // regexContext retains the token boundary needed to classify the next slash.
52
+ // It is deliberately smaller than a parser: the scope scanner needs only to
53
+ // skip opaque regex text correctly, never to validate the surrounding program.
54
+ type regexContext struct {
55
+ kind regexPredecessor
56
+ last byte
57
+
58
+ lastAdjacent bool
59
+ word string
60
+ previousWord string
61
+ beforePreviousWord string
62
+ wordMember bool
63
+ previousMember bool
64
+ wordExpression bool
65
+ previousExpression bool
66
+
67
+ pendingFunction bool
68
+ functionExpression bool
69
+ functionBody bool
70
+ functionBodyExpression bool
71
+ classExpressions []bool
72
+
73
+ parens []regexParen
74
+ braces []bool
75
+ }
76
+
77
+ type regexParen struct {
78
+ control bool
79
+ function bool
80
+ expression bool
81
+ }
82
+
83
+ type regexPredecessor int
84
+
85
+ const (
86
+ regexPredecessorStart regexPredecessor = iota
87
+ regexPredecessorOperator
88
+ regexPredecessorWord
89
+ regexPredecessorValue
90
+ regexPredecessorControlHeader
91
+ )
92
+
93
+ func (context *regexContext) allowedAfter() bool {
94
+ switch context.kind {
95
+ case regexPredecessorValue:
96
+ return false
97
+ case regexPredecessorWord:
98
+ word, member := context.lastWord()
99
+ return regexKeywordPrecedes(word, member)
100
+ default:
101
+ return true
102
+ }
103
+ }
104
+ func (context *regexContext) writeValue(symbol byte) {
105
+ context.finishWord()
106
+ context.kind = regexPredecessorValue
107
+ context.last = symbol
108
+ context.lastAdjacent = true
109
+ }
110
+
111
+ func (context *regexContext) separator() {
112
+ context.finishWord()
113
+ context.lastAdjacent = false
114
+ }
115
+
116
+ func (context *regexContext) writeCode(symbol byte) {
117
+ if isIdentifierByte(symbol) {
118
+ if context.word == "" {
119
+ context.wordExpression = context.expressionPosition()
120
+ context.wordMember = context.last == '.'
121
+ }
122
+ context.word += string(symbol)
123
+ context.kind = regexPredecessorWord
124
+ context.last = symbol
125
+ context.lastAdjacent = true
126
+ return
127
+ }
128
+ context.finishWord()
129
+ if symbol == ':' && context.kind == regexPredecessorWord {
130
+ word, member := context.lastWord()
131
+ if !member && word == "function" {
132
+ context.pendingFunction = false
133
+ } else if !member && word == "class" && len(context.classExpressions) > 0 {
134
+ context.classExpressions = context.classExpressions[:len(context.classExpressions)-1]
135
+ }
136
+ }
137
+ switch symbol {
138
+ case '(':
139
+ paren := regexParen{control: context.controlHeaderPrecedes()}
140
+ if context.pendingFunction {
141
+ paren.function = true
142
+ paren.expression = context.functionExpression
143
+ context.pendingFunction = false
144
+ }
145
+ context.parens = append(context.parens, paren)
146
+ context.kind = regexPredecessorOperator
147
+ case ')':
148
+ paren := regexParen{}
149
+ if depth := len(context.parens); depth > 0 {
150
+ paren = context.parens[depth-1]
151
+ context.parens = context.parens[:depth-1]
152
+ }
153
+ if paren.function {
154
+ context.functionBody = true
155
+ context.functionBodyExpression = paren.expression
156
+ }
157
+ if paren.control {
158
+ context.kind = regexPredecessorControlHeader
159
+ } else {
160
+ context.kind = regexPredecessorValue
161
+ }
162
+ case '{':
163
+ context.braces = append(context.braces, context.blockPrecedes())
164
+ context.kind = regexPredecessorOperator
165
+ case '}':
166
+ block := false
167
+ if depth := len(context.braces); depth > 0 {
168
+ block = context.braces[depth-1]
169
+ context.braces = context.braces[:depth-1]
170
+ }
171
+ if block {
172
+ context.kind = regexPredecessorControlHeader
173
+ } else {
174
+ context.kind = regexPredecessorValue
175
+ }
176
+ case ']':
177
+ context.kind = regexPredecessorValue
178
+ case '+', '-':
179
+ if context.lastAdjacent && context.last == symbol {
180
+ context.kind = regexPredecessorValue
181
+ } else {
182
+ context.kind = regexPredecessorOperator
183
+ }
184
+ default:
185
+ context.kind = regexPredecessorOperator
186
+ }
187
+ context.last = symbol
188
+ context.lastAdjacent = true
189
+ }
190
+
191
+ func (context *regexContext) blockPrecedes() bool {
192
+ if context.functionBody {
193
+ context.functionBody = false
194
+ return !context.functionBodyExpression
195
+ }
196
+ if context.classBodyPrecedes() {
197
+ depth := len(context.classExpressions) - 1
198
+ expression := context.classExpressions[depth]
199
+ context.classExpressions = context.classExpressions[:depth]
200
+ return !expression
201
+ }
202
+ if context.kind == regexPredecessorControlHeader {
203
+ return true
204
+ }
205
+ if context.kind == regexPredecessorStart {
206
+ return true
207
+ }
208
+ if context.kind == regexPredecessorWord {
209
+ word, _ := context.lastWord()
210
+ switch word {
211
+ case "do", "else", "finally", "try":
212
+ return true
213
+ }
214
+ }
215
+ return context.last == ';' || context.last == '{' || context.last == '}'
216
+ }
217
+
218
+ func (context *regexContext) classBodyPrecedes() bool {
219
+ if len(context.classExpressions) == 0 || len(context.parens) != 0 {
220
+ return false
221
+ }
222
+ return context.kind == regexPredecessorWord ||
223
+ context.kind == regexPredecessorValue
224
+ }
225
+
226
+ func (context *regexContext) controlHeaderPrecedes() bool {
227
+ word, member := context.lastWord()
228
+ if member {
229
+ return false
230
+ }
231
+ switch word {
232
+ case "if", "while", "for", "with", "switch", "catch":
233
+ return true
234
+ case "await":
235
+ return context.beforePreviousWord == "for"
236
+ default:
237
+ return false
238
+ }
239
+ }
240
+
241
+ func (context *regexContext) expressionPosition() bool {
242
+ switch context.kind {
243
+ case regexPredecessorStart, regexPredecessorControlHeader:
244
+ return false
245
+ case regexPredecessorValue:
246
+ return false
247
+ case regexPredecessorWord:
248
+ word, member := context.lastWord()
249
+ if word == "async" {
250
+ return context.previousExpression
251
+ }
252
+ return regexKeywordPrecedes(word, member)
253
+ default:
254
+ return context.last != ';' && context.last != '{' && context.last != '}'
255
+ }
256
+ }
257
+
258
+ func (context *regexContext) finishWord() {
259
+ if context.word == "" {
260
+ return
261
+ }
262
+ if context.word == "function" && !context.wordMember {
263
+ context.pendingFunction = true
264
+ context.functionExpression = context.wordExpression
265
+ } else if context.word == "class" && !context.wordMember {
266
+ context.classExpressions = append(context.classExpressions, context.wordExpression)
267
+ }
268
+ context.beforePreviousWord = context.previousWord
269
+ context.previousWord = context.word
270
+ context.previousMember = context.wordMember
271
+ context.previousExpression = context.wordExpression
272
+ context.word = ""
273
+ context.wordMember = false
274
+ }
275
+
276
+ func (context *regexContext) lastWord() (string, bool) {
277
+ if context.word != "" {
278
+ return context.word, context.wordMember
279
+ }
280
+ return context.previousWord, context.previousMember
281
+ }
282
+
283
+ // lexicalScopeAt reports the scope of the byte at offset, scanning forward from
284
+ // the start of the document.
285
+ //
286
+ // A backward search for the nearest `/**` cannot answer the question, because
287
+ // the same three bytes appear in `const example = "/** @par"` — and answering
288
+ // "jsdoc" there offers a JSDoc rule's tags in the middle of a string literal.
289
+ // Comment state is not a property of the bytes before the cursor; it is a
290
+ // property of the token stream that produced them, so the scan starts where the
291
+ // token stream does.
292
+ //
293
+ // It stays a scanner rather than a parse. The proxy holds text, not a tree, and
294
+ // asking tsgo would put a round trip on the keystroke path — the exact cost this
295
+ // design exists to avoid. Forward scanning costs one pass over the prefix, the
296
+ // same complexity the backward search it replaced already had: a few
297
+ // milliseconds per megabyte, so well under a millisecond for an ordinary source
298
+ // file (see BenchmarkCursorInJSDoc). That is small enough beside the upstream
299
+ // completion round trip the same request makes, so the scan is recomputed per
300
+ // request instead of a span cache having to be invalidated per didChange.
301
+ //
302
+ // Recovery matters as much as recognition here, because the buffer is live and
303
+ // therefore usually not valid TypeScript. An unterminated string ends at the
304
+ // line break, exactly as the TypeScript scanner recovers, so a stray apostrophe
305
+ // mid-edit cannot swallow every JSDoc block below it.
306
+ //
307
+ // One boundary stays with the scanner: JSX element text is read as code, so
308
+ // `/**` typed between JSX tags still opens a comment here. Separating JSX text
309
+ // from a comparison operator needs the grammar, which is the parse this pass
310
+ // exists to avoid, and the position is far rarer than the literals above.
311
+ func lexicalScopeAt(text string, offset int) lexicalScope {
312
+ if offset > len(text) {
313
+ offset = len(text)
314
+ }
315
+ scope := lexicalScopeCode
316
+ quote := byte(0)
317
+ // braces counts the `{` depth of the current code region, and templateBraces
318
+ // remembers the enclosing depth per open `${`, so the `}` that ends an
319
+ // interpolation is told apart from the `}` that ends an object literal in it.
320
+ braces := 0
321
+ var templateBraces []int
322
+ // regex tracks delimiter and value boundaries so a slash after a control
323
+ // header or division operator is not confused with one after a value.
324
+ regex := regexContext{}
325
+
326
+ index := 0
327
+ for index < offset {
328
+ symbol := text[index]
329
+ switch scope {
330
+ case lexicalScopeCode:
331
+ // A switch on the byte itself, so the overwhelmingly common case — a byte
332
+ // that starts no literal and no comment — costs one jump rather than a
333
+ // walk through every delimiter test.
334
+ switch symbol {
335
+ case '/':
336
+ next := byte(0)
337
+ if index+1 < len(text) {
338
+ next = text[index+1]
339
+ }
340
+ switch {
341
+ case next == '*':
342
+ regex.separator()
343
+ // `/**/` is an empty block comment, not a JSDoc block; TypeScript
344
+ // reads the third `*` as part of the terminator.
345
+ if index+2 < len(text) && text[index+2] == '*' &&
346
+ (index+3 >= len(text) || text[index+3] != '/') {
347
+ scope = lexicalScopeJSDoc
348
+ } else {
349
+ scope = lexicalScopeBlockComment
350
+ }
351
+ index += 2
352
+ case next == '/':
353
+ regex.separator()
354
+ scope = lexicalScopeLineComment
355
+ index += 2
356
+ default:
357
+ // A comment opener already won above, so this `/` can only be
358
+ // division or a regex literal. Skipping the literal keeps a class
359
+ // such as `/["'/*]/` from opening a string or a comment that the
360
+ // source never wrote.
361
+ end := -1
362
+ if regex.allowedAfter() {
363
+ end = regexLiteralEnd(text, index)
364
+ }
365
+ if end == -1 {
366
+ // Division, or no terminator before the line ended.
367
+ regex.writeCode(symbol)
368
+ index++
369
+ break
370
+ }
371
+ if offset < end {
372
+ return lexicalScopeRegex
373
+ }
374
+ regex.writeValue(symbol)
375
+ index = end
376
+ }
377
+ case '"', '\'':
378
+ scope = lexicalScopeString
379
+ quote = symbol
380
+ regex.writeValue(symbol)
381
+ index++
382
+ case '`':
383
+ scope = lexicalScopeTemplate
384
+ regex.writeValue(symbol)
385
+ index++
386
+ case '{':
387
+ braces++
388
+ regex.writeCode(symbol)
389
+ index++
390
+ case '}':
391
+ if braces > 0 {
392
+ braces--
393
+ } else if depth := len(templateBraces); depth > 0 {
394
+ braces = templateBraces[depth-1]
395
+ templateBraces = templateBraces[:depth-1]
396
+ scope = lexicalScopeTemplate
397
+ }
398
+ regex.writeCode(symbol)
399
+ index++
400
+ default:
401
+ // Every whitespace byte is below `' '`, and so is every control byte
402
+ // that could not precede a regex either.
403
+ if symbol > ' ' {
404
+ regex.writeCode(symbol)
405
+ } else {
406
+ regex.separator()
407
+ }
408
+ index++
409
+ }
410
+ case lexicalScopeLineComment:
411
+ // CR ends a line for TypeScript too, so a CR-only buffer cannot leave the
412
+ // rest of the file inside one `//`.
413
+ if symbol == '\n' || symbol == '\r' {
414
+ scope = lexicalScopeCode
415
+ regex.separator()
416
+ }
417
+ index++
418
+ case lexicalScopeBlockComment, lexicalScopeJSDoc:
419
+ if symbol == '*' && index+1 < len(text) && text[index+1] == '/' {
420
+ scope = lexicalScopeCode
421
+ index += 2
422
+ break
423
+ }
424
+ index++
425
+ case lexicalScopeString:
426
+ switch {
427
+ case symbol == '\\':
428
+ index += escapeWidth(text, index)
429
+ case symbol == quote:
430
+ scope = lexicalScopeCode
431
+ regex.writeValue(symbol)
432
+ index++
433
+ case symbol == '\n' || symbol == '\r':
434
+ scope = lexicalScopeCode
435
+ regex.separator()
436
+ index++
437
+ default:
438
+ index++
439
+ }
440
+ case lexicalScopeTemplate:
441
+ switch {
442
+ case symbol == '\\':
443
+ index += escapeWidth(text, index)
444
+ case symbol == '`':
445
+ scope = lexicalScopeCode
446
+ regex.writeValue(symbol)
447
+ index++
448
+ case symbol == '$' && index+1 < len(text) && text[index+1] == '{':
449
+ templateBraces = append(templateBraces, braces)
450
+ braces = 0
451
+ scope = lexicalScopeCode
452
+ regex.writeCode('{')
453
+ index += 2
454
+ default:
455
+ index++
456
+ }
457
+ }
458
+ }
459
+ return scope
460
+ }
461
+
462
+ // escapeWidth returns how many bytes a backslash escape consumes, counting a
463
+ // CRLF line continuation as one unit so the `\n` cannot be mistaken for the
464
+ // unterminated-literal recovery point.
465
+ func escapeWidth(text string, index int) int {
466
+ if index+2 < len(text) && text[index+1] == '\r' && text[index+2] == '\n' {
467
+ return 3
468
+ }
469
+ return 2
470
+ }
471
+
472
+ func regexKeywordPrecedes(word string, member bool) bool {
473
+ if member {
474
+ // `in`, `of`, `new`, and the rest are legal property names, and a member
475
+ // access is a value: `obj.in / 2` divides.
476
+ return false
477
+ }
478
+ switch word {
479
+ case "await", "case", "delete", "do", "else", "extends", "in", "instanceof", "new",
480
+ "of", "return", "throw", "typeof", "void", "yield":
481
+ return true
482
+ default:
483
+ return false
484
+ }
485
+ }
486
+
487
+ // regexLiteralEnd returns the offset just past a regular expression literal
488
+ // that starts at index, or -1 when the text is not one.
489
+ //
490
+ // A regex literal cannot span a line, so an unterminated scan means the `/` was
491
+ // division after all and the caller resumes there. The `[...]` class is tracked
492
+ // because an unescaped `/` inside one does not terminate the literal.
493
+ func regexLiteralEnd(text string, index int) int {
494
+ inClass := false
495
+ for cursor := index + 1; cursor < len(text); cursor++ {
496
+ switch text[cursor] {
497
+ case '\\':
498
+ cursor++
499
+ case '\n', '\r':
500
+ return -1
501
+ case '[':
502
+ inClass = true
503
+ case ']':
504
+ inClass = false
505
+ case '/':
506
+ if !inClass {
507
+ // Flags are identifier bytes; code scope reads them harmlessly.
508
+ return cursor + 1
509
+ }
510
+ }
511
+ }
512
+ return -1
513
+ }
514
+
515
+ func isIdentifierByte(symbol byte) bool {
516
+ switch {
517
+ case symbol >= 'a' && symbol <= 'z':
518
+ return true
519
+ case symbol >= 'A' && symbol <= 'Z':
520
+ return true
521
+ case symbol >= '0' && symbol <= '9':
522
+ return true
523
+ case symbol == '_' || symbol == '$':
524
+ return true
525
+ default:
526
+ // Every byte of a non-ASCII rune belongs to an identifier or to literal
527
+ // text; either way it is not an operator that could precede a regex.
528
+ return symbol >= 0x80
529
+ }
530
+ }
@@ -0,0 +1,41 @@
1
+ package lspserver
2
+
3
+ import (
4
+ "strings"
5
+ "testing"
6
+ )
7
+
8
+ // BenchmarkCursorInJSDoc measures the scope decision on the completion hot path.
9
+ //
10
+ // Completion asks it per request on the live buffer, so its cost has to stay
11
+ // small against the round trip to tsgo that the same request makes. The buffer
12
+ // here is deliberately larger than a real source file and the cursor sits at its
13
+ // end, which is the worst case for a forward scan: the whole prefix is walked.
14
+ //
15
+ // Usage:
16
+ //
17
+ // go test ./internal/lspserver -run=^$ -bench=^BenchmarkCursorInJSDoc$
18
+ func BenchmarkCursorInJSDoc(b *testing.B) {
19
+ chunk := strings.Join([]string{
20
+ "/**",
21
+ " * Greets one user.",
22
+ " * @param name user name",
23
+ " */",
24
+ "export function greet(name: string): string {",
25
+ " const quoted = \"/** not a doc comment */\";",
26
+ " const pattern = /[\"'/*]/;",
27
+ " return `Hello, ${name}${quoted.length / 2}`;",
28
+ "}",
29
+ "",
30
+ }, "\n")
31
+ text := strings.Repeat(chunk, 1+1024*1024/len(chunk))
32
+ text += "\n/**\n * @par"
33
+
34
+ b.SetBytes(int64(len(text)))
35
+ b.ResetTimer()
36
+ for range b.N {
37
+ if !cursorInJSDoc(text, len(text)) {
38
+ b.Fatal("the trailing doc comment must be in scope")
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,39 @@
1
+ package lspserver
2
+
3
+ import "testing"
4
+
5
+ // TestLSPCompletionScopeClassifiesRegexStatementAndOperatorBoundaries keeps
6
+ // JSDoc-shaped bytes inside regexes from escaping into completion scope while
7
+ // preserving real comments after expressions and live-buffer recovery.
8
+ func TestLSPCompletionScopeClassifiesRegexStatementAndOperatorBoundaries(t *testing.T) {
9
+ cases := []struct {
10
+ name string
11
+ text string
12
+ want lexicalScope
13
+ }{
14
+ {"if header", "if (ok) /[/** @tag]/.test(value)", lexicalScopeCode},
15
+ {"commented if header", "if /* c */ (ok) /[/** @tag]/.test(value)", lexicalScopeCode},
16
+ {"while header", "while (ok) /[/** @tag]/.test(value)", lexicalScopeCode},
17
+ {"for header", "for (; ok;) /[/** @tag]/.test(value)", lexicalScopeCode},
18
+ {"with header", "with (scope) /[/** @tag]/.test(value)", lexicalScopeCode},
19
+ {"regex after division", "const value = left / /[/** @tag]/.source", lexicalScopeCode},
20
+ {"regex after spaced unary plus", "const value = left + +/[/** @tag]/.source", lexicalScopeCode},
21
+ {"regex after commented return", "return /* c */ /[/** @tag]/.test(value)", lexicalScopeCode},
22
+ {"function declaration block", "function f() {} /[/** @tag]/.test(value)", lexicalScopeCode},
23
+ {"class declaration block", "class C {} /[/** @tag]/.test(value)", lexicalScopeCode},
24
+ {"class heritage object argument", "class C extends mixin({}) {} /[/** @tag]/.test(value)", lexicalScopeCode},
25
+ {"member named class", "const member = object.class; const value = {} / /[/** @tag]/.source", lexicalScopeCode},
26
+ {"object property named class", "const { class: member } = object; const value = {} / /[/** @tag]/.source", lexicalScopeCode},
27
+ {"member named like a control keyword", "const value = object.if() / 2; /** @tag", lexicalScopeJSDoc},
28
+ {"division after regex", "const value = /ok/ / 2; /** @tag", lexicalScopeJSDoc},
29
+ {"comment after expression", "const value = 1; /** @tag", lexicalScopeJSDoc},
30
+ {"template interpolation", "const value = `${(() => { if (ok) /[/** @tag]/.test(value); return value; })()}`; /** @tag", lexicalScopeJSDoc},
31
+ {"cr recovery", "const bad = \"oops\r/** @tag", lexicalScopeJSDoc},
32
+ }
33
+
34
+ for _, entry := range cases {
35
+ if got := lexicalScopeAt(entry.text, len(entry.text)); got != entry.want {
36
+ t.Errorf("%s: scope = %s, want %s", entry.name, got, entry.want)
37
+ }
38
+ }
39
+ }