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.
- package/README.md +7 -5
- package/cmd/graphdump/main.go +2 -4
- package/cmd/ttsc/filter.go +12 -4
- package/cmd/ttsc/filter_resolves_flag_names_case_insensitively_test.go +53 -0
- package/cmd/ttsc/flags_gen.go +3 -3
- package/cmd/ttscgraph/dump.go +5 -7
- package/cmd/ttscgraph/run_dump_emits_only_checker_verified_member_relations_test.go +118 -0
- package/cmd/ttscgraph/serve.go +58 -308
- package/cmd/ttscgraph/serve_session_keeps_base_url_fallback_unchanged_test.go +49 -0
- package/cmd/ttscgraph/serve_session_keeps_esm_directory_candidate_unchanged_test.go +61 -0
- package/cmd/ttscgraph/serve_session_keeps_exports_blocked_file_candidates_unchanged_test.go +64 -0
- package/cmd/ttscgraph/serve_session_keeps_inactive_conditional_export_unchanged_test.go +66 -0
- package/cmd/ttscgraph/serve_session_keeps_lower_priority_module_candidate_unchanged_test.go +67 -0
- package/cmd/ttscgraph/serve_session_keeps_lower_priority_paths_pattern_unchanged_test.go +58 -0
- package/cmd/ttscgraph/serve_session_keeps_nonlongest_root_dirs_candidate_unchanged_test.go +54 -0
- package/cmd/ttscgraph/serve_session_reloads_commonjs_package_entry_for_esm_test.go +50 -0
- package/cmd/ttscgraph/serve_session_reloads_module_suffix_candidate_test.go +41 -0
- package/cmd/ttscgraph/serve_session_reloads_superseding_module_candidates_test.go +216 -0
- package/cmd/ttscgraph/serve_session_retries_failed_dump_test.go +103 -0
- package/driver/graph.go +12 -6
- package/driver/lsp.go +30 -0
- package/driver/program.go +37 -1
- package/driver/resolution_candidates.go +819 -0
- package/driver/windowsjunction/windowsjunction.go +40 -0
- package/go.mod +2 -0
- package/internal/graph/build.go +45 -11
- package/internal/graph/dump.go +276 -100
- package/internal/graph/dump_checkout_paths_are_stable_test.go +210 -0
- package/internal/graph/dump_diagnostics_ride_the_generation_that_built_it_test.go +9 -3
- package/internal/graph/dump_paths.go +172 -0
- package/internal/graph/dump_paths_are_portable_and_injective_test.go +93 -0
- package/internal/graph/dump_uses_ecma_line_terminators_test.go +87 -0
- package/internal/graph/duplicate_variable_object_members_keep_first_declaration_test.go +61 -0
- package/internal/graph/edges.go +208 -0
- package/internal/graph/graph.go +143 -5
- package/internal/graph/line_model.go +122 -0
- package/internal/graph/member_relation_edges_follow_checker_validity_test.go +85 -0
- package/internal/graph/member_relation_edges_preserve_checker_boundaries_test.go +191 -0
- package/internal/graph/member_relation_edges_reject_checker_invalid_pairs_test.go +119 -0
- package/internal/graph/node_ids_preserve_hash_bearing_components_test.go +41 -0
- package/internal/graph/node_modifiers_emit_union_strings_test.go +4 -1
- package/internal/graph/object_literal_members_ride_on_the_snapshot_test.go +192 -0
- package/internal/graph/object_member_signatures_preserve_literal_whitespace_test.go +67 -0
- package/internal/graph/object_members.go +229 -0
- package/internal/graph/provenance.go +88 -30
- package/internal/graph/source_texts_cover_every_resident_program_source_test.go +73 -0
- package/internal/graphsymbols/position_helpers_follow_ecma_line_terminators_test.go +66 -0
- package/internal/graphsymbols/provider.go +26 -91
- package/internal/lspserver/lsp_clean_didopen_refreshes_compiler_state_test.go +144 -0
- package/internal/lspserver/lsp_completion.go +109 -0
- package/internal/lspserver/lsp_completion_gate_never_refuses_a_matching_line_test.go +50 -0
- package/internal/lspserver/lsp_completion_hints_longest_trigger_wins_test.go +149 -0
- package/internal/lspserver/lsp_completion_hints_nearest_trigger_wins_test.go +37 -0
- package/internal/lspserver/lsp_completion_merges_into_upstream_response_test.go +149 -0
- package/internal/lspserver/lsp_completion_nearest_hint_replaces_nearest_filter_test.go +43 -0
- package/internal/lspserver/lsp_completion_preserves_completion_list_extensions_test.go +99 -0
- package/internal/lspserver/lsp_completion_refuses_hints_inside_a_string_literal_test.go +69 -0
- package/internal/lspserver/lsp_completion_replaces_full_filter_in_rank_order_test.go +85 -0
- package/internal/lspserver/lsp_completion_request.go +455 -0
- package/internal/lspserver/lsp_completion_scope.go +530 -0
- package/internal/lspserver/lsp_completion_scope_bench_test.go +41 -0
- package/internal/lspserver/lsp_completion_scope_classifies_regex_statement_and_operator_boundaries_test.go +39 -0
- package/internal/lspserver/lsp_completion_scope_ignores_jsdoc_bytes_in_literals_test.go +108 -0
- package/internal/lspserver/lsp_completion_scope_refuses_regex_hints_test.go +52 -0
- package/internal/lspserver/lsp_completion_trigger_uses_last_rune_test.go +24 -0
- package/internal/lspserver/lsp_deleted_watched_file_withdraws_plugin_diagnostics_test.go +110 -0
- package/internal/lspserver/lsp_didsave_keeps_the_incremental_changed_uri_path_test.go +78 -0
- package/internal/lspserver/lsp_hints_absence_is_not_an_error_test.go +46 -0
- package/internal/lspserver/lsp_hints_refresh_coalesces_concurrent_requests_test.go +82 -0
- package/internal/lspserver/lsp_hints_refresh_ignores_a_superseded_generation_test.go +40 -0
- package/internal/lspserver/lsp_hints_refresh_keeps_each_producer_isolated_test.go +64 -0
- package/internal/lspserver/lsp_hints_refresh_replaces_the_session_snapshot_test.go +66 -0
- package/internal/lspserver/lsp_hints_refresh_scheduled_by_editor_events_test.go +90 -0
- package/internal/lspserver/lsp_hints_refresh_serves_completion_while_running_test.go +91 -0
- package/internal/lspserver/lsp_index_byte_from_bounds_its_search_start_test.go +38 -0
- package/internal/lspserver/lsp_initialize_constrains_position_encoding_to_utf16_test.go +205 -0
- package/internal/lspserver/lsp_late_completion_trigger_is_reported_once_test.go +105 -0
- package/internal/lspserver/lsp_native_completion_hints_decode_flat_lint_wire_test.go +66 -0
- package/internal/lspserver/lsp_native_completion_hints_grouped_compatibility_boundary_test.go +60 -0
- package/internal/lspserver/lsp_native_plugin_source.go +315 -2
- package/internal/lspserver/lsp_native_resident.go +304 -0
- package/internal/lspserver/lsp_plugin.go +85 -8
- package/internal/lspserver/lsp_plugin_completion_resolve_stays_local_test.go +82 -0
- package/internal/lspserver/lsp_position_conversions_span_astral_and_combining_runes_test.go +119 -0
- package/internal/lspserver/lsp_proxy.go +601 -9
- package/internal/lspserver/lsp_refresh.go +70 -0
- package/internal/lspserver/lsp_resident_falls_back_when_serve_unsupported_test.go +43 -0
- package/internal/lspserver/lsp_resident_maps_verb_args_test.go +34 -0
- package/internal/lspserver/lsp_server.go +13 -1
- package/internal/lspserver/lsp_watched_file_changes_reach_compiler_caches_test.go +172 -0
- package/lib/TtscCompiler.d.ts +3 -1
- package/lib/TtscCompiler.js +5 -1
- package/lib/TtscCompiler.js.map +1 -1
- package/lib/TtscService.d.ts +13 -3
- package/lib/TtscService.js +6 -6
- package/lib/TtscService.js.map +1 -1
- package/lib/compiler/internal/project/readConfigJson.d.ts +30 -0
- package/lib/compiler/internal/project/readConfigJson.js +208 -0
- package/lib/compiler/internal/project/readConfigJson.js.map +1 -0
- package/lib/compiler/internal/project/readProjectConfig.js +53 -149
- package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
- package/lib/compiler/internal/residentTransformProcess.d.ts +15 -2
- package/lib/compiler/internal/residentTransformProcess.js +159 -39
- package/lib/compiler/internal/residentTransformProcess.js.map +1 -1
- package/lib/compiler/internal/runBuild.d.ts +6 -0
- package/lib/compiler/internal/runBuild.js +143 -44
- package/lib/compiler/internal/runBuild.js.map +1 -1
- package/lib/compiler/internal/startResidentTransform.d.ts +5 -1
- package/lib/compiler/internal/startResidentTransform.js +3 -1
- package/lib/compiler/internal/startResidentTransform.js.map +1 -1
- package/lib/compiler/internal/transformProjectInMemory.js +12 -2
- package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
- package/lib/flags/parser.d.ts +19 -0
- package/lib/flags/parser.js +78 -36
- package/lib/flags/parser.js.map +1 -1
- package/lib/flags/schema.d.ts +49 -11
- package/lib/flags/schema.js +94 -22
- package/lib/flags/schema.js.map +1 -1
- package/lib/internal/assertSafeExplicitCacheDirectory.d.ts +5 -0
- package/lib/internal/assertSafeExplicitCacheDirectory.js +26 -0
- package/lib/internal/assertSafeExplicitCacheDirectory.js.map +1 -0
- package/lib/launcher/internal/runTtsc.js +107 -69
- package/lib/launcher/internal/runTtsc.js.map +1 -1
- package/lib/launcher/internal/runTtsx.js +28 -41
- package/lib/launcher/internal/runTtsx.js.map +1 -1
- package/lib/launcher/internal/runtimeHooks.d.ts +15 -0
- package/lib/launcher/internal/runtimeHooks.js +18 -1
- package/lib/launcher/internal/runtimeHooks.js.map +1 -1
- package/lib/launcher/internal/watchTopology.d.ts +43 -0
- package/lib/launcher/internal/watchTopology.js +360 -0
- package/lib/launcher/internal/watchTopology.js.map +1 -0
- package/lib/plugin/internal/buildSourcePlugin.d.ts +14 -3
- package/lib/plugin/internal/buildSourcePlugin.js +247 -61
- package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
- package/lib/plugin/internal/loadProjectPlugins.js +63 -2
- package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
- package/lib/structures/ITtscCompilerTransformation.d.ts +16 -0
- package/lib/structures/internal/ITtscParsedProjectConfig.d.ts +2 -0
- package/package.json +8 -8
- package/shim/ast/shim.go +48 -3
- package/shim/astnav/go.mod +14 -0
- package/shim/astnav/go.sum +20 -0
- package/shim/astnav/shim.go +15 -0
- package/shim/checker/shim.go +100 -0
- package/shim/core/shim.go +47 -0
- package/shim/diagnosticwriter/lint.go +50 -0
- package/src/TtscCompiler.ts +5 -1
- package/src/TtscService.ts +37 -10
- package/src/compiler/internal/project/readConfigJson.ts +209 -0
- package/src/compiler/internal/project/readProjectConfig.ts +62 -158
- package/src/compiler/internal/residentTransformProcess.ts +197 -39
- package/src/compiler/internal/runBuild.ts +169 -47
- package/src/compiler/internal/startResidentTransform.ts +12 -1
- package/src/compiler/internal/transformProjectInMemory.ts +12 -1
- package/src/flags/parser.ts +113 -42
- package/src/flags/schema.ts +113 -31
- package/src/internal/assertSafeExplicitCacheDirectory.ts +29 -0
- package/src/launcher/internal/runTtsc.ts +117 -68
- package/src/launcher/internal/runTtsx.ts +29 -44
- package/src/launcher/internal/runtimeHooks.ts +30 -2
- package/src/launcher/internal/watchTopology.ts +466 -0
- package/src/plugin/internal/buildSourcePlugin.ts +326 -64
- package/src/plugin/internal/loadProjectPlugins.ts +80 -2
- package/src/structures/ITtscCompilerTransformation.ts +17 -0
- package/src/structures/internal/ITtscParsedProjectConfig.ts +2 -0
- package/tools/shim_audit/baseline.json +0 -3
- package/tools/shim_audit/main.go +42 -14
- package/tools/shim_audit/shim_dir_coverage_test.go +25 -0
- package/utility/flags_gen.go +3 -3
- 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
|
+
}
|