@ttsc/lint 0.18.4 → 0.19.1
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 +125 -43
- package/go.mod +5 -4
- package/internal/rulecode/rulecode.go +88 -0
- package/lib/index.d.ts +10 -1
- package/lib/index.js +45 -17
- package/lib/index.js.map +1 -1
- package/lib/structures/ITtscLintConfig.d.ts +3 -0
- package/lib/structures/TtscLintRuleSetting.d.ts +3 -2
- package/lib/structures/format/ITtscLintFormat.d.ts +3 -2
- package/lib/structures/format/ITtscLintFormatSortImports.d.ts +20 -4
- package/lib/structures/rules/ITtscLintBoundariesRuleOptions.d.ts +80 -8
- package/lib/structures/rules/ITtscLintBoundariesRules.d.ts +6 -8
- package/lib/structures/rules/ITtscLintContributorRules.d.ts +18 -2
- package/lib/structures/rules/ITtscLintCoreRuleOptions.d.ts +347 -0
- package/lib/structures/rules/ITtscLintCoreRuleOptions.js +3 -0
- package/lib/structures/rules/ITtscLintCoreRuleOptions.js.map +1 -0
- package/lib/structures/rules/ITtscLintCoreRules.d.ts +101 -67
- package/lib/structures/rules/ITtscLintRegexpRules.d.ts +7 -2
- package/lib/structures/rules/ITtscLintRuleOptionsMap.d.ts +53 -7
- package/lib/structures/rules/ITtscLintRules.d.ts +6 -2
- package/lib/structures/rules/ITtscLintSolidRules.d.ts +0 -8
- package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.d.ts +206 -0
- package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.js +8 -0
- package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.js.map +1 -0
- package/lib/structures/rules/ITtscLintTypeScriptRules.d.ts +55 -42
- package/lib/structures/rules/ITtscLintUnicornRuleOptions.d.ts +276 -0
- package/lib/structures/rules/ITtscLintUnicornRuleOptions.js +3 -0
- package/lib/structures/rules/ITtscLintUnicornRuleOptions.js.map +1 -0
- package/lib/structures/rules/ITtscLintUnicornRules.d.ts +44 -36
- package/lib/structures/rules/index.d.ts +3 -0
- package/lib/structures/rules/index.js +3 -0
- package/lib/structures/rules/index.js.map +1 -1
- package/linthost/ast_helpers.go +521 -23
- package/linthost/ast_selector_matcher.go +1628 -0
- package/linthost/ast_selector_parser.go +628 -0
- package/linthost/comment_scan.go +154 -0
- package/linthost/compile.go +129 -79
- package/linthost/config.go +352 -88
- package/linthost/config_format.go +15 -1
- package/linthost/contrib_adapter.go +47 -8
- package/linthost/declaration_rules.go +12 -11
- package/linthost/directives.go +9 -67
- package/linthost/dispatch.go +37 -3
- package/linthost/engine.go +368 -53
- package/linthost/escape_string.go +77 -0
- package/linthost/fix.go +133 -23
- package/linthost/flags_gen.go +18 -17
- package/linthost/format.go +73 -167
- package/linthost/format_editor_settings.go +134 -26
- package/linthost/grapheme_tables_gen.go +1920 -0
- package/linthost/host.go +159 -61
- package/linthost/literal_escape_scan.go +117 -0
- package/linthost/lsp.go +545 -98
- package/linthost/numeric_literal_precision.go +228 -0
- package/linthost/polyfill_browserslist.go +1439 -0
- package/linthost/polyfill_browserslist_config.go +603 -0
- package/linthost/polyfill_corejs.go +750 -0
- package/linthost/polyfill_data.go +113 -0
- package/linthost/polyfill_data_gen.json +39868 -0
- package/linthost/print_nodes_array.go +7 -1
- package/linthost/print_nodes_object.go +7 -1
- package/linthost/project_engine.go +231 -0
- package/linthost/project_rules.go +78 -0
- package/linthost/regex_clean.go +191 -0
- package/linthost/regex_tree.go +1544 -0
- package/linthost/regex_tree_optimizer.go +1685 -0
- package/linthost/regexp_cache.go +44 -0
- package/linthost/rule_codes.go +105 -0
- package/linthost/rule_codes.json +745 -0
- package/linthost/rules_ban_ts_comment.go +312 -0
- package/linthost/rules_boundaries.go +128 -43
- package/linthost/rules_boundaries_dependencies.go +1013 -0
- package/linthost/rules_core_extra.go +62 -92
- package/linthost/rules_cypress.go +1 -1
- package/linthost/rules_default_case.go +87 -11
- package/linthost/rules_dupes.go +38 -13
- package/linthost/rules_empty.go +188 -20
- package/linthost/rules_escape.go +62 -48
- package/linthost/rules_format_arrow_parens.go +19 -5
- package/linthost/rules_format_bracket_spacing.go +1 -1
- package/linthost/rules_format_clause_join.go +1 -1
- package/linthost/rules_format_declaration_header.go +31 -19
- package/linthost/rules_format_indent.go +1 -1
- package/linthost/rules_format_jsdoc.go +1 -1
- package/linthost/rules_format_orphan_semi.go +1 -1
- package/linthost/rules_format_parameter_properties.go +17 -7
- package/linthost/rules_format_print_width.go +1 -1
- package/linthost/rules_format_quote_props.go +1 -1
- package/linthost/rules_format_quotes.go +1 -1
- package/linthost/rules_format_semi.go +106 -79
- package/linthost/rules_format_sort_imports.go +79 -45
- package/linthost/rules_format_statement_split.go +1 -1
- package/linthost/rules_format_ternary_nullish_parens.go +1 -1
- package/linthost/rules_format_trailing_comma.go +64 -1
- package/linthost/rules_format_whitespace.go +1 -1
- package/linthost/rules_functional.go +21 -22
- package/linthost/rules_gap.go +80 -3
- package/linthost/rules_grouped_accessor_pairs.go +84 -25
- package/linthost/rules_guard_for_in.go +64 -147
- package/linthost/rules_jsx_a11y.go +232 -22
- package/linthost/rules_logic.go +84 -38
- package/linthost/rules_nextjs.go +105 -16
- package/linthost/rules_no_class_assign.go +70 -0
- package/linthost/rules_no_duplicate_imports.go +266 -0
- package/linthost/rules_no_else_return.go +156 -38
- package/linthost/rules_no_extend_native.go +113 -17
- package/linthost/rules_no_fallthrough.go +1013 -0
- package/linthost/rules_no_func_assign.go +70 -0
- package/linthost/rules_no_import_assign.go +278 -86
- package/linthost/rules_no_inner_declarations.go +186 -0
- package/linthost/rules_no_loop_func.go +438 -53
- package/linthost/rules_no_mixed_operators.go +208 -52
- package/linthost/rules_no_param_reassign.go +376 -61
- package/linthost/rules_no_promise_executor_return.go +156 -0
- package/linthost/rules_no_restricted_imports.go +1013 -57
- package/linthost/rules_no_restricted_syntax.go +190 -26
- package/linthost/rules_no_useless_constructor.go +6 -36
- package/linthost/rules_problems.go +154 -311
- package/linthost/rules_promise.go +2578 -109
- package/linthost/rules_react_extras.go +71 -45
- package/linthost/rules_react_hooks.go +18 -2
- package/linthost/rules_react_perf.go +1 -0
- package/linthost/rules_react_refresh.go +1 -1
- package/linthost/rules_regexp.go +230 -14
- package/linthost/rules_security.go +45 -19
- package/linthost/rules_self.go +21 -1
- package/linthost/rules_solid.go +13 -7
- package/linthost/rules_storybook.go +1 -1
- package/linthost/rules_suggestions.go +490 -84
- package/linthost/rules_testing_library.go +12 -1
- package/linthost/rules_ts.go +227 -54
- package/linthost/rules_ts_async.go +0 -131
- package/linthost/rules_ts_no_base_to_string.go +3 -4
- package/linthost/rules_ts_no_misused_promises.go +817 -0
- package/linthost/rules_ts_no_restricted_types.go +320 -34
- package/linthost/rules_ts_no_unnecessary_qualifier.go +132 -55
- package/linthost/rules_ts_no_unsafe_assignment.go +608 -60
- package/linthost/rules_ts_require_array_sort_compare.go +5 -5
- package/linthost/rules_ts_switch_exhaustiveness_check.go +556 -86
- package/linthost/rules_unicorn_better_regex.go +224 -16
- package/linthost/rules_unicorn_consistent_destructuring.go +1002 -7
- package/linthost/rules_unicorn_consistent_existence_index_check.go +208 -29
- package/linthost/rules_unicorn_consistent_function_scoping.go +1041 -8
- package/linthost/rules_unicorn_consistent_template_literal_escape.go +178 -7
- package/linthost/rules_unicorn_escape_case.go +30 -19
- package/linthost/rules_unicorn_filename_case.go +763 -16
- package/linthost/rules_unicorn_import_style.go +783 -17
- package/linthost/rules_unicorn_isolated_functions.go +934 -14
- package/linthost/rules_unicorn_no_hex_escape.go +26 -11
- package/linthost/rules_unicorn_no_typeof_undefined.go +234 -25
- package/linthost/rules_unicorn_no_unnecessary_polyfills.go +376 -21
- package/linthost/rules_unicorn_no_unused_properties.go +756 -19
- package/linthost/rules_unicorn_number_literal_case.go +58 -39
- package/linthost/rules_unicorn_prefer_at.go +7 -3
- package/linthost/rules_unicorn_prefer_number_properties.go +381 -48
- package/linthost/rules_unicorn_prefer_optional_catch_binding.go +24 -24
- package/linthost/rules_unicorn_prefer_simple_condition_first.go +597 -19
- package/linthost/rules_unicorn_prefer_string_raw.go +147 -16
- package/linthost/rules_unicorn_prevent_abbreviations.go +1963 -95
- package/linthost/rules_unicorn_string_content.go +611 -15
- package/linthost/rules_unicorn_switch_case_break_position.go +172 -15
- package/linthost/rules_unicorn_template_indent.go +923 -18
- package/linthost/rules_unicorn_text_encoding_identifier_case.go +202 -42
- package/linthost/rules_unicorn_throw_new_error.go +218 -26
- package/linthost/rules_var.go +829 -142
- package/linthost/string_length.go +222 -0
- package/package.json +4 -3
- package/rule/project.go +206 -0
- package/rule/rule.go +58 -1
- package/src/index.ts +58 -17
- package/src/structures/ITtscLintConfig.ts +3 -0
- package/src/structures/TtscLintRuleSetting.ts +3 -2
- package/src/structures/format/ITtscLintFormat.ts +3 -2
- package/src/structures/format/ITtscLintFormatSortImports.ts +21 -4
- package/src/structures/rules/ITtscLintBoundariesRuleOptions.ts +122 -8
- package/src/structures/rules/ITtscLintBoundariesRules.ts +6 -8
- package/src/structures/rules/ITtscLintContributorRules.ts +18 -2
- package/src/structures/rules/ITtscLintCoreRuleOptions.ts +465 -0
- package/src/structures/rules/ITtscLintCoreRules.ts +122 -67
- package/src/structures/rules/ITtscLintRegexpRules.ts +7 -2
- package/src/structures/rules/ITtscLintRuleOptionsMap.ts +88 -7
- package/src/structures/rules/ITtscLintRules.ts +7 -2
- package/src/structures/rules/ITtscLintSolidRules.ts +0 -9
- package/src/structures/rules/ITtscLintTypeScriptRuleOptions.ts +248 -0
- package/src/structures/rules/ITtscLintTypeScriptRules.ts +64 -42
- package/src/structures/rules/ITtscLintUnicornRuleOptions.ts +350 -0
- package/src/structures/rules/ITtscLintUnicornRules.ts +60 -36
- package/src/structures/rules/index.ts +3 -0
package/linthost/lsp.go
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
package linthost
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
+
"context"
|
|
5
|
+
"crypto/sha256"
|
|
4
6
|
"encoding/json"
|
|
5
7
|
"errors"
|
|
6
8
|
"flag"
|
|
@@ -17,11 +19,14 @@ import (
|
|
|
17
19
|
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
18
20
|
shimcore "github.com/microsoft/typescript-go/shim/core"
|
|
19
21
|
shimparser "github.com/microsoft/typescript-go/shim/parser"
|
|
22
|
+
|
|
23
|
+
publicrule "github.com/samchon/ttsc/packages/lint/rule"
|
|
20
24
|
)
|
|
21
25
|
|
|
22
26
|
const (
|
|
23
|
-
commandLintFixAll
|
|
24
|
-
|
|
27
|
+
commandLintFixAll = "ttsc.lint.fixAll"
|
|
28
|
+
commandLintApplySuggestion = "ttsc.lint.applySuggestion"
|
|
29
|
+
commandFormatDocument = "ttsc.format.document"
|
|
25
30
|
)
|
|
26
31
|
|
|
27
32
|
type lspPosition struct {
|
|
@@ -34,6 +39,16 @@ type lspRange struct {
|
|
|
34
39
|
End lspPosition `json:"end"`
|
|
35
40
|
}
|
|
36
41
|
|
|
42
|
+
type lspPositionWire struct {
|
|
43
|
+
Line *int `json:"line"`
|
|
44
|
+
Character *int `json:"character"`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type lspRangeWire struct {
|
|
48
|
+
Start *lspPositionWire `json:"start"`
|
|
49
|
+
End *lspPositionWire `json:"end"`
|
|
50
|
+
}
|
|
51
|
+
|
|
37
52
|
type lspDiagnostic struct {
|
|
38
53
|
Range lspRange `json:"range"`
|
|
39
54
|
Severity int `json:"severity,omitempty"`
|
|
@@ -68,6 +83,27 @@ type lspWorkspaceEdit struct {
|
|
|
68
83
|
Changes map[string][]lspTextEdit `json:"changes,omitempty"`
|
|
69
84
|
}
|
|
70
85
|
|
|
86
|
+
type lspSuggestionSelection struct {
|
|
87
|
+
Rule string `json:"rule"`
|
|
88
|
+
Message string `json:"message"`
|
|
89
|
+
Pos int `json:"pos"`
|
|
90
|
+
End int `json:"end"`
|
|
91
|
+
SuggestionIndex int `json:"suggestionIndex"`
|
|
92
|
+
Title string `json:"title"`
|
|
93
|
+
SourceHash string `json:"sourceHash"`
|
|
94
|
+
Fingerprint string `json:"fingerprint"`
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
type lspProjectDiagnostics struct {
|
|
98
|
+
URI string `json:"uri"`
|
|
99
|
+
Diagnostics []lspDiagnostic `json:"diagnostics"`
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
type lspDiagnosticsResult struct {
|
|
103
|
+
Document []lspDiagnostic `json:"document"`
|
|
104
|
+
Project *lspProjectDiagnostics `json:"project,omitempty"`
|
|
105
|
+
}
|
|
106
|
+
|
|
71
107
|
type lspCommandOptions struct {
|
|
72
108
|
argumentsJSON string
|
|
73
109
|
command string
|
|
@@ -79,24 +115,26 @@ type lspCommandOptions struct {
|
|
|
79
115
|
contentStdin bool
|
|
80
116
|
cwd string
|
|
81
117
|
pluginsJSON string
|
|
82
|
-
// rangeJSON
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
tsconfig string
|
|
89
|
-
uri string
|
|
118
|
+
// rangeJSON carries the editor selection used to limit quickfix.ttsc
|
|
119
|
+
// actions. Source fix-all and format actions remain document-wide.
|
|
120
|
+
rangeJSON string
|
|
121
|
+
tsconfig string
|
|
122
|
+
uri string
|
|
123
|
+
projectIdentity publicrule.ProjectIdentity
|
|
90
124
|
}
|
|
91
125
|
|
|
92
126
|
// RunLSPCommandIDs prints the workspace/executeCommand ids owned by @ttsc/lint.
|
|
93
127
|
func RunLSPCommandIDs([]string) int {
|
|
94
|
-
return writeJSON([]string{
|
|
128
|
+
return writeJSON([]string{
|
|
129
|
+
commandLintFixAll,
|
|
130
|
+
commandLintApplySuggestion,
|
|
131
|
+
commandFormatDocument,
|
|
132
|
+
})
|
|
95
133
|
}
|
|
96
134
|
|
|
97
135
|
// RunLSPCodeActionKinds prints the CodeActionKind values @ttsc/lint may return.
|
|
98
136
|
func RunLSPCodeActionKinds([]string) int {
|
|
99
|
-
return writeJSON([]string{"source.fixAll.ttsc", "source.format"})
|
|
137
|
+
return writeJSON([]string{"quickfix.ttsc", "source.fixAll.ttsc", "source.format"})
|
|
100
138
|
}
|
|
101
139
|
|
|
102
140
|
// RunLSPDiagnostics prints lint diagnostics for one file URI as LSP JSON.
|
|
@@ -105,18 +143,28 @@ func RunLSPDiagnostics(args []string) int {
|
|
|
105
143
|
if !ok {
|
|
106
144
|
return 2
|
|
107
145
|
}
|
|
108
|
-
findings,
|
|
146
|
+
findings, prog, closeProgram, code := lspFindings(opts, false)
|
|
109
147
|
if closeProgram != nil {
|
|
110
148
|
defer closeProgram()
|
|
111
149
|
}
|
|
112
150
|
if code != 0 {
|
|
113
151
|
return code
|
|
114
152
|
}
|
|
115
|
-
|
|
116
|
-
for _, finding := range findings {
|
|
117
|
-
|
|
153
|
+
result := lspDiagnosticsResult{Document: []lspDiagnostic{}}
|
|
154
|
+
for _, finding := range filterFindingsForPath(findings, mustFilePathFromURI(opts.uri)) {
|
|
155
|
+
result.Document = append(result.Document, findingToLSPDiagnostic(finding))
|
|
118
156
|
}
|
|
119
|
-
|
|
157
|
+
if prog != nil && prog.projectCycle != nil && len(prog.projectCycle.results.byName) > 0 {
|
|
158
|
+
publication := &lspProjectDiagnostics{
|
|
159
|
+
URI: fileURL(prog.identity.LogicalConfigPath),
|
|
160
|
+
Diagnostics: []lspDiagnostic{},
|
|
161
|
+
}
|
|
162
|
+
for _, finding := range prog.projectCycle.findings {
|
|
163
|
+
publication.Diagnostics = append(publication.Diagnostics, findingToLSPDiagnostic(finding))
|
|
164
|
+
}
|
|
165
|
+
result.Project = publication
|
|
166
|
+
}
|
|
167
|
+
return writeJSON(result)
|
|
120
168
|
}
|
|
121
169
|
|
|
122
170
|
// RunLSPCodeActions prints code actions available for one file URI/range.
|
|
@@ -126,8 +174,13 @@ func RunLSPCodeActions(args []string) int {
|
|
|
126
174
|
return 2
|
|
127
175
|
}
|
|
128
176
|
acceptsLint := acceptsActionKind(opts.contextJSON, "source.fixAll.ttsc")
|
|
177
|
+
acceptsQuickFix := acceptsActionKind(opts.contextJSON, "quickfix.ttsc")
|
|
129
178
|
acceptsFormat := acceptsActionKind(opts.contextJSON, "source.format")
|
|
130
|
-
|
|
179
|
+
quickFixRange, quickFixRangeOK := parseRequestedLSPRange(opts.rangeJSON)
|
|
180
|
+
if acceptsQuickFix && !quickFixRangeOK {
|
|
181
|
+
acceptsQuickFix = false
|
|
182
|
+
}
|
|
183
|
+
if !acceptsLint && !acceptsQuickFix && !acceptsFormat {
|
|
131
184
|
return writeJSON([]lspCodeAction{})
|
|
132
185
|
}
|
|
133
186
|
if lspProjectTargetHasSegment(opts, "node_modules") {
|
|
@@ -143,9 +196,13 @@ func RunLSPCodeActions(args []string) int {
|
|
|
143
196
|
if code != 0 {
|
|
144
197
|
return code
|
|
145
198
|
}
|
|
199
|
+
findings = filterFindingsForPath(findings, mustFilePathFromURI(opts.uri))
|
|
146
200
|
lintFindings := filterLintFindings(findings)
|
|
147
201
|
formatFindings := filterFormatFindings(findings)
|
|
148
202
|
var actions []lspCodeAction
|
|
203
|
+
if acceptsQuickFix {
|
|
204
|
+
actions = append(actions, lspSuggestionCodeActions(opts, lintFindings, quickFixRange)...)
|
|
205
|
+
}
|
|
149
206
|
if acceptsLint && hasFixableFinding(lintFindings) {
|
|
150
207
|
uriArg, _ := json.Marshal(opts.uri)
|
|
151
208
|
actions = append(actions, lspCodeAction{
|
|
@@ -180,7 +237,9 @@ func RunLSPExecuteCommand(args []string) int {
|
|
|
180
237
|
if !ok {
|
|
181
238
|
return 2
|
|
182
239
|
}
|
|
183
|
-
if opts.command != commandLintFixAll &&
|
|
240
|
+
if opts.command != commandLintFixAll &&
|
|
241
|
+
opts.command != commandLintApplySuggestion &&
|
|
242
|
+
opts.command != commandFormatDocument {
|
|
184
243
|
fmt.Fprintf(os.Stderr, "@ttsc/lint lsp-execute-command: unknown command %q\n", opts.command)
|
|
185
244
|
return 2
|
|
186
245
|
}
|
|
@@ -190,6 +249,13 @@ func RunLSPExecuteCommand(args []string) int {
|
|
|
190
249
|
return 2
|
|
191
250
|
}
|
|
192
251
|
opts.uri = uri
|
|
252
|
+
if opts.command == commandLintApplySuggestion {
|
|
253
|
+
edit, code := lspWorkspaceEditForSuggestion(opts)
|
|
254
|
+
if code != 0 {
|
|
255
|
+
return code
|
|
256
|
+
}
|
|
257
|
+
return writeJSON(edit)
|
|
258
|
+
}
|
|
193
259
|
// --content-stdin selects the lightweight in-memory format path: the full
|
|
194
260
|
// document buffer is read from stdin and formatted with AST+source rules
|
|
195
261
|
// only, with no temp-workspace copy and no tsgo Program. It applies to
|
|
@@ -221,6 +287,7 @@ func parseLSPCommandOptions(name string, args []string) (*lspCommandOptions, boo
|
|
|
221
287
|
cwd := fs.String("cwd", "", "")
|
|
222
288
|
tsconfig := fs.String("tsconfig", "tsconfig.json", "")
|
|
223
289
|
pluginsJSON := fs.String("plugins-json", "", "")
|
|
290
|
+
projectContextJSON := fs.String("project-context-json", "", "")
|
|
224
291
|
uri := fs.String("uri", "", "")
|
|
225
292
|
rangeJSON := fs.String("range-json", "", "")
|
|
226
293
|
contextJSON := fs.String("context-json", "", "")
|
|
@@ -235,16 +302,22 @@ func parseLSPCommandOptions(name string, args []string) (*lspCommandOptions, boo
|
|
|
235
302
|
fmt.Fprintln(os.Stderr, err)
|
|
236
303
|
return nil, false
|
|
237
304
|
}
|
|
305
|
+
projectIdentity, err := decodeProjectIdentity(*projectContextJSON)
|
|
306
|
+
if err != nil {
|
|
307
|
+
fmt.Fprintln(os.Stderr, err)
|
|
308
|
+
return nil, false
|
|
309
|
+
}
|
|
238
310
|
return &lspCommandOptions{
|
|
239
|
-
argumentsJSON:
|
|
240
|
-
command:
|
|
241
|
-
contextJSON:
|
|
242
|
-
contentStdin:
|
|
243
|
-
cwd:
|
|
244
|
-
pluginsJSON:
|
|
245
|
-
rangeJSON:
|
|
246
|
-
tsconfig:
|
|
247
|
-
uri:
|
|
311
|
+
argumentsJSON: *argumentsJSON,
|
|
312
|
+
command: *command,
|
|
313
|
+
contextJSON: *contextJSON,
|
|
314
|
+
contentStdin: *contentStdin,
|
|
315
|
+
cwd: resolvedCwd,
|
|
316
|
+
pluginsJSON: *pluginsJSON,
|
|
317
|
+
rangeJSON: *rangeJSON,
|
|
318
|
+
tsconfig: *tsconfig,
|
|
319
|
+
uri: *uri,
|
|
320
|
+
projectIdentity: projectIdentity,
|
|
248
321
|
}, true
|
|
249
322
|
}
|
|
250
323
|
|
|
@@ -258,18 +331,26 @@ func lspFindings(opts *lspCommandOptions, includeFormatDefaults bool) ([]*Findin
|
|
|
258
331
|
fmt.Fprintln(os.Stderr, err)
|
|
259
332
|
return nil, nil, nil, 2
|
|
260
333
|
}
|
|
261
|
-
rules, err :=
|
|
334
|
+
rules, err := loadLSPCommandRules(
|
|
335
|
+
opts.pluginsJSON,
|
|
336
|
+
opts.cwd,
|
|
337
|
+
opts.tsconfig,
|
|
338
|
+
target,
|
|
339
|
+
includeFormatDefaults,
|
|
340
|
+
)
|
|
262
341
|
if err != nil {
|
|
263
342
|
fmt.Fprintln(os.Stderr, err)
|
|
264
343
|
return nil, nil, nil, 2
|
|
265
344
|
}
|
|
266
|
-
if includeFormatDefaults {
|
|
267
|
-
rules = formatCommandResolver{inner: rules}
|
|
268
|
-
}
|
|
269
345
|
engine := NewEngineWithResolver(rules)
|
|
346
|
+
if err := engine.ConfigError(); err != nil {
|
|
347
|
+
fmt.Fprintln(os.Stderr, err)
|
|
348
|
+
return nil, nil, nil, 2
|
|
349
|
+
}
|
|
270
350
|
prog, parseDiags, err := loadProgram(opts.cwd, opts.tsconfig, loadProgramOptions{
|
|
271
351
|
forceNoEmit: true,
|
|
272
352
|
needsRuleChecker: engine.NeedsTypeChecker(),
|
|
353
|
+
projectIdentity: opts.projectIdentity,
|
|
273
354
|
})
|
|
274
355
|
if err != nil {
|
|
275
356
|
fmt.Fprintf(os.Stderr, "@ttsc/lint: %v\n", err)
|
|
@@ -279,18 +360,53 @@ func lspFindings(opts *lspCommandOptions, includeFormatDefaults bool) ([]*Findin
|
|
|
279
360
|
// tsgo already owns parse diagnostics in the upstream LSP process.
|
|
280
361
|
return nil, prog, prog.close, 0
|
|
281
362
|
}
|
|
282
|
-
findings :=
|
|
363
|
+
findings := prog.runLintCycle(engine)
|
|
283
364
|
return findings, prog, prog.close, 0
|
|
284
365
|
}
|
|
285
366
|
|
|
367
|
+
// loadLSPCommandRules constructs the resolver shared by every LSP command
|
|
368
|
+
// path. Format requests use the same missing-config fallback, documented
|
|
369
|
+
// defaults, editor overrides, and language precedence as the CLI formatter and
|
|
370
|
+
// in-memory buffer formatter; lint-only requests retain the strict lint loader.
|
|
371
|
+
// formatTarget is the real editor document even when cwd/tsconfig point at the
|
|
372
|
+
// temporary workspace used by the disk execute-command path.
|
|
373
|
+
func loadLSPCommandRules(
|
|
374
|
+
pluginsJSON string,
|
|
375
|
+
cwd string,
|
|
376
|
+
tsconfigPath string,
|
|
377
|
+
formatTarget string,
|
|
378
|
+
includeFormatDefaults bool,
|
|
379
|
+
) (RuleResolver, error) {
|
|
380
|
+
if !includeFormatDefaults {
|
|
381
|
+
return loadRules(pluginsJSON, cwd, tsconfigPath)
|
|
382
|
+
}
|
|
383
|
+
rules, err := loadFormatRules(pluginsJSON, cwd, tsconfigPath)
|
|
384
|
+
if err != nil {
|
|
385
|
+
return nil, err
|
|
386
|
+
}
|
|
387
|
+
return newFormatCommandResolver(
|
|
388
|
+
rules,
|
|
389
|
+
filepath.Dir(formatTarget),
|
|
390
|
+
vscodeLanguageID(formatTarget),
|
|
391
|
+
)
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
func mustFilePathFromURI(uri string) string {
|
|
395
|
+
target, err := filePathFromURI(uri)
|
|
396
|
+
if err != nil {
|
|
397
|
+
return ""
|
|
398
|
+
}
|
|
399
|
+
return target
|
|
400
|
+
}
|
|
401
|
+
|
|
286
402
|
func filterFindingsForPath(findings []*Finding, target string) []*Finding {
|
|
287
|
-
target = canonicalProjectPath("", target)
|
|
403
|
+
target = canonicalProjectPath("", realProjectPath(target))
|
|
288
404
|
out := make([]*Finding, 0, len(findings))
|
|
289
405
|
for _, finding := range findings {
|
|
290
406
|
if finding == nil || finding.File == nil {
|
|
291
407
|
continue
|
|
292
408
|
}
|
|
293
|
-
if canonicalProjectPath("", finding.File.FileName()) == target {
|
|
409
|
+
if canonicalProjectPath("", realProjectPath(finding.File.FileName())) == target {
|
|
294
410
|
out = append(out, finding)
|
|
295
411
|
}
|
|
296
412
|
}
|
|
@@ -366,24 +482,267 @@ func acceptsActionKind(raw string, kind string) bool {
|
|
|
366
482
|
return false
|
|
367
483
|
}
|
|
368
484
|
|
|
369
|
-
func
|
|
485
|
+
func lspSuggestionCodeActions(
|
|
486
|
+
opts *lspCommandOptions,
|
|
487
|
+
findings []*Finding,
|
|
488
|
+
requestedRange lspRange,
|
|
489
|
+
) []lspCodeAction {
|
|
490
|
+
actions := make([]lspCodeAction, 0)
|
|
491
|
+
sourceHashes := make(map[*shimast.SourceFile]string)
|
|
492
|
+
for _, finding := range findings {
|
|
493
|
+
if finding == nil || finding.File == nil || len(finding.Suggestions) == 0 {
|
|
494
|
+
continue
|
|
495
|
+
}
|
|
496
|
+
if !lspRangesOverlap(requestedRange, lspRangeForFinding(finding)) {
|
|
497
|
+
continue
|
|
498
|
+
}
|
|
499
|
+
sourceHash, ok := sourceHashes[finding.File]
|
|
500
|
+
if !ok {
|
|
501
|
+
sourceHash = lspSourceHash(finding.File.Text())
|
|
502
|
+
sourceHashes[finding.File] = sourceHash
|
|
503
|
+
}
|
|
504
|
+
for index, suggestion := range finding.Suggestions {
|
|
505
|
+
if suggestion.Title == "" || len(suggestion.Edits) == 0 {
|
|
506
|
+
continue
|
|
507
|
+
}
|
|
508
|
+
uriArg, err := json.Marshal(opts.uri)
|
|
509
|
+
if err != nil {
|
|
510
|
+
continue
|
|
511
|
+
}
|
|
512
|
+
selectionArg, err := json.Marshal(lspSuggestionSelection{
|
|
513
|
+
Rule: finding.Rule,
|
|
514
|
+
Message: finding.Message,
|
|
515
|
+
Pos: finding.Pos,
|
|
516
|
+
End: finding.End,
|
|
517
|
+
SuggestionIndex: index,
|
|
518
|
+
Title: suggestion.Title,
|
|
519
|
+
SourceHash: sourceHash,
|
|
520
|
+
Fingerprint: lspSuggestionFingerprint(finding, suggestion),
|
|
521
|
+
})
|
|
522
|
+
if err != nil {
|
|
523
|
+
continue
|
|
524
|
+
}
|
|
525
|
+
actions = append(actions, lspCodeAction{
|
|
526
|
+
Title: suggestion.Title,
|
|
527
|
+
Kind: "quickfix.ttsc",
|
|
528
|
+
Command: &lspCommand{
|
|
529
|
+
Title: suggestion.Title,
|
|
530
|
+
Command: commandLintApplySuggestion,
|
|
531
|
+
Arguments: []json.RawMessage{uriArg, selectionArg},
|
|
532
|
+
},
|
|
533
|
+
})
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return actions
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
func parseRequestedLSPRange(raw string) (lspRange, bool) {
|
|
540
|
+
var wire lspRangeWire
|
|
541
|
+
if strings.TrimSpace(raw) == "" || json.Unmarshal([]byte(raw), &wire) != nil ||
|
|
542
|
+
wire.Start == nil || wire.End == nil ||
|
|
543
|
+
wire.Start.Line == nil || wire.Start.Character == nil ||
|
|
544
|
+
wire.End.Line == nil || wire.End.Character == nil {
|
|
545
|
+
return lspRange{}, false
|
|
546
|
+
}
|
|
547
|
+
requested := lspRange{
|
|
548
|
+
Start: lspPosition{Line: *wire.Start.Line, Character: *wire.Start.Character},
|
|
549
|
+
End: lspPosition{Line: *wire.End.Line, Character: *wire.End.Character},
|
|
550
|
+
}
|
|
551
|
+
if requested.Start.Line < 0 || requested.Start.Character < 0 ||
|
|
552
|
+
requested.End.Line < 0 || requested.End.Character < 0 ||
|
|
553
|
+
compareLSPPositions(requested.Start, requested.End) > 0 {
|
|
554
|
+
return lspRange{}, false
|
|
555
|
+
}
|
|
556
|
+
return requested, true
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
func lspRangesOverlap(left lspRange, right lspRange) bool {
|
|
560
|
+
if compareLSPPositions(left.Start, left.End) == 0 {
|
|
561
|
+
return compareLSPPositions(left.Start, right.Start) >= 0 &&
|
|
562
|
+
compareLSPPositions(left.Start, right.End) < 0
|
|
563
|
+
}
|
|
564
|
+
if compareLSPPositions(right.Start, right.End) == 0 {
|
|
565
|
+
return compareLSPPositions(right.Start, left.Start) >= 0 &&
|
|
566
|
+
compareLSPPositions(right.Start, left.End) < 0
|
|
567
|
+
}
|
|
568
|
+
return compareLSPPositions(left.Start, right.End) < 0 &&
|
|
569
|
+
compareLSPPositions(right.Start, left.End) < 0
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
func compareLSPPositions(left lspPosition, right lspPosition) int {
|
|
573
|
+
if left.Line < right.Line {
|
|
574
|
+
return -1
|
|
575
|
+
}
|
|
576
|
+
if left.Line > right.Line {
|
|
577
|
+
return 1
|
|
578
|
+
}
|
|
579
|
+
if left.Character < right.Character {
|
|
580
|
+
return -1
|
|
581
|
+
}
|
|
582
|
+
if left.Character > right.Character {
|
|
583
|
+
return 1
|
|
584
|
+
}
|
|
585
|
+
return 0
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
func lspSourceHash(source string) string {
|
|
589
|
+
return fmt.Sprintf("%x", sha256.Sum256([]byte(source)))
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
func lspSuggestionFingerprint(finding *Finding, suggestion Suggestion) string {
|
|
593
|
+
payload, _ := json.Marshal(struct {
|
|
594
|
+
Rule string `json:"rule"`
|
|
595
|
+
Message string `json:"message"`
|
|
596
|
+
Pos int `json:"pos"`
|
|
597
|
+
End int `json:"end"`
|
|
598
|
+
Title string `json:"title"`
|
|
599
|
+
Edits []TextEdit `json:"edits"`
|
|
600
|
+
}{
|
|
601
|
+
Rule: finding.Rule,
|
|
602
|
+
Message: finding.Message,
|
|
603
|
+
Pos: finding.Pos,
|
|
604
|
+
End: finding.End,
|
|
605
|
+
Title: suggestion.Title,
|
|
606
|
+
Edits: suggestion.Edits,
|
|
607
|
+
})
|
|
608
|
+
return fmt.Sprintf("%x", sha256.Sum256(payload))
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
func lspWorkspaceEditForSuggestion(opts *lspCommandOptions) (*lspWorkspaceEdit, int) {
|
|
370
612
|
target, err := filePathFromURI(opts.uri)
|
|
371
613
|
if err != nil {
|
|
372
614
|
fmt.Fprintln(os.Stderr, err)
|
|
373
615
|
return nil, 2
|
|
374
616
|
}
|
|
375
|
-
|
|
376
|
-
|
|
617
|
+
physicalCwd := realProjectPath(opts.cwd)
|
|
618
|
+
physicalTarget := realProjectPath(target)
|
|
619
|
+
if _, ok := projectRelativePath(physicalCwd, physicalTarget); !ok {
|
|
620
|
+
fmt.Fprintf(os.Stderr, "@ttsc/lint: LSP command target %s is outside cwd %s\n", physicalTarget, physicalCwd)
|
|
621
|
+
return nil, 2
|
|
622
|
+
}
|
|
623
|
+
if lspProjectTargetHasSegment(opts, "node_modules") {
|
|
624
|
+
return nil, 0
|
|
625
|
+
}
|
|
626
|
+
selection, err := suggestionSelectionArgument(opts.argumentsJSON)
|
|
627
|
+
if err != nil {
|
|
628
|
+
fmt.Fprintln(os.Stderr, err)
|
|
377
629
|
return nil, 2
|
|
378
630
|
}
|
|
379
|
-
|
|
631
|
+
current, err := os.ReadFile(physicalTarget)
|
|
632
|
+
if err != nil || lspSourceHash(string(current)) != selection.SourceHash {
|
|
380
633
|
return nil, 0
|
|
381
634
|
}
|
|
635
|
+
findings, _, closeProgram, code := lspFindings(opts, false)
|
|
636
|
+
if closeProgram != nil {
|
|
637
|
+
defer closeProgram()
|
|
638
|
+
}
|
|
639
|
+
if code != 0 {
|
|
640
|
+
return nil, code
|
|
641
|
+
}
|
|
642
|
+
findings = filterFindingsForPath(findings, physicalTarget)
|
|
643
|
+
for _, finding := range findings {
|
|
644
|
+
if finding == nil || finding.Rule != selection.Rule ||
|
|
645
|
+
finding.Message != selection.Message ||
|
|
646
|
+
finding.Pos != selection.Pos || finding.End != selection.End ||
|
|
647
|
+
selection.SuggestionIndex >= len(finding.Suggestions) {
|
|
648
|
+
continue
|
|
649
|
+
}
|
|
650
|
+
suggestion := finding.Suggestions[selection.SuggestionIndex]
|
|
651
|
+
if suggestion.Title != selection.Title || finding.File == nil ||
|
|
652
|
+
lspSourceHash(finding.File.Text()) != selection.SourceHash ||
|
|
653
|
+
lspSuggestionFingerprint(finding, suggestion) != selection.Fingerprint {
|
|
654
|
+
continue
|
|
655
|
+
}
|
|
656
|
+
source := finding.File.Text()
|
|
657
|
+
selected := selectTextEdits(len(source), suggestion.Edits)
|
|
658
|
+
if len(selected) == 0 || len(selected) != len(suggestion.Edits) {
|
|
659
|
+
return nil, 0
|
|
660
|
+
}
|
|
661
|
+
edits := make([]lspTextEdit, 0, len(selected))
|
|
662
|
+
for _, edit := range selected {
|
|
663
|
+
edits = append(edits, lspTextEdit{
|
|
664
|
+
Range: lspRange{
|
|
665
|
+
Start: byteOffsetToLSPPosition(source, edit.Pos),
|
|
666
|
+
End: byteOffsetToLSPPosition(source, edit.End),
|
|
667
|
+
},
|
|
668
|
+
NewText: edit.Text,
|
|
669
|
+
})
|
|
670
|
+
}
|
|
671
|
+
current, err := os.ReadFile(physicalTarget)
|
|
672
|
+
if err != nil || lspSourceHash(string(current)) != selection.SourceHash {
|
|
673
|
+
return nil, 0
|
|
674
|
+
}
|
|
675
|
+
return &lspWorkspaceEdit{Changes: map[string][]lspTextEdit{opts.uri: edits}}, 0
|
|
676
|
+
}
|
|
677
|
+
return nil, 0
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
func suggestionSelectionArgument(raw string) (lspSuggestionSelection, error) {
|
|
681
|
+
var args []json.RawMessage
|
|
682
|
+
if strings.TrimSpace(raw) == "" || json.Unmarshal([]byte(raw), &args) != nil || len(args) < 2 {
|
|
683
|
+
return lspSuggestionSelection{}, errors.New("@ttsc/lint lsp-execute-command: missing suggestion selection argument")
|
|
684
|
+
}
|
|
685
|
+
var selection lspSuggestionSelection
|
|
686
|
+
if err := json.Unmarshal(args[1], &selection); err != nil {
|
|
687
|
+
return lspSuggestionSelection{}, fmt.Errorf("@ttsc/lint lsp-execute-command: invalid suggestion selection: %w", err)
|
|
688
|
+
}
|
|
689
|
+
if selection.Rule == "" || selection.Message == "" || selection.Title == "" ||
|
|
690
|
+
len(selection.SourceHash) != sha256.Size*2 ||
|
|
691
|
+
len(selection.Fingerprint) != sha256.Size*2 ||
|
|
692
|
+
selection.Pos < 0 || selection.End < selection.Pos || selection.SuggestionIndex < 0 {
|
|
693
|
+
return lspSuggestionSelection{}, errors.New("@ttsc/lint lsp-execute-command: invalid suggestion selection")
|
|
694
|
+
}
|
|
695
|
+
return selection, nil
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
func lspWorkspaceEditForCommand(opts *lspCommandOptions) (*lspWorkspaceEdit, int) {
|
|
699
|
+
target, err := filePathFromURI(opts.uri)
|
|
700
|
+
if err != nil {
|
|
701
|
+
fmt.Fprintln(os.Stderr, err)
|
|
702
|
+
return nil, 2
|
|
703
|
+
}
|
|
704
|
+
if lspProjectTargetHasSegment(opts, "node_modules") {
|
|
705
|
+
return nil, 0
|
|
706
|
+
}
|
|
707
|
+
// Containment is the only physical guard: resolve both sides so a symlinked
|
|
708
|
+
// or short-name spelling of either cannot smuggle the target outside cwd.
|
|
709
|
+
// `target` itself stays in its LOGICAL (URI) spelling for everything that
|
|
710
|
+
// indexes into the temp workspace below — the temp tree materializes a
|
|
711
|
+
// project-internal symlink/junction under its logical name, so resolving the
|
|
712
|
+
// target to its physical destination here would address a path the temp tree
|
|
713
|
+
// never created (samchon/ttsc#614).
|
|
714
|
+
physicalCwd := realProjectPath(opts.cwd)
|
|
715
|
+
physicalTarget := realProjectPath(target)
|
|
716
|
+
if _, ok := projectRelativePath(physicalCwd, physicalTarget); !ok {
|
|
717
|
+
fmt.Fprintf(os.Stderr, "@ttsc/lint: LSP command target %s is outside cwd %s\n", physicalTarget, physicalCwd)
|
|
718
|
+
return nil, 2
|
|
719
|
+
}
|
|
382
720
|
original, err := os.ReadFile(target)
|
|
383
721
|
if err != nil {
|
|
384
722
|
fmt.Fprintf(os.Stderr, "@ttsc/lint: read %s: %v\n", target, err)
|
|
385
723
|
return nil, 2
|
|
386
724
|
}
|
|
725
|
+
return lspWorkspaceEditForSeededCommand(opts, target, string(original), nil)
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// lspWorkspaceEditForSeededCommand runs a command in the ordinary temporary
|
|
729
|
+
// project while optionally replacing the target copy with editor-owned text.
|
|
730
|
+
// original is always the document against which the returned full-document
|
|
731
|
+
// edit is measured. A non-nil sourceOverlay is written only inside the
|
|
732
|
+
// temporary workspace, so type-aware contributor rules receive a real Program
|
|
733
|
+
// and Checker for the dirty document without using or mutating its disk twin as
|
|
734
|
+
// command input. Dirty buffers with syntax errors fail closed before any fix is
|
|
735
|
+
// applied.
|
|
736
|
+
func lspWorkspaceEditForSeededCommand(
|
|
737
|
+
opts *lspCommandOptions,
|
|
738
|
+
target string,
|
|
739
|
+
original string,
|
|
740
|
+
sourceOverlay *string,
|
|
741
|
+
) (*lspWorkspaceEdit, int) {
|
|
742
|
+
// Pass the LOGICAL cwd/target so the temp workspace is indexed by the
|
|
743
|
+
// project's logical layout (a project-internal symlink or junction keeps its
|
|
744
|
+
// logical name). Physical resolution stays inside prepareLSPCommandWorkspace
|
|
745
|
+
// for the copy source and inside tempPathFor's boundary-alias fallback.
|
|
387
746
|
tempRoot, tempTarget, tempTsconfig, cleanup, err := prepareLSPCommandWorkspace(opts.cwd, opts.tsconfig, target)
|
|
388
747
|
if err != nil {
|
|
389
748
|
fmt.Fprintln(os.Stderr, err)
|
|
@@ -391,16 +750,34 @@ func lspWorkspaceEditForCommand(opts *lspCommandOptions) (*lspWorkspaceEdit, int
|
|
|
391
750
|
}
|
|
392
751
|
defer cleanup()
|
|
393
752
|
|
|
753
|
+
if sourceOverlay != nil {
|
|
754
|
+
if err := os.MkdirAll(filepath.Dir(tempTarget), 0o755); err != nil {
|
|
755
|
+
fmt.Fprintf(os.Stderr, "@ttsc/lint: create dirty-buffer directory: %v\n", err)
|
|
756
|
+
return nil, 2
|
|
757
|
+
}
|
|
758
|
+
if err := os.WriteFile(tempTarget, []byte(*sourceOverlay), 0o600); err != nil {
|
|
759
|
+
fmt.Fprintf(os.Stderr, "@ttsc/lint: seed dirty buffer %s: %v\n", target, err)
|
|
760
|
+
return nil, 2
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
394
764
|
pluginsJSON := remapLSPPluginsJSONForTempWorkspace(opts.pluginsJSON, opts.cwd, tempRoot)
|
|
395
|
-
rules, err :=
|
|
765
|
+
rules, err := loadLSPCommandRules(
|
|
766
|
+
pluginsJSON,
|
|
767
|
+
tempRoot,
|
|
768
|
+
tempTsconfig,
|
|
769
|
+
target,
|
|
770
|
+
opts.command == commandFormatDocument,
|
|
771
|
+
)
|
|
396
772
|
if err != nil {
|
|
397
773
|
fmt.Fprintln(os.Stderr, err)
|
|
398
774
|
return nil, 2
|
|
399
775
|
}
|
|
400
|
-
if opts.command == commandFormatDocument {
|
|
401
|
-
rules = formatCommandResolver{inner: rules}
|
|
402
|
-
}
|
|
403
776
|
engine := NewEngineWithResolver(rules)
|
|
777
|
+
if err := engine.ConfigError(); err != nil {
|
|
778
|
+
fmt.Fprintln(os.Stderr, err)
|
|
779
|
+
return nil, 2
|
|
780
|
+
}
|
|
404
781
|
needsRuleChecker := engine.NeedsTypeChecker()
|
|
405
782
|
maxPasses := maxFixPasses
|
|
406
783
|
if opts.command == commandFormatDocument {
|
|
@@ -411,6 +788,7 @@ func lspWorkspaceEditForCommand(opts *lspCommandOptions) (*lspWorkspaceEdit, int
|
|
|
411
788
|
prog, parseDiags, err := loadProgram(tempRoot, tempTsconfig, loadProgramOptions{
|
|
412
789
|
forceNoEmit: true,
|
|
413
790
|
needsRuleChecker: needsRuleChecker,
|
|
791
|
+
projectIdentity: opts.projectIdentity,
|
|
414
792
|
})
|
|
415
793
|
if err != nil {
|
|
416
794
|
fmt.Fprintf(os.Stderr, "@ttsc/lint: %v\n", err)
|
|
@@ -420,7 +798,27 @@ func lspWorkspaceEditForCommand(opts *lspCommandOptions) (*lspWorkspaceEdit, int
|
|
|
420
798
|
prog.close()
|
|
421
799
|
return nil, 0
|
|
422
800
|
}
|
|
423
|
-
|
|
801
|
+
// The command target must resolve to a source file the temp program loaded.
|
|
802
|
+
// A nil here means the temp workspace did not materialize the target under
|
|
803
|
+
// the spelling the tsconfig references — the signature of the symlink/
|
|
804
|
+
// junction copy regression. Fail loud (exit 2) instead of returning a silent
|
|
805
|
+
// nil edit the editor cannot distinguish from an already-clean document.
|
|
806
|
+
targetFile := prog.findSourceFile(tempTarget)
|
|
807
|
+
if targetFile == nil {
|
|
808
|
+
prog.close()
|
|
809
|
+
fmt.Fprintf(os.Stderr,
|
|
810
|
+
"@ttsc/lint: LSP %s: target source file %s is not in the program\n",
|
|
811
|
+
opts.command, tempTarget)
|
|
812
|
+
return nil, 2
|
|
813
|
+
}
|
|
814
|
+
if sourceOverlay != nil &&
|
|
815
|
+
len(prog.tsProgram.GetSyntacticDiagnostics(context.Background(), targetFile)) > 0 {
|
|
816
|
+
// A dirty overlay buffer with syntax errors is a benign no-op: don't fight
|
|
817
|
+
// the editor's own diagnostics on the dirty document with a partial fix.
|
|
818
|
+
prog.close()
|
|
819
|
+
return nil, 0
|
|
820
|
+
}
|
|
821
|
+
findings := filterFindingsForPath(prog.runLintCycle(engine), tempTarget)
|
|
424
822
|
prog.close()
|
|
425
823
|
if opts.command == commandFormatDocument {
|
|
426
824
|
findings = filterFormatFindings(findings)
|
|
@@ -448,18 +846,18 @@ func lspWorkspaceEditForCommand(opts *lspCommandOptions) (*lspWorkspaceEdit, int
|
|
|
448
846
|
fmt.Fprintf(os.Stderr, "@ttsc/lint: read cascaded %s: %v\n", tempTarget, err)
|
|
449
847
|
return nil, 2
|
|
450
848
|
}
|
|
451
|
-
return workspaceEditForFullDocument(opts.uri,
|
|
849
|
+
return workspaceEditForFullDocument(opts.uri, original, string(next)), 0
|
|
452
850
|
}
|
|
453
851
|
|
|
454
852
|
// lspFormatBuffer formats an in-memory document buffer using only the
|
|
455
|
-
// format-class rules
|
|
456
|
-
//
|
|
853
|
+
// format-class rules. It is the path behind --content-stdin for
|
|
854
|
+
// ttsc.format.document.
|
|
457
855
|
//
|
|
458
|
-
//
|
|
459
|
-
//
|
|
460
|
-
//
|
|
461
|
-
//
|
|
462
|
-
//
|
|
856
|
+
// Built-in format rules are AST+source only and use the lightweight single-file
|
|
857
|
+
// parser below. Contributor format rules conservatively require a checker, so
|
|
858
|
+
// they use a temporary project seeded with `content`. Both branches derive
|
|
859
|
+
// findings and edits from the supplied buffer; neither uses stale disk content
|
|
860
|
+
// as the document or mutates the original project.
|
|
463
861
|
//
|
|
464
862
|
// Returns the same WorkspaceEdit shape as the disk path, or (nil, 0) on a
|
|
465
863
|
// no-op (no fixable findings, or text unchanged after convergence).
|
|
@@ -471,32 +869,47 @@ func lspFormatBuffer(content string, opts *lspCommandOptions) (*lspWorkspaceEdit
|
|
|
471
869
|
}
|
|
472
870
|
// Guard rails mirror lspWorkspaceEditForCommand: skip targets outside the
|
|
473
871
|
// project root or inside node_modules.
|
|
474
|
-
|
|
475
|
-
|
|
872
|
+
physicalCwd := realProjectPath(opts.cwd)
|
|
873
|
+
physicalTarget := realProjectPath(target)
|
|
874
|
+
if _, ok := projectRelativePath(physicalCwd, physicalTarget); !ok {
|
|
875
|
+
fmt.Fprintf(os.Stderr, "@ttsc/lint: LSP command target %s is outside cwd %s\n", physicalTarget, physicalCwd)
|
|
476
876
|
return nil, 2
|
|
477
877
|
}
|
|
478
|
-
if
|
|
878
|
+
if lspProjectTargetHasSegment(opts, "node_modules") {
|
|
479
879
|
return nil, 0
|
|
480
880
|
}
|
|
481
881
|
|
|
482
|
-
|
|
882
|
+
resolver, err := loadLSPCommandRules(
|
|
883
|
+
opts.pluginsJSON,
|
|
884
|
+
opts.cwd,
|
|
885
|
+
opts.tsconfig,
|
|
886
|
+
target,
|
|
887
|
+
true,
|
|
888
|
+
)
|
|
483
889
|
if err != nil {
|
|
484
890
|
fmt.Fprintln(os.Stderr, err)
|
|
485
891
|
return nil, 2
|
|
486
892
|
}
|
|
487
|
-
|
|
488
|
-
if err != nil {
|
|
893
|
+
engine := NewEngineWithResolver(resolver)
|
|
894
|
+
if err := engine.ConfigError(); err != nil {
|
|
489
895
|
fmt.Fprintln(os.Stderr, err)
|
|
490
896
|
return nil, 2
|
|
491
897
|
}
|
|
492
|
-
engine := NewEngineWithResolver(resolver)
|
|
493
898
|
if engine.NeedsTypeChecker() {
|
|
494
|
-
//
|
|
495
|
-
//
|
|
496
|
-
//
|
|
497
|
-
//
|
|
498
|
-
//
|
|
499
|
-
|
|
899
|
+
// Contributor format rules conservatively require a checker. Seed the
|
|
900
|
+
// dirty text into the temporary project so the checker, findings, fix
|
|
901
|
+
// ranges, and final WorkspaceEdit all describe the same editor document.
|
|
902
|
+
// Built-in AST-only format rules keep the fast single-file path below.
|
|
903
|
+
// Pass the LOGICAL target: the seeded command indexes it into the temp
|
|
904
|
+
// workspace (which mirrors the project's logical layout), so a
|
|
905
|
+
// project-internal symlink/junction must keep its logical name here too.
|
|
906
|
+
sourceOverlay := content
|
|
907
|
+
return lspWorkspaceEditForSeededCommand(
|
|
908
|
+
opts,
|
|
909
|
+
target,
|
|
910
|
+
content,
|
|
911
|
+
&sourceOverlay,
|
|
912
|
+
)
|
|
500
913
|
}
|
|
501
914
|
scriptKind := scriptKindForPath(target)
|
|
502
915
|
|
|
@@ -546,21 +959,23 @@ func scriptKindForPath(path string) shimcore.ScriptKind {
|
|
|
546
959
|
}
|
|
547
960
|
|
|
548
961
|
// applyFindingFixesToText is the in-memory counterpart of
|
|
549
|
-
// applyFindingFixes/applyTextEditsToFile (fix.go): it
|
|
550
|
-
// finding's
|
|
551
|
-
//
|
|
552
|
-
// new string plus the number of edits applied. It never writes to
|
|
553
|
-
// never reloads a Program. Findings carry byte offsets into the same
|
|
554
|
-
// that was just parsed, so no per-file grouping is needed
|
|
962
|
+
// applyFindingFixes/applyTextEditsToFile (fix.go): it groups every fixable
|
|
963
|
+
// finding's edits, selects a non-overlapping, per-finding-atomic set with the
|
|
964
|
+
// same selectTextEditGroups logic, applies them right-to-left to `text`, and
|
|
965
|
+
// returns the new string plus the number of edits applied. It never writes to
|
|
966
|
+
// disk and never reloads a Program. Findings carry byte offsets into the same
|
|
967
|
+
// `text` that was just parsed, so no per-file grouping is needed — but each
|
|
968
|
+
// finding still forms its own atomic edit group so a partially-overlapped
|
|
969
|
+
// multi-edit fix is dropped whole rather than half-applied (samchon/ttsc#605).
|
|
555
970
|
func applyFindingFixesToText(text string, findings []*Finding) (string, int) {
|
|
556
|
-
|
|
971
|
+
groups := make([][]TextEdit, 0, len(findings))
|
|
557
972
|
for _, finding := range findings {
|
|
558
973
|
if finding == nil || len(finding.Fix) == 0 {
|
|
559
974
|
continue
|
|
560
975
|
}
|
|
561
|
-
|
|
976
|
+
groups = append(groups, finding.Fix)
|
|
562
977
|
}
|
|
563
|
-
selected :=
|
|
978
|
+
selected := selectTextEditGroups(len(text), groups)
|
|
564
979
|
if len(selected) == 0 {
|
|
565
980
|
return text, 0
|
|
566
981
|
}
|
|
@@ -598,11 +1013,15 @@ func prepareLSPCommandWorkspace(cwd string, tsconfig string, target string) (str
|
|
|
598
1013
|
cleanup()
|
|
599
1014
|
return "", "", "", nil, fmt.Errorf("@ttsc/lint: LSP command target %s is outside cwd %s", target, cwd)
|
|
600
1015
|
}
|
|
601
|
-
|
|
1016
|
+
// Copy from the resolved (physical) cwd so a symlinked or short-name project
|
|
1017
|
+
// root still descends into the real tree; the copy reproduces the project's
|
|
1018
|
+
// LOGICAL layout, which tempPathFor above indexes into by the logical cwd.
|
|
1019
|
+
physicalCwd := realProjectPath(cwd)
|
|
1020
|
+
if err := copyLSPCommandWorkspace(physicalCwd, tempRoot); err != nil {
|
|
602
1021
|
cleanup()
|
|
603
1022
|
return "", "", "", nil, err
|
|
604
1023
|
}
|
|
605
|
-
if err := linkNearestNodeModules(tempRoot,
|
|
1024
|
+
if err := linkNearestNodeModules(tempRoot, physicalCwd); err != nil {
|
|
606
1025
|
cleanup()
|
|
607
1026
|
return "", "", "", nil, err
|
|
608
1027
|
}
|
|
@@ -633,7 +1052,11 @@ func copyLSPCommandWorkspace(src string, dst string) error {
|
|
|
633
1052
|
if entry.IsDir() {
|
|
634
1053
|
return filepath.SkipDir
|
|
635
1054
|
}
|
|
636
|
-
|
|
1055
|
+
// A skipped directory that is a reparse point (a symlinked or junctioned
|
|
1056
|
+
// node_modules/.git) is dropped without materializing its contents. A
|
|
1057
|
+
// junction reports ModeSymlink on current toolchains and ModeIrregular on
|
|
1058
|
+
// older ones, so check both bits.
|
|
1059
|
+
if entry.Type()&(fs.ModeSymlink|fs.ModeIrregular) != 0 {
|
|
637
1060
|
return nil
|
|
638
1061
|
}
|
|
639
1062
|
}
|
|
@@ -652,28 +1075,34 @@ func copyLSPCommandWorkspaceEntry(src string, dst string, seenDirs map[string]st
|
|
|
652
1075
|
if err != nil {
|
|
653
1076
|
return err
|
|
654
1077
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
1078
|
+
// A directory that is itself a reparse point — a symlink OR an NTFS
|
|
1079
|
+
// junction — is not descended into by filepath.WalkDir, so its contents
|
|
1080
|
+
// must be materialized here under the LOGICAL name. Go reports a junction as
|
|
1081
|
+
// ModeSymlink on current toolchains but as ModeIrregular on older ones;
|
|
1082
|
+
// treat either bit as a link so the copy is correct regardless of version.
|
|
1083
|
+
// A plain directory (neither bit) is created empty and left for WalkDir to
|
|
1084
|
+
// descend (samchon/ttsc#614).
|
|
1085
|
+
isLink := linkInfo.Mode()&(os.ModeSymlink|os.ModeIrregular) != 0
|
|
1086
|
+
if isLink {
|
|
1087
|
+
// resolveDirLink chases the symlink/junction via os.Readlink, which
|
|
1088
|
+
// resolves junctions that neither ModeSymlink nor EvalSymlinks expose on
|
|
1089
|
+
// older toolchains. The real path keys the cycle guard, scoped to the
|
|
1090
|
+
// active recursion branch (defer delete) so sibling aliases pointing at
|
|
1091
|
+
// one real directory (e.g. `src-a -> real-src` and `src-b -> real-src` in
|
|
1092
|
+
// different tsconfig entries) each get materialized; the test
|
|
1093
|
+
// `TestLSPExecuteCommandMaterializesDuplicateSymlinkedDirectories` pins
|
|
1094
|
+
// this contract.
|
|
1095
|
+
realDir := resolveDirLink(src)
|
|
1096
|
+
if _, ok := seenDirs[realDir]; ok {
|
|
1097
|
+
return nil
|
|
671
1098
|
}
|
|
1099
|
+
seenDirs[realDir] = struct{}{}
|
|
1100
|
+
defer delete(seenDirs, realDir)
|
|
672
1101
|
}
|
|
673
1102
|
if err := os.MkdirAll(dst, mode.Perm()); err != nil {
|
|
674
1103
|
return err
|
|
675
1104
|
}
|
|
676
|
-
if !
|
|
1105
|
+
if !isLink {
|
|
677
1106
|
return nil
|
|
678
1107
|
}
|
|
679
1108
|
entries, err := os.ReadDir(src)
|
|
@@ -741,7 +1170,11 @@ func lspProjectTargetHasSegment(opts *lspCommandOptions, segment string) bool {
|
|
|
741
1170
|
if err != nil {
|
|
742
1171
|
return false
|
|
743
1172
|
}
|
|
744
|
-
|
|
1173
|
+
if opts.projectIdentity.LogicalProjectRoot != "" &&
|
|
1174
|
+
projectPathHasSegment(opts.projectIdentity.LogicalProjectRoot, target, segment) {
|
|
1175
|
+
return true
|
|
1176
|
+
}
|
|
1177
|
+
return projectPathHasSegment(realProjectPath(opts.cwd), realProjectPath(target), segment)
|
|
745
1178
|
}
|
|
746
1179
|
|
|
747
1180
|
func lspProjectTargetOutsideCwd(opts *lspCommandOptions) bool {
|
|
@@ -752,7 +1185,7 @@ func lspProjectTargetOutsideCwd(opts *lspCommandOptions) bool {
|
|
|
752
1185
|
if err != nil {
|
|
753
1186
|
return false
|
|
754
1187
|
}
|
|
755
|
-
_, ok := projectRelativePath(opts.cwd, target)
|
|
1188
|
+
_, ok := projectRelativePath(realProjectPath(opts.cwd), realProjectPath(target))
|
|
756
1189
|
return !ok
|
|
757
1190
|
}
|
|
758
1191
|
|
|
@@ -791,9 +1224,23 @@ func remapLSPPluginsJSONForTempWorkspace(raw string, cwd string, tempRoot string
|
|
|
791
1224
|
return string(next)
|
|
792
1225
|
}
|
|
793
1226
|
|
|
1227
|
+
// tempPathFor maps a project `file` to its location inside the temp workspace,
|
|
1228
|
+
// which mirrors the project's LOGICAL layout. It therefore prefers the logical
|
|
1229
|
+
// relative path: a project-internal symlink or junction (src -> real-src),
|
|
1230
|
+
// materialized by copyLSPCommandWorkspace under its logical name, must be
|
|
1231
|
+
// addressed the same way here — resolving `file` physically would point at a
|
|
1232
|
+
// spelling (real-src/main.ts) the temp tree never created (samchon/ttsc#614).
|
|
1233
|
+
// Only when cwd and file don't share a logical prefix — an incidental boundary
|
|
1234
|
+
// alias such as a Windows 8.3 short cwd or macOS /tmp -> /private/tmp, where the
|
|
1235
|
+
// two spellings can independently pick either alias — does it fall back to
|
|
1236
|
+
// realProjectPath to reconcile them. Containment guards resolve physically and
|
|
1237
|
+
// live at the call sites, not here.
|
|
794
1238
|
func tempPathFor(cwd string, tempRoot string, file string) (string, bool) {
|
|
795
|
-
rel,
|
|
796
|
-
|
|
1239
|
+
if rel, ok := projectRelativePath(cwd, file); ok {
|
|
1240
|
+
return filepath.Join(tempRoot, rel), true
|
|
1241
|
+
}
|
|
1242
|
+
rel, ok := projectRelativePath(realProjectPath(cwd), realProjectPath(file))
|
|
1243
|
+
if !ok {
|
|
797
1244
|
return "", false
|
|
798
1245
|
}
|
|
799
1246
|
return filepath.Join(tempRoot, rel), true
|