@ttsc/lint 0.19.2 → 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 +1 -1
- package/lib/index.js +26 -4
- package/lib/index.js.map +1 -1
- package/lib/structures/format/ITtscLintFormat.d.ts +14 -10
- package/lib/structures/rules/ITtscLintRegexpRules.d.ts +24 -9
- package/lib/structures/rules/ITtscLintSecurityRules.d.ts +18 -0
- package/lib/structures/rules/ITtscLintSolidRules.d.ts +22 -2
- package/lib/structures/rules/ITtscLintStorybookRules.d.ts +21 -0
- package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.d.ts +10 -1
- package/lib/structures/rules/ITtscLintTypeScriptRules.d.ts +13 -2
- package/linthost/compile.go +9 -4
- package/linthost/config.go +18 -14
- package/linthost/contrib_adapter.go +57 -0
- package/linthost/dispatch.go +5 -1
- package/linthost/display_width.go +179 -41
- package/linthost/engine.go +120 -4
- package/linthost/flags_gen.go +4 -4
- package/linthost/hints.go +207 -0
- package/linthost/host.go +68 -0
- package/linthost/lsp.go +148 -29
- package/linthost/print_dispatch.go +24 -0
- package/linthost/print_nodes_array.go +88 -15
- package/linthost/print_nodes_call.go +38 -31
- package/linthost/print_nodes_control_flow.go +319 -0
- package/linthost/print_nodes_function.go +140 -13
- package/linthost/print_nodes_list.go +22 -10
- package/linthost/project_engine.go +18 -1
- package/linthost/project_rules.go +47 -0
- package/linthost/rule_docs.go +114 -0
- package/linthost/rules_boundaries.go +80 -2
- package/linthost/rules_format_bracket_spacing.go +18 -6
- package/linthost/rules_format_clause_join.go +11 -8
- package/linthost/rules_format_indent.go +155 -4
- package/linthost/rules_format_print_width.go +12 -35
- package/linthost/rules_format_quote_props.go +88 -27
- package/linthost/rules_format_statement_split.go +81 -0
- package/linthost/rules_format_trailing_comma.go +62 -94
- package/linthost/rules_gap.go +26 -17
- package/linthost/rules_jsdoc.go +54 -0
- package/linthost/rules_logic.go +30 -15
- package/linthost/rules_no_redeclare.go +12 -3
- package/linthost/rules_promise.go +37 -15
- package/linthost/rules_regexp.go +443 -49
- package/linthost/rules_security.go +243 -4
- package/linthost/rules_solid.go +430 -10
- package/linthost/rules_storybook.go +255 -10
- package/linthost/rules_suggestions.go +60 -39
- package/linthost/rules_ts.go +7 -0
- package/linthost/rules_ts_async.go +80 -2
- package/linthost/rules_ts_extra.go +21 -7
- package/linthost/serve.go +318 -0
- package/linthost/width_tables_gen.go +219 -0
- package/package.json +2 -2
- package/rule/hint.go +142 -0
- package/rule/rule.go +197 -1
- package/src/index.ts +35 -4
- package/src/structures/format/ITtscLintFormat.ts +14 -10
- package/src/structures/rules/ITtscLintRegexpRules.ts +24 -9
- package/src/structures/rules/ITtscLintSecurityRules.ts +18 -0
- package/src/structures/rules/ITtscLintSolidRules.ts +22 -2
- package/src/structures/rules/ITtscLintStorybookRules.ts +21 -0
- package/src/structures/rules/ITtscLintTypeScriptRuleOptions.ts +10 -1
- package/src/structures/rules/ITtscLintTypeScriptRules.ts +13 -2
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
package linthost
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"os"
|
|
6
|
+
|
|
7
|
+
publicrule "github.com/samchon/ttsc/packages/lint/rule"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// RunLSPHints prints the editor-completion corpus every project rule published
|
|
11
|
+
// for this project, as JSON.
|
|
12
|
+
//
|
|
13
|
+
// Unlike the other LSP verbs this takes no `--uri`: a corpus describes the
|
|
14
|
+
// Program, not a document. It loads a Program only when the resolved config
|
|
15
|
+
// declares a rule that can publish one, because a corpus is a projection of what
|
|
16
|
+
// a project rule's Check found — which is why the caller is expected to cache
|
|
17
|
+
// the answer and ask again only when the Program's inputs changed, never per
|
|
18
|
+
// editor request.
|
|
19
|
+
//
|
|
20
|
+
// An empty corpus is a successful answer. A project with no hint-publishing rule
|
|
21
|
+
// is the common case, and a caller must be able to tell it apart from a failure;
|
|
22
|
+
// a nonzero exit here would read as "the project is broken".
|
|
23
|
+
func RunLSPHints(args []string) int {
|
|
24
|
+
opts, ok := parseLSPCommandOptions("lsp-hints", args)
|
|
25
|
+
if !ok {
|
|
26
|
+
return 2
|
|
27
|
+
}
|
|
28
|
+
hints, code := computeLSPHints(opts)
|
|
29
|
+
if code != 0 {
|
|
30
|
+
return code
|
|
31
|
+
}
|
|
32
|
+
return writeJSON(hints)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// computeLSPHints builds the corpus for one project. Split from RunLSPHints so
|
|
36
|
+
// the resident lsp-serve loop reuses it over a warm Program, the same split
|
|
37
|
+
// computeLSPDiagnostics and computeLSPCodeActions already take.
|
|
38
|
+
func computeLSPHints(opts *lspCommandOptions) ([]publicrule.Hint, int) {
|
|
39
|
+
rules, err := loadRules(opts.pluginsJSON, opts.cwd, opts.tsconfig)
|
|
40
|
+
if err != nil {
|
|
41
|
+
fmt.Fprintln(os.Stderr, err)
|
|
42
|
+
return nil, 2
|
|
43
|
+
}
|
|
44
|
+
engine := NewEngineWithResolver(rules)
|
|
45
|
+
if err := engine.ConfigError(); err != nil {
|
|
46
|
+
fmt.Fprintln(os.Stderr, err)
|
|
47
|
+
return nil, 2
|
|
48
|
+
}
|
|
49
|
+
publishes, needsChecker := engine.hasHintPublisher()
|
|
50
|
+
if !publishes {
|
|
51
|
+
// Nothing the config declared can publish a corpus, so there is no
|
|
52
|
+
// projection to take and the Program is never built. A project with no
|
|
53
|
+
// hint-publishing rule is the common case, and it used to pay a full parse
|
|
54
|
+
// and bind — and a checker build, when any unrelated file rule was
|
|
55
|
+
// type-aware — to arrive at this same empty answer.
|
|
56
|
+
return []publicrule.Hint{}, 0
|
|
57
|
+
}
|
|
58
|
+
prog, parseDiags, closeProgram, err := acquireProgram(opts, needsChecker)
|
|
59
|
+
if closeProgram != nil {
|
|
60
|
+
defer closeProgram()
|
|
61
|
+
}
|
|
62
|
+
if err != nil {
|
|
63
|
+
fmt.Fprintf(os.Stderr, "@ttsc/lint: %v\n", err)
|
|
64
|
+
return nil, 2
|
|
65
|
+
}
|
|
66
|
+
if prog == nil || len(parseDiags) > 0 {
|
|
67
|
+
// The project does not parse right now. Rules never ran, so there is no
|
|
68
|
+
// corpus — but these are tsgo's diagnostics to own, and failing here would
|
|
69
|
+
// make an editor treat a syntax error mid-typing as a broken plugin. A nil
|
|
70
|
+
// Program without diagnostics reaches the same answer rather than a JSON
|
|
71
|
+
// null, which a caller decoding an array would read as a broken plugin too.
|
|
72
|
+
return []publicrule.Hint{}, 0
|
|
73
|
+
}
|
|
74
|
+
return collectProjectHints(prog.runProjectCycle(engine)), 0
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// hasHintPublisher reports whether any declared project rule can publish a
|
|
78
|
+
// corpus, and whether serving those rules needs a type checker.
|
|
79
|
+
//
|
|
80
|
+
// Both answers come from the registration table and the resolved config, so
|
|
81
|
+
// they are available before a Program exists — which is the point. A rule that
|
|
82
|
+
// the config never declared, or turned off, cannot contribute to the corpus,
|
|
83
|
+
// and collectProjectHints would skip it after the Program was built anyway.
|
|
84
|
+
//
|
|
85
|
+
// The checker answer is deliberately narrower than Engine.NeedsTypeChecker.
|
|
86
|
+
// That flag is engine-wide, so one type-aware *file* rule would make this verb
|
|
87
|
+
// build a checker no corpus reads. Here a checker is requested only when a rule
|
|
88
|
+
// that actually publishes hints declined to say it needs none — the same
|
|
89
|
+
// conservative default projectRuleNeedsTypeChecker applies everywhere else.
|
|
90
|
+
func (e *Engine) hasHintPublisher() (publishes bool, needsChecker bool) {
|
|
91
|
+
if e == nil {
|
|
92
|
+
return false, false
|
|
93
|
+
}
|
|
94
|
+
for _, name := range allProjectRuleNames() {
|
|
95
|
+
setting := e.projectSettings[name]
|
|
96
|
+
if !setting.Declared || setting.Severity == SeverityOff {
|
|
97
|
+
continue
|
|
98
|
+
}
|
|
99
|
+
adapter, registered := registeredProjectRules[name]
|
|
100
|
+
if !registered {
|
|
101
|
+
continue
|
|
102
|
+
}
|
|
103
|
+
if _, publisher := adapter.inner.(publicrule.HintRule); !publisher {
|
|
104
|
+
continue
|
|
105
|
+
}
|
|
106
|
+
publishes = true
|
|
107
|
+
if projectRuleNeedsTypeChecker(name) {
|
|
108
|
+
// Every publisher's need is folded in rather than returning early: one
|
|
109
|
+
// rule declining a checker does not spare a sibling that reads one.
|
|
110
|
+
needsChecker = true
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return publishes, needsChecker
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// collectProjectHints gathers the editor-completion corpus every declared
|
|
117
|
+
// project rule published for this Program.
|
|
118
|
+
//
|
|
119
|
+
// It runs after evaluateProject rather than inside it, because a corpus is a
|
|
120
|
+
// projection of finished state: asking mid-check would publish whatever the rule
|
|
121
|
+
// had indexed so far. Nothing calls this during `ttsc check`, so a rule's Hints
|
|
122
|
+
// is never invoked, and its corpus never allocated, unless an editor asks.
|
|
123
|
+
//
|
|
124
|
+
// A rule is asked only when it passed and published state — the same gate a file
|
|
125
|
+
// rule writes by hand against ProjectRulePassed. A rule that failed produced a
|
|
126
|
+
// corpus describing a Program it just rejected, and offering that would complete
|
|
127
|
+
// against facts the rule itself disowns.
|
|
128
|
+
func collectProjectHints(cycle *projectCycle) []publicrule.Hint {
|
|
129
|
+
if cycle == nil || cycle.results == nil {
|
|
130
|
+
return nil
|
|
131
|
+
}
|
|
132
|
+
hints := []publicrule.Hint{}
|
|
133
|
+
for _, name := range allProjectRuleNames() {
|
|
134
|
+
result, exists := cycle.results.byName[name]
|
|
135
|
+
if !exists || result.reporter == nil {
|
|
136
|
+
continue
|
|
137
|
+
}
|
|
138
|
+
adapter, registered := registeredProjectRules[name]
|
|
139
|
+
if !registered {
|
|
140
|
+
continue
|
|
141
|
+
}
|
|
142
|
+
provider, ok := adapter.inner.(publicrule.HintRule)
|
|
143
|
+
if !ok {
|
|
144
|
+
continue
|
|
145
|
+
}
|
|
146
|
+
snapshot := result.reporter.snapshot()
|
|
147
|
+
if snapshot.Status != publicrule.ProjectRulePassed || snapshot.State == nil {
|
|
148
|
+
continue
|
|
149
|
+
}
|
|
150
|
+
hints = append(hints, ruleHints(name, provider, result, snapshot)...)
|
|
151
|
+
}
|
|
152
|
+
return hints
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ruleHints calls one rule's Hints behind a recover barrier and drops the hints
|
|
156
|
+
// a rule cannot have meant.
|
|
157
|
+
//
|
|
158
|
+
// The barrier matches the metadata-inspection contract: a contributor panicking
|
|
159
|
+
// while describing itself loses its contribution rather than the process. This
|
|
160
|
+
// path is worse than most to leave unguarded — it runs inside an editor session,
|
|
161
|
+
// where a crash is not a failed build but a language server that stopped
|
|
162
|
+
// answering.
|
|
163
|
+
func ruleHints(
|
|
164
|
+
name string,
|
|
165
|
+
provider publicrule.HintRule,
|
|
166
|
+
result projectCycleResult,
|
|
167
|
+
snapshot publicrule.ProjectRuleResult,
|
|
168
|
+
) (hints []publicrule.Hint) {
|
|
169
|
+
defer func() {
|
|
170
|
+
if recovered := recover(); recovered != nil {
|
|
171
|
+
fmt.Fprintf(
|
|
172
|
+
os.Stderr,
|
|
173
|
+
"@ttsc/lint: project rule %q panicked while publishing hints: %v; dropping its corpus\n",
|
|
174
|
+
name,
|
|
175
|
+
recovered,
|
|
176
|
+
)
|
|
177
|
+
hints = nil
|
|
178
|
+
}
|
|
179
|
+
}()
|
|
180
|
+
published := provider.Hints(&publicrule.HintContext{
|
|
181
|
+
Identity: result.identity,
|
|
182
|
+
State: snapshot.State,
|
|
183
|
+
Severity: publicrule.Severity(result.severity),
|
|
184
|
+
Options: result.options,
|
|
185
|
+
})
|
|
186
|
+
kept := make([]publicrule.Hint, 0, len(published))
|
|
187
|
+
for _, hint := range published {
|
|
188
|
+
if !usableHint(hint) {
|
|
189
|
+
continue
|
|
190
|
+
}
|
|
191
|
+
kept = append(kept, hint)
|
|
192
|
+
}
|
|
193
|
+
return kept
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// usableHint reports whether a hint can be offered at all.
|
|
197
|
+
//
|
|
198
|
+
// A hint with no scope would apply anywhere on any line, which is never what a
|
|
199
|
+
// rule meant — it would surface in every decorator and every string literal — so
|
|
200
|
+
// it is dropped rather than honored. A hint with no After is the same mistake
|
|
201
|
+
// spelled differently: it matches every line in scope, so the corpus fires on an
|
|
202
|
+
// empty doc comment. An empty Insert completes to nothing.
|
|
203
|
+
func usableHint(hint publicrule.Hint) bool {
|
|
204
|
+
return hint.Insert != "" &&
|
|
205
|
+
hint.Trigger.Scope != "" &&
|
|
206
|
+
hint.Trigger.After != ""
|
|
207
|
+
}
|
package/linthost/host.go
CHANGED
|
@@ -257,6 +257,23 @@ func resolveProjectPathAncestor(target string) (string, bool) {
|
|
|
257
257
|
}
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
+
// runProjectCycle evaluates the project rules alone and returns the cycle,
|
|
261
|
+
// without walking a single file.
|
|
262
|
+
//
|
|
263
|
+
// A consumer wanting only what project rules produced — a hint corpus, say —
|
|
264
|
+
// should not pay for the file walk that produces findings it will throw away.
|
|
265
|
+
// The cycle is memoized on the program exactly as runLintCycle memoizes it, so
|
|
266
|
+
// asking for hints and then linting the same program evaluates each rule once.
|
|
267
|
+
func (p *program) runProjectCycle(engine *Engine) *projectCycle {
|
|
268
|
+
if p == nil || engine == nil {
|
|
269
|
+
return nil
|
|
270
|
+
}
|
|
271
|
+
if p.projectCycle == nil {
|
|
272
|
+
p.projectCycle = engine.evaluateProject(p.identity, p.userSourceFiles(), p.checker)
|
|
273
|
+
}
|
|
274
|
+
return p.projectCycle
|
|
275
|
+
}
|
|
276
|
+
|
|
260
277
|
func (p *program) runLintCycle(engine *Engine) []*Finding {
|
|
261
278
|
if p == nil || engine == nil {
|
|
262
279
|
return nil
|
|
@@ -278,6 +295,57 @@ func (p *program) close() {
|
|
|
278
295
|
p.checker = nil
|
|
279
296
|
}
|
|
280
297
|
|
|
298
|
+
// sourceFileByPath returns the resident Program's source file whose name matches
|
|
299
|
+
// absPath (slash-normalized), or nil when the Program has no such file. Mirrors
|
|
300
|
+
// how driver.Program.SourceFile resolves a path over SourceFiles().
|
|
301
|
+
func (p *program) sourceFileByPath(absPath string) *shimast.SourceFile {
|
|
302
|
+
if p == nil || p.tsProgram == nil {
|
|
303
|
+
return nil
|
|
304
|
+
}
|
|
305
|
+
normalized := filepath.ToSlash(absPath)
|
|
306
|
+
for _, file := range p.tsProgram.SourceFiles() {
|
|
307
|
+
if file == nil {
|
|
308
|
+
continue
|
|
309
|
+
}
|
|
310
|
+
if filepath.ToSlash(file.FileName()) == normalized {
|
|
311
|
+
return file
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return nil
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// applyChange incrementally updates the resident Program for one changed file:
|
|
318
|
+
// tsgo re-parses only that file and reuses every other file's AST, and the
|
|
319
|
+
// standalone lint checker is rebuilt over the new Program. It mirrors
|
|
320
|
+
// driver.Session.Apply, adapted to lint's standalone serial checker (lint cannot
|
|
321
|
+
// borrow tsgo's pooled checker — see loadProgram). The caller has already
|
|
322
|
+
// confirmed absPath is a known source file; a config edit or a new/removed file
|
|
323
|
+
// is handled by a full reload upstream, not here. tsgo returns a rebuilt Program
|
|
324
|
+
// when the edit reshaped the import graph, and that rebuilt Program is still
|
|
325
|
+
// correct, so the reused flag is not consulted.
|
|
326
|
+
func (p *program) applyChange(absPath string) {
|
|
327
|
+
if p == nil || p.tsProgram == nil {
|
|
328
|
+
return
|
|
329
|
+
}
|
|
330
|
+
name := absPath
|
|
331
|
+
if file := p.sourceFileByPath(absPath); file != nil {
|
|
332
|
+
name = file.FileName()
|
|
333
|
+
}
|
|
334
|
+
fs := bundled.WrapFS(cachedvfs.From(osvfs.FS()))
|
|
335
|
+
host := shimcompiler.NewCompilerHost(p.cwd, fs, bundled.LibPath(), nil, nil)
|
|
336
|
+
changed := shimtspath.ToPath(name, p.cwd, fs.UseCaseSensitiveFileNames())
|
|
337
|
+
newProg, _ := p.tsProgram.UpdateProgram(changed, host, nil)
|
|
338
|
+
if newProg != nil {
|
|
339
|
+
p.tsProgram = newProg
|
|
340
|
+
if p.checker != nil {
|
|
341
|
+
p.checker, _ = shimchecker.NewChecker(newProg, nil)
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
// The prior cycle described the pre-edit Program; drop it so the next verb
|
|
345
|
+
// re-evaluates its rules over the updated ASTs.
|
|
346
|
+
p.projectCycle = nil
|
|
347
|
+
}
|
|
348
|
+
|
|
281
349
|
// userSourceFiles returns the tsconfig-selected source files the lint engine
|
|
282
350
|
// owns. The tsconfig file list is the boundary: imported libraries, generated
|
|
283
351
|
// output, and JSON modules may still appear in Program.SourceFiles(), but lint
|
package/linthost/lsp.go
CHANGED
|
@@ -50,11 +50,39 @@ type lspRangeWire struct {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
type lspDiagnostic struct {
|
|
53
|
-
Range
|
|
54
|
-
Severity
|
|
55
|
-
Code
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
Range lspRange `json:"range"`
|
|
54
|
+
Severity int `json:"severity,omitempty"`
|
|
55
|
+
Code string `json:"code,omitempty"`
|
|
56
|
+
CodeDescription *lspCodeDescription `json:"codeDescription,omitempty"`
|
|
57
|
+
Source string `json:"source,omitempty"`
|
|
58
|
+
Message string `json:"message"`
|
|
59
|
+
Tags []int `json:"tags,omitempty"`
|
|
60
|
+
RelatedInformation []lspRelatedInformation `json:"relatedInformation,omitempty"`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// lspLocation is the LSP Location: a URI plus a range inside it. The lint
|
|
64
|
+
// sidecar only ever emits locations in the file being linted, so the URI is
|
|
65
|
+
// always that file's, but the field is present because LSP requires it on every
|
|
66
|
+
// related location.
|
|
67
|
+
type lspLocation struct {
|
|
68
|
+
URI string `json:"uri"`
|
|
69
|
+
Range lspRange `json:"range"`
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// lspRelatedInformation is one entry of a diagnostic's relatedInformation: a
|
|
73
|
+
// secondary location the finding points at, with a message naming the link. The
|
|
74
|
+
// editor renders each as a clickable line under the diagnostic.
|
|
75
|
+
type lspRelatedInformation struct {
|
|
76
|
+
Location lspLocation `json:"location"`
|
|
77
|
+
Message string `json:"message"`
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// lspCodeDescription mirrors the proxy's LSPCodeDescription: a docs URL for the
|
|
81
|
+
// diagnostic's Code, which editors surface as the link on the rule id in the
|
|
82
|
+
// Problems panel. ruleDocumentationURL derives the href per rule family (see
|
|
83
|
+
// rule_docs.go); rules with no vetted page leave the whole field absent.
|
|
84
|
+
type lspCodeDescription struct {
|
|
85
|
+
Href string `json:"href"`
|
|
58
86
|
}
|
|
59
87
|
|
|
60
88
|
type lspCommand struct {
|
|
@@ -123,18 +151,29 @@ type lspCommandOptions struct {
|
|
|
123
151
|
projectIdentity publicrule.ProjectIdentity
|
|
124
152
|
}
|
|
125
153
|
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
154
|
+
// lspCommandIDs is the workspace/executeCommand ids owned by @ttsc/lint, shared
|
|
155
|
+
// by the one-shot verb and the resident lsp-serve loop.
|
|
156
|
+
func lspCommandIDs() []string {
|
|
157
|
+
return []string{
|
|
129
158
|
commandLintFixAll,
|
|
130
159
|
commandLintApplySuggestion,
|
|
131
160
|
commandFormatDocument,
|
|
132
|
-
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// lspCodeActionKinds is the CodeActionKind values @ttsc/lint may return.
|
|
165
|
+
func lspCodeActionKinds() []string {
|
|
166
|
+
return []string{"quickfix.ttsc", "source.fixAll.ttsc", "source.format"}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// RunLSPCommandIDs prints the workspace/executeCommand ids owned by @ttsc/lint.
|
|
170
|
+
func RunLSPCommandIDs([]string) int {
|
|
171
|
+
return writeJSON(lspCommandIDs())
|
|
133
172
|
}
|
|
134
173
|
|
|
135
174
|
// RunLSPCodeActionKinds prints the CodeActionKind values @ttsc/lint may return.
|
|
136
175
|
func RunLSPCodeActionKinds([]string) int {
|
|
137
|
-
return writeJSON(
|
|
176
|
+
return writeJSON(lspCodeActionKinds())
|
|
138
177
|
}
|
|
139
178
|
|
|
140
179
|
// RunLSPDiagnostics prints lint diagnostics for one file URI as LSP JSON.
|
|
@@ -143,12 +182,23 @@ func RunLSPDiagnostics(args []string) int {
|
|
|
143
182
|
if !ok {
|
|
144
183
|
return 2
|
|
145
184
|
}
|
|
185
|
+
result, code := computeLSPDiagnostics(opts)
|
|
186
|
+
if code != 0 {
|
|
187
|
+
return code
|
|
188
|
+
}
|
|
189
|
+
return writeJSON(result)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// computeLSPDiagnostics builds the diagnostics result for one file URI. Split
|
|
193
|
+
// from RunLSPDiagnostics so the resident lsp-serve loop can produce the same
|
|
194
|
+
// result against a warm Program without re-parsing per verb.
|
|
195
|
+
func computeLSPDiagnostics(opts *lspCommandOptions) (lspDiagnosticsResult, int) {
|
|
146
196
|
findings, prog, closeProgram, code := lspFindings(opts, false)
|
|
147
197
|
if closeProgram != nil {
|
|
148
198
|
defer closeProgram()
|
|
149
199
|
}
|
|
150
200
|
if code != 0 {
|
|
151
|
-
return code
|
|
201
|
+
return lspDiagnosticsResult{}, code
|
|
152
202
|
}
|
|
153
203
|
result := lspDiagnosticsResult{Document: []lspDiagnostic{}}
|
|
154
204
|
for _, finding := range filterFindingsForPath(findings, mustFilePathFromURI(opts.uri)) {
|
|
@@ -164,7 +214,7 @@ func RunLSPDiagnostics(args []string) int {
|
|
|
164
214
|
}
|
|
165
215
|
result.Project = publication
|
|
166
216
|
}
|
|
167
|
-
return
|
|
217
|
+
return result, 0
|
|
168
218
|
}
|
|
169
219
|
|
|
170
220
|
// RunLSPCodeActions prints code actions available for one file URI/range.
|
|
@@ -173,6 +223,17 @@ func RunLSPCodeActions(args []string) int {
|
|
|
173
223
|
if !ok {
|
|
174
224
|
return 2
|
|
175
225
|
}
|
|
226
|
+
actions, code := computeLSPCodeActions(opts)
|
|
227
|
+
if code != 0 {
|
|
228
|
+
return code
|
|
229
|
+
}
|
|
230
|
+
return writeJSON(actions)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// computeLSPCodeActions builds the code actions for one file URI/range. Split
|
|
234
|
+
// from RunLSPCodeActions so the resident lsp-serve loop reuses it over a warm
|
|
235
|
+
// Program.
|
|
236
|
+
func computeLSPCodeActions(opts *lspCommandOptions) ([]lspCodeAction, int) {
|
|
176
237
|
acceptsLint := acceptsActionKind(opts.contextJSON, "source.fixAll.ttsc")
|
|
177
238
|
acceptsQuickFix := acceptsActionKind(opts.contextJSON, "quickfix.ttsc")
|
|
178
239
|
acceptsFormat := acceptsActionKind(opts.contextJSON, "source.format")
|
|
@@ -181,20 +242,20 @@ func RunLSPCodeActions(args []string) int {
|
|
|
181
242
|
acceptsQuickFix = false
|
|
182
243
|
}
|
|
183
244
|
if !acceptsLint && !acceptsQuickFix && !acceptsFormat {
|
|
184
|
-
return
|
|
245
|
+
return []lspCodeAction{}, 0
|
|
185
246
|
}
|
|
186
247
|
if lspProjectTargetHasSegment(opts, "node_modules") {
|
|
187
|
-
return
|
|
248
|
+
return []lspCodeAction{}, 0
|
|
188
249
|
}
|
|
189
250
|
if lspProjectTargetOutsideCwd(opts) {
|
|
190
|
-
return
|
|
251
|
+
return []lspCodeAction{}, 0
|
|
191
252
|
}
|
|
192
253
|
findings, _, closeProgram, code := lspFindings(opts, acceptsFormat)
|
|
193
254
|
if closeProgram != nil {
|
|
194
255
|
defer closeProgram()
|
|
195
256
|
}
|
|
196
257
|
if code != 0 {
|
|
197
|
-
return code
|
|
258
|
+
return nil, code
|
|
198
259
|
}
|
|
199
260
|
findings = filterFindingsForPath(findings, mustFilePathFromURI(opts.uri))
|
|
200
261
|
lintFindings := filterLintFindings(findings)
|
|
@@ -228,7 +289,7 @@ func RunLSPCodeActions(args []string) int {
|
|
|
228
289
|
},
|
|
229
290
|
})
|
|
230
291
|
}
|
|
231
|
-
return
|
|
292
|
+
return actions, 0
|
|
232
293
|
}
|
|
233
294
|
|
|
234
295
|
// RunLSPExecuteCommand returns a WorkspaceEdit for a lint-owned command.
|
|
@@ -347,21 +408,17 @@ func lspFindings(opts *lspCommandOptions, includeFormatDefaults bool) ([]*Findin
|
|
|
347
408
|
fmt.Fprintln(os.Stderr, err)
|
|
348
409
|
return nil, nil, nil, 2
|
|
349
410
|
}
|
|
350
|
-
prog, parseDiags, err :=
|
|
351
|
-
forceNoEmit: true,
|
|
352
|
-
needsRuleChecker: engine.NeedsTypeChecker(),
|
|
353
|
-
projectIdentity: opts.projectIdentity,
|
|
354
|
-
})
|
|
411
|
+
prog, parseDiags, closeProgram, err := acquireProgram(opts, engine.NeedsTypeChecker())
|
|
355
412
|
if err != nil {
|
|
356
413
|
fmt.Fprintf(os.Stderr, "@ttsc/lint: %v\n", err)
|
|
357
414
|
return nil, nil, nil, 2
|
|
358
415
|
}
|
|
359
416
|
if len(parseDiags) > 0 {
|
|
360
417
|
// tsgo already owns parse diagnostics in the upstream LSP process.
|
|
361
|
-
return nil, prog,
|
|
418
|
+
return nil, prog, closeProgram, 0
|
|
362
419
|
}
|
|
363
420
|
findings := prog.runLintCycle(engine)
|
|
364
|
-
return findings, prog,
|
|
421
|
+
return findings, prog, closeProgram, 0
|
|
365
422
|
}
|
|
366
423
|
|
|
367
424
|
// loadLSPCommandRules constructs the resolver shared by every LSP command
|
|
@@ -415,14 +472,65 @@ func filterFindingsForPath(findings []*Finding, target string) []*Finding {
|
|
|
415
472
|
|
|
416
473
|
func findingToLSPDiagnostic(finding *Finding) lspDiagnostic {
|
|
417
474
|
return lspDiagnostic{
|
|
418
|
-
Range:
|
|
419
|
-
Severity:
|
|
420
|
-
Code:
|
|
421
|
-
|
|
422
|
-
|
|
475
|
+
Range: lspRangeForFinding(finding),
|
|
476
|
+
Severity: lspSeverity(finding.Severity),
|
|
477
|
+
Code: finding.Rule,
|
|
478
|
+
CodeDescription: lspCodeDescriptionForRule(finding.Rule),
|
|
479
|
+
Source: "@ttsc/lint",
|
|
480
|
+
Message: finding.Message,
|
|
481
|
+
Tags: lspDiagnosticTags(finding.Tags),
|
|
482
|
+
RelatedInformation: lspRelatedInformationForFinding(finding),
|
|
423
483
|
}
|
|
424
484
|
}
|
|
425
485
|
|
|
486
|
+
// lspRelatedInformationForFinding renders the finding's related locations. Each
|
|
487
|
+
// location lives in the finding's own file, so it resolves the byte ranges
|
|
488
|
+
// against that file's text and stamps the file's URI onto every entry. Returns
|
|
489
|
+
// nil for a finding with no related locations, keeping the omitempty field
|
|
490
|
+
// absent rather than an empty array.
|
|
491
|
+
func lspRelatedInformationForFinding(finding *Finding) []lspRelatedInformation {
|
|
492
|
+
if finding == nil || len(finding.RelatedInformation) == 0 || finding.File == nil {
|
|
493
|
+
return nil
|
|
494
|
+
}
|
|
495
|
+
text := finding.File.Text()
|
|
496
|
+
uri := fileURL(finding.File.FileName())
|
|
497
|
+
out := make([]lspRelatedInformation, 0, len(finding.RelatedInformation))
|
|
498
|
+
for _, item := range finding.RelatedInformation {
|
|
499
|
+
out = append(out, lspRelatedInformation{
|
|
500
|
+
Location: lspLocation{
|
|
501
|
+
URI: uri,
|
|
502
|
+
Range: lspRange{
|
|
503
|
+
Start: byteOffsetToLSPPosition(text, item.Pos),
|
|
504
|
+
End: byteOffsetToLSPPosition(text, item.End),
|
|
505
|
+
},
|
|
506
|
+
},
|
|
507
|
+
Message: item.Message,
|
|
508
|
+
})
|
|
509
|
+
}
|
|
510
|
+
return out
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// lspDiagnosticTags converts the rule-supplied tags to their integer wire
|
|
514
|
+
// values, dropping any the wire does not define so a future rule.DiagnosticTag
|
|
515
|
+
// value cannot ship an integer no editor understands. Returns nil for an empty
|
|
516
|
+
// set, keeping the omitempty field absent rather than an empty array.
|
|
517
|
+
func lspDiagnosticTags(tags []publicrule.DiagnosticTag) []int {
|
|
518
|
+
if len(tags) == 0 {
|
|
519
|
+
return nil
|
|
520
|
+
}
|
|
521
|
+
out := make([]int, 0, len(tags))
|
|
522
|
+
for _, tag := range tags {
|
|
523
|
+
switch tag {
|
|
524
|
+
case publicrule.DiagnosticTagUnnecessary, publicrule.DiagnosticTagDeprecated:
|
|
525
|
+
out = append(out, int(tag))
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
if len(out) == 0 {
|
|
529
|
+
return nil
|
|
530
|
+
}
|
|
531
|
+
return out
|
|
532
|
+
}
|
|
533
|
+
|
|
426
534
|
func lspRangeForFinding(finding *Finding) lspRange {
|
|
427
535
|
text := ""
|
|
428
536
|
if finding != nil && finding.File != nil {
|
|
@@ -1297,6 +1405,17 @@ func filePathFromURI(raw string) (string, error) {
|
|
|
1297
1405
|
return abs, nil
|
|
1298
1406
|
}
|
|
1299
1407
|
|
|
1408
|
+
// byteOffsetToLSPPosition converts a byte offset in the linted file into the LSP
|
|
1409
|
+
// Position the editor is given for it: a 0-based line and a column counted in
|
|
1410
|
+
// UTF-16 code units.
|
|
1411
|
+
//
|
|
1412
|
+
// UTF-16 is both the LSP default and the encoding the sidecar protocol fixes:
|
|
1413
|
+
// the host settles the session's PositionEncodingKind negotiation on UTF-16 so a
|
|
1414
|
+
// sidecar always emits UTF-16 columns and always receives them, which is why no
|
|
1415
|
+
// request or response on this wire carries an encoding field. The compiler's own
|
|
1416
|
+
// ranges for a line and the ranges emitted here therefore agree. See the LSP
|
|
1417
|
+
// sidecar verb section of
|
|
1418
|
+
// website/src/content/docs/development/concepts/protocol.mdx.
|
|
1300
1419
|
func byteOffsetToLSPPosition(text string, offset int) lspPosition {
|
|
1301
1420
|
if offset < 0 {
|
|
1302
1421
|
offset = 0
|
|
@@ -121,6 +121,30 @@ func dispatchNode(ctx *PrintContext, node *shimast.Node) (Doc, bool, bool) {
|
|
|
121
121
|
case shimast.KindConditionalExpression:
|
|
122
122
|
doc, covered := printConditionalExpression(ctx, node)
|
|
123
123
|
return doc, covered, true
|
|
124
|
+
case shimast.KindPropertyAssignment,
|
|
125
|
+
shimast.KindMethodDeclaration,
|
|
126
|
+
shimast.KindGetAccessor,
|
|
127
|
+
shimast.KindSetAccessor:
|
|
128
|
+
doc, covered := printObjectMember(ctx, node)
|
|
129
|
+
return doc, covered, true
|
|
130
|
+
case shimast.KindForStatement,
|
|
131
|
+
shimast.KindForOfStatement,
|
|
132
|
+
shimast.KindForInStatement,
|
|
133
|
+
shimast.KindWhileStatement,
|
|
134
|
+
shimast.KindIfStatement,
|
|
135
|
+
shimast.KindTryStatement,
|
|
136
|
+
shimast.KindSwitchStatement:
|
|
137
|
+
doc, covered := printControlFlowStatement(ctx, node)
|
|
138
|
+
return doc, covered, true
|
|
139
|
+
case shimast.KindCaseBlock:
|
|
140
|
+
doc, covered := printSwitchCaseBlock(ctx, node)
|
|
141
|
+
return doc, covered, true
|
|
142
|
+
case shimast.KindVariableStatement:
|
|
143
|
+
doc, covered := printVariableStatement(ctx, node)
|
|
144
|
+
return doc, covered, true
|
|
145
|
+
case shimast.KindThrowStatement:
|
|
146
|
+
doc, covered := printThrowStatement(ctx, node)
|
|
147
|
+
return doc, covered, true
|
|
124
148
|
}
|
|
125
149
|
return Doc{}, false, false
|
|
126
150
|
}
|