@ttsc/lint 0.19.3 → 0.20.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 +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
|
@@ -94,6 +94,7 @@ type contributorMetadata struct {
|
|
|
94
94
|
visits []shimast.Kind
|
|
95
95
|
visitsDeclarationFiles bool
|
|
96
96
|
needsTypeChecker bool
|
|
97
|
+
diagnosticTags []rule.DiagnosticTag
|
|
97
98
|
}
|
|
98
99
|
|
|
99
100
|
// inspectContributor evaluates the public rule metadata behind a recover
|
|
@@ -124,6 +125,9 @@ func inspectContributor(contributor rule.Rule) (metadata contributorMetadata, er
|
|
|
124
125
|
if typeAware, ok := contributor.(rule.TypeAwareRule); ok {
|
|
125
126
|
metadata.needsTypeChecker = typeAware.NeedsTypeChecker()
|
|
126
127
|
}
|
|
128
|
+
if tagged, ok := contributor.(rule.TaggedRule); ok {
|
|
129
|
+
metadata.diagnosticTags = tagged.DiagnosticTags()
|
|
130
|
+
}
|
|
127
131
|
if optionsRule, ok := contributor.(rule.OptionsRule); ok {
|
|
128
132
|
metadata.acceptsOptions = optionsRule.AcceptsTtscLintOptions()
|
|
129
133
|
}
|
|
@@ -142,6 +146,7 @@ func newContributorAdapter(metadata contributorMetadata) contributorAdapter {
|
|
|
142
146
|
visits: metadata.visits,
|
|
143
147
|
visitsDeclarationFiles: metadata.visitsDeclarationFiles,
|
|
144
148
|
needsTypeChecker: metadata.needsTypeChecker,
|
|
149
|
+
diagnosticTags: metadata.diagnosticTags,
|
|
145
150
|
}
|
|
146
151
|
}
|
|
147
152
|
|
|
@@ -159,6 +164,7 @@ type contributorAdapter struct {
|
|
|
159
164
|
visits []shimast.Kind
|
|
160
165
|
visitsDeclarationFiles bool
|
|
161
166
|
needsTypeChecker bool
|
|
167
|
+
diagnosticTags []rule.DiagnosticTag
|
|
162
168
|
}
|
|
163
169
|
|
|
164
170
|
// AcceptsTtscLintOptions preserves the public contributor contract:
|
|
@@ -190,6 +196,14 @@ func (a contributorAdapter) VisitsDeclarationFiles() bool {
|
|
|
190
196
|
return a.visitsDeclarationFiles
|
|
191
197
|
}
|
|
192
198
|
|
|
199
|
+
// DiagnosticTags forwards the contributor's rule.TaggedRule classification so
|
|
200
|
+
// the host reads it through the same assertion path as a built-in tagged rule.
|
|
201
|
+
// Without this the wrapping adapter would hide the marker and every
|
|
202
|
+
// contributor's tags would silently vanish.
|
|
203
|
+
func (a contributorAdapter) DiagnosticTags() []rule.DiagnosticTag {
|
|
204
|
+
return a.diagnosticTags
|
|
205
|
+
}
|
|
206
|
+
|
|
193
207
|
// formatContributorAdapter is the FormatRule-tagged variant of
|
|
194
208
|
// contributorAdapter. Wrapping the lint-only adapter (rather than
|
|
195
209
|
// duplicating its method set) keeps the marker addition trivial and
|
|
@@ -241,6 +255,49 @@ func (r contextReporter) ReportRangeFix(pos, end int, message string, edits ...r
|
|
|
241
255
|
r.ctx.ReportRangeFix(pos, end, message, toInternalTextEdits(edits)...)
|
|
242
256
|
}
|
|
243
257
|
|
|
258
|
+
// ReportSuggestion and ReportRangeSuggestion satisfy rule.SuggestionReporter,
|
|
259
|
+
// so a contributor can offer a choice of fixes — the surface built-in rules
|
|
260
|
+
// already had internally and contributors could not reach.
|
|
261
|
+
func (r contextReporter) ReportSuggestion(node *shimast.Node, message string, suggestions ...rule.Suggestion) {
|
|
262
|
+
r.ctx.ReportFixSuggestions(node, message, nil, toInternalSuggestions(suggestions)...)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
func (r contextReporter) ReportRangeSuggestion(pos, end int, message string, suggestions ...rule.Suggestion) {
|
|
266
|
+
r.ctx.ReportRangeSuggestions(pos, end, message, toInternalSuggestions(suggestions)...)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
func toInternalSuggestions(suggestions []rule.Suggestion) []Suggestion {
|
|
270
|
+
if len(suggestions) == 0 {
|
|
271
|
+
return nil
|
|
272
|
+
}
|
|
273
|
+
out := make([]Suggestion, len(suggestions))
|
|
274
|
+
for i, suggestion := range suggestions {
|
|
275
|
+
out[i] = Suggestion{
|
|
276
|
+
Title: suggestion.Title,
|
|
277
|
+
Edits: toInternalTextEdits(suggestion.Edits),
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return out
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
func (r contextReporter) ReportRelated(node *shimast.Node, message string, related ...rule.RelatedInformation) {
|
|
284
|
+
r.ctx.ReportRelated(node, message, related...)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
func (r contextReporter) ReportRangeRelated(pos, end int, message string, related ...rule.RelatedInformation) {
|
|
288
|
+
r.ctx.ReportRangeRelated(pos, end, message, related...)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// contextReporter must satisfy the optional reporter extensions whole, since the
|
|
292
|
+
// public Context type-asserts against each: a signature drift here would
|
|
293
|
+
// silently drop the rule back to the plain-diagnostic fallback instead of
|
|
294
|
+
// failing the build.
|
|
295
|
+
var (
|
|
296
|
+
_ rule.FixReporter = contextReporter{}
|
|
297
|
+
_ rule.SuggestionReporter = contextReporter{}
|
|
298
|
+
_ rule.RelatedReporter = contextReporter{}
|
|
299
|
+
)
|
|
300
|
+
|
|
244
301
|
func toInternalTextEdits(edits []rule.TextEdit) []TextEdit {
|
|
245
302
|
if len(edits) == 0 {
|
|
246
303
|
return nil
|
package/linthost/dispatch.go
CHANGED
|
@@ -76,7 +76,7 @@ func run(args []string) int {
|
|
|
76
76
|
// Don't pay contributor-registration cost for the version banner.
|
|
77
77
|
fmt.Fprintf(os.Stdout, "@ttsc/lint %s\n", Version)
|
|
78
78
|
return 0
|
|
79
|
-
case "check", "fix", "format", "build", "transform", "lsp-command-ids", "lsp-code-action-kinds", "lsp-diagnostics", "lsp-code-actions", "lsp-execute-command":
|
|
79
|
+
case "check", "fix", "format", "build", "transform", "lsp-command-ids", "lsp-code-action-kinds", "lsp-diagnostics", "lsp-code-actions", "lsp-execute-command", "lsp-hints", "lsp-serve":
|
|
80
80
|
default:
|
|
81
81
|
fmt.Fprintf(os.Stderr, "@ttsc/lint: unknown command %q\n", args[0])
|
|
82
82
|
return 2
|
|
@@ -105,6 +105,10 @@ func run(args []string) int {
|
|
|
105
105
|
return RunLSPCodeActions(args[1:])
|
|
106
106
|
case "lsp-execute-command":
|
|
107
107
|
return RunLSPExecuteCommand(args[1:])
|
|
108
|
+
case "lsp-hints":
|
|
109
|
+
return RunLSPHints(args[1:])
|
|
110
|
+
case "lsp-serve":
|
|
111
|
+
return RunLSPServe(os.Stdin, os.Stdout, args[1:])
|
|
108
112
|
}
|
|
109
113
|
return 2
|
|
110
114
|
}
|
|
@@ -1,25 +1,72 @@
|
|
|
1
1
|
package linthost
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import (
|
|
4
|
+
"regexp"
|
|
5
|
+
"strings"
|
|
6
|
+
"sync"
|
|
7
|
+
"unicode/utf16"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
//go:generate node ../tools/widthgen/main.cjs
|
|
4
11
|
|
|
5
12
|
// displayWidth returns the number of terminal columns a string occupies,
|
|
6
|
-
// matching Prettier's getStringWidth
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// the layout engine's fit checks and the print-width rule's source measurement,
|
|
10
|
-
// must use display columns rather than byte length, or a multi-byte token (a
|
|
13
|
+
// matching Prettier's getStringWidth. Width decisions across the formatter —
|
|
14
|
+
// the layout engine's fit checks and the print-width rule's source measurement
|
|
15
|
+
// — must use display columns rather than byte length, or a multi-byte token (a
|
|
11
16
|
// subscript digit, a Hangul or CJK identifier) is over-counted and the line is
|
|
12
17
|
// wrongly broken.
|
|
13
18
|
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
19
|
+
// This is a transcription of `prettier/src/utilities/get-string-width.js`, not
|
|
20
|
+
// an implementation of the same idea. That distinction is the whole history of
|
|
21
|
+
// this function: it began as a hand-written 13-range table that diverged on
|
|
22
|
+
// 11,482 code points, and a first repair ported `string-width` — the package
|
|
23
|
+
// the report named — which Prettier does not use, and which regressed
|
|
24
|
+
// Devanagari, the Hangul fillers, the bidi controls, and 98 astral
|
|
25
|
+
// text-presentation emoji before it was reverted. The tables and the emoji
|
|
26
|
+
// pattern are generated from the module the lockfile pins, so there is no
|
|
27
|
+
// second source that can be right about a different Prettier.
|
|
28
|
+
//
|
|
29
|
+
// The procedure, in Prettier's order:
|
|
30
|
+
//
|
|
31
|
+
// 1. An empty string is zero, and a pure-ASCII string is its length. The fast
|
|
32
|
+
// path is observable, not merely an optimization: U+007F returns 1 through
|
|
33
|
+
// it although the loop below would skip it.
|
|
34
|
+
// 2. Every RGI emoji match is replaced by one space (for the ~65 Prettier
|
|
35
|
+
// charges narrow) or two, so a sequence costs its own width and its code
|
|
36
|
+
// points are never counted individually.
|
|
37
|
+
// 3. What remains is counted per code point, skipping C0/C1 controls, the
|
|
38
|
+
// combining diacriticals U+0300-U+036F, and the variation selectors
|
|
39
|
+
// U+FE00-U+FE0F, charging 2 for East Asian Wide or Fullwidth and 1
|
|
40
|
+
// otherwise. Per CODE POINT, not per grapheme cluster: a combining mark
|
|
41
|
+
// outside that one block is charged, which is why `हिंदी` is five columns.
|
|
42
|
+
//
|
|
43
|
+
// One deviation is deliberate and predates all of this: a tab counts as one
|
|
44
|
+
// column here, where Prettier's loop skips it as a control. Callers that need
|
|
45
|
+
// tab-stop expansion handle it separately, and the layout engine emits
|
|
46
|
+
// indentation outside the text it measures.
|
|
17
47
|
func displayWidth(s string) int {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
48
|
+
if s == "" {
|
|
49
|
+
return 0
|
|
50
|
+
}
|
|
51
|
+
if !hasNonASCII(s) {
|
|
52
|
+
return len(s)
|
|
21
53
|
}
|
|
22
|
-
|
|
54
|
+
width := 0
|
|
55
|
+
for _, r := range replacePrettierEmoji(s) {
|
|
56
|
+
switch {
|
|
57
|
+
case r == '\t':
|
|
58
|
+
// The documented deviation, kept so tab-stop expansion stays the caller's.
|
|
59
|
+
width++
|
|
60
|
+
case r <= 0x1F, r >= 0x7F && r <= 0x9F:
|
|
61
|
+
case r >= 0x0300 && r <= 0x036F:
|
|
62
|
+
case r >= 0xFE00 && r <= 0xFE0F:
|
|
63
|
+
case isPrettierWide(r):
|
|
64
|
+
width += 2
|
|
65
|
+
default:
|
|
66
|
+
width++
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return width
|
|
23
70
|
}
|
|
24
71
|
|
|
25
72
|
// displayWidthAfterLastNewline returns the display width of the substring that
|
|
@@ -35,38 +82,129 @@ func displayWidthAfterLastNewline(s string) int {
|
|
|
35
82
|
return displayWidth(s)
|
|
36
83
|
}
|
|
37
84
|
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
85
|
+
// displayWidthFromColumn returns the display width of s laid out starting at
|
|
86
|
+
// column `start`, expanding a tab to the next multiple of tabWidth. It is the
|
|
87
|
+
// tab-aware form the source-measuring rules need, where a literal tab in the
|
|
88
|
+
// file does advance to a tab stop; displayWidth itself charges a tab one column
|
|
89
|
+
// because the layout engine emits indentation outside the text it measures.
|
|
90
|
+
func displayWidthFromColumn(s string, tabWidth int, start int) int {
|
|
91
|
+
if tabWidth <= 0 {
|
|
92
|
+
tabWidth = 2
|
|
44
93
|
}
|
|
45
|
-
|
|
46
|
-
|
|
94
|
+
// Walked one tab at a time rather than by splitting, so each tab advances
|
|
95
|
+
// from the column the text before it actually reached.
|
|
96
|
+
column := start
|
|
97
|
+
rest := s
|
|
98
|
+
for {
|
|
99
|
+
index := strings.IndexByte(rest, '\t')
|
|
100
|
+
if index < 0 {
|
|
101
|
+
column += displayWidth(rest)
|
|
102
|
+
break
|
|
103
|
+
}
|
|
104
|
+
column += displayWidth(rest[:index])
|
|
105
|
+
column += tabWidth - (column % tabWidth)
|
|
106
|
+
rest = rest[index+1:]
|
|
47
107
|
}
|
|
48
|
-
return
|
|
108
|
+
return column - start
|
|
49
109
|
}
|
|
50
110
|
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
func
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
111
|
+
// hasNonASCII reports whether s holds a byte outside Prettier's printable-ASCII
|
|
112
|
+
// fast-path range, matching its `/[^\x20-\x7F]/u` test. A multi-byte rune's
|
|
113
|
+
// lead byte is >= 0x80, so a byte scan answers the same question.
|
|
114
|
+
func hasNonASCII(s string) bool {
|
|
115
|
+
for i := 0; i < len(s); i++ {
|
|
116
|
+
if s[i] < 0x20 || s[i] > 0x7F {
|
|
117
|
+
return true
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return false
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
var prettierEmojiRegexp = sync.OnceValue(func() *regexp.Regexp {
|
|
124
|
+
return regexp.MustCompile(prettierEmojiPattern)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// replacePrettierEmoji performs Prettier's emoji substitution: every RGI emoji
|
|
128
|
+
// becomes one space when Prettier charges it one column, two spaces otherwise.
|
|
129
|
+
//
|
|
130
|
+
// The work happens in the shifted UTF-16 domain the generated pattern is
|
|
131
|
+
// written against, because emoji-regex matches code units and a Go string
|
|
132
|
+
// cannot hold a lone surrogate. Text goes in as runes, becomes units, each
|
|
133
|
+
// surrogate unit moves into a private-use range, the match runs, and the result
|
|
134
|
+
// comes back. The mapping is a bijection over a range nothing else occupies.
|
|
135
|
+
func replacePrettierEmoji(s string) string {
|
|
136
|
+
shifted := shiftToUTF16Domain(s)
|
|
137
|
+
replaced := prettierEmojiRegexp().ReplaceAllStringFunc(shifted, func(match string) string {
|
|
138
|
+
if isNarrowPrettierEmoji(match) {
|
|
139
|
+
return " "
|
|
140
|
+
}
|
|
141
|
+
return " "
|
|
142
|
+
})
|
|
143
|
+
if replaced == shifted {
|
|
144
|
+
return s
|
|
145
|
+
}
|
|
146
|
+
return unshiftFromUTF16Domain(replaced)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// isNarrowPrettierEmoji reports whether a match is one of the emoji Prettier
|
|
150
|
+
// charges a single column. The set is single code points, so a match of any
|
|
151
|
+
// other length is never narrow.
|
|
152
|
+
func isNarrowPrettierEmoji(match string) bool {
|
|
153
|
+
runes := []rune(unshiftFromUTF16Domain(match))
|
|
154
|
+
return len(runes) == 1 && inUnicodeRanges(prettierNarrowEmojiRanges[:], runes[0])
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// shiftToUTF16Domain encodes s as UTF-16 and represents each code unit as one
|
|
158
|
+
// rune, relocating surrogates so the result is a valid Go string.
|
|
159
|
+
func shiftToUTF16Domain(s string) string {
|
|
160
|
+
units := utf16.Encode([]rune(s))
|
|
161
|
+
out := make([]rune, 0, len(units))
|
|
162
|
+
for _, unit := range units {
|
|
163
|
+
r := rune(unit)
|
|
164
|
+
if r >= 0xD800 && r <= 0xDFFF {
|
|
165
|
+
r = r - 0xD800 + surrogateShift
|
|
166
|
+
}
|
|
167
|
+
out = append(out, r)
|
|
168
|
+
}
|
|
169
|
+
return string(out)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// unshiftFromUTF16Domain is shiftToUTF16Domain's inverse.
|
|
173
|
+
func unshiftFromUTF16Domain(s string) string {
|
|
174
|
+
units := make([]uint16, 0, len(s))
|
|
175
|
+
for _, r := range s {
|
|
176
|
+
if r >= surrogateShift && r <= surrogateShift+0x7FF {
|
|
177
|
+
r = r - surrogateShift + 0xD800
|
|
178
|
+
}
|
|
179
|
+
if r > 0xFFFF {
|
|
180
|
+
units = append(units, utf16.Encode([]rune{r})...)
|
|
181
|
+
continue
|
|
182
|
+
}
|
|
183
|
+
units = append(units, uint16(r))
|
|
184
|
+
}
|
|
185
|
+
return string(utf16.Decode(units))
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
func isPrettierWide(r rune) bool {
|
|
189
|
+
return inUnicodeRanges(prettierWideRanges[:], r) ||
|
|
190
|
+
inUnicodeRanges(prettierFullWidthRanges[:], r)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// inUnicodeRanges reports whether r falls in one of the sorted, disjoint
|
|
194
|
+
// ranges of a generated property table.
|
|
195
|
+
func inUnicodeRanges(ranges []unicodeRange, r rune) bool {
|
|
196
|
+
lo, hi := 0, len(ranges)
|
|
197
|
+
for lo < hi {
|
|
198
|
+
middle := int(uint(lo+hi) >> 1)
|
|
199
|
+
candidate := ranges[middle]
|
|
200
|
+
switch {
|
|
201
|
+
case r < candidate.lo:
|
|
202
|
+
hi = middle
|
|
203
|
+
case r > candidate.hi:
|
|
204
|
+
lo = middle + 1
|
|
205
|
+
default:
|
|
206
|
+
return true
|
|
207
|
+
}
|
|
70
208
|
}
|
|
71
209
|
return false
|
|
72
210
|
}
|
package/linthost/engine.go
CHANGED
|
@@ -107,6 +107,17 @@ func ruleNeedsTypeChecker(r Rule) bool {
|
|
|
107
107
|
return ok && tr.NeedsTypeChecker()
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
// ruleDiagnosticTags returns the diagnostic tags a rule classifies its findings
|
|
111
|
+
// with, or nil when the rule implements no marker or returns none. Read once per
|
|
112
|
+
// (file, rule) at dispatch and copied onto every finding the rule produces.
|
|
113
|
+
func ruleDiagnosticTags(r Rule) []publicrule.DiagnosticTag {
|
|
114
|
+
tagged, ok := r.(publicrule.TaggedRule)
|
|
115
|
+
if !ok {
|
|
116
|
+
return nil
|
|
117
|
+
}
|
|
118
|
+
return tagged.DiagnosticTags()
|
|
119
|
+
}
|
|
120
|
+
|
|
110
121
|
func ruleAcceptsOptions(r Rule) bool {
|
|
111
122
|
acceptor, ok := r.(ruleOptionsAcceptor)
|
|
112
123
|
return ok && acceptor.AcceptsTtscLintOptions()
|
|
@@ -139,6 +150,7 @@ type Context struct {
|
|
|
139
150
|
|
|
140
151
|
rule Rule
|
|
141
152
|
isFormat bool
|
|
153
|
+
tags []publicrule.DiagnosticTag
|
|
142
154
|
quarantined bool
|
|
143
155
|
collect func(*Finding)
|
|
144
156
|
projectResults publicrule.ProjectResultReader
|
|
@@ -216,6 +228,15 @@ type Finding struct {
|
|
|
216
228
|
Fix []TextEdit
|
|
217
229
|
Suggestions []Suggestion
|
|
218
230
|
IsFormat bool
|
|
231
|
+
// Tags classify what the finding is (unnecessary, deprecated), for an editor
|
|
232
|
+
// to render it distinctively. Populated from the rule's TaggedRule marker at
|
|
233
|
+
// dispatch, so every finding a tagged rule produces carries its tags.
|
|
234
|
+
Tags []publicrule.DiagnosticTag
|
|
235
|
+
// RelatedInformation are secondary locations this finding points at, each with
|
|
236
|
+
// a message. Positions are byte offsets into File — already normalized against
|
|
237
|
+
// it at report time — so the LSP renderer resolves them against File's text
|
|
238
|
+
// and attaches File's own URI.
|
|
239
|
+
RelatedInformation []publicrule.RelatedInformation
|
|
219
240
|
|
|
220
241
|
engineFailure bool
|
|
221
242
|
}
|
|
@@ -262,6 +283,7 @@ func (c *Context) ReportFix(node *shimast.Node, message string, edits ...TextEdi
|
|
|
262
283
|
Message: message,
|
|
263
284
|
Fix: cloneTextEdits(edits),
|
|
264
285
|
IsFormat: c.isFormat,
|
|
286
|
+
Tags: c.tags,
|
|
265
287
|
})
|
|
266
288
|
}
|
|
267
289
|
|
|
@@ -282,6 +304,7 @@ func (c *Context) ReportSuggestion(node *shimast.Node, message string, title str
|
|
|
282
304
|
Message: message,
|
|
283
305
|
Suggestions: newSuggestions(title, edits),
|
|
284
306
|
IsFormat: c.isFormat,
|
|
307
|
+
Tags: c.tags,
|
|
285
308
|
})
|
|
286
309
|
}
|
|
287
310
|
|
|
@@ -309,6 +332,7 @@ func (c *Context) ReportFixSuggestions(
|
|
|
309
332
|
Fix: cloneTextEdits(fix),
|
|
310
333
|
Suggestions: cloneSuggestions(suggestions),
|
|
311
334
|
IsFormat: c.isFormat,
|
|
335
|
+
Tags: c.tags,
|
|
312
336
|
})
|
|
313
337
|
}
|
|
314
338
|
|
|
@@ -348,6 +372,7 @@ func (c *Context) ReportRangeFix(pos, end int, message string, edits ...TextEdit
|
|
|
348
372
|
Message: message,
|
|
349
373
|
Fix: cloneTextEdits(edits),
|
|
350
374
|
IsFormat: c.isFormat,
|
|
375
|
+
Tags: c.tags,
|
|
351
376
|
})
|
|
352
377
|
}
|
|
353
378
|
|
|
@@ -368,9 +393,95 @@ func (c *Context) ReportRangeSuggestion(pos, end int, message string, title stri
|
|
|
368
393
|
Message: message,
|
|
369
394
|
Suggestions: newSuggestions(title, edits),
|
|
370
395
|
IsFormat: c.isFormat,
|
|
396
|
+
Tags: c.tags,
|
|
397
|
+
})
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ReportRangeSuggestions records a finding at an explicit range with several
|
|
401
|
+
// candidate suggestions. It is the range counterpart of ReportFixSuggestions,
|
|
402
|
+
// added so the public contributor surface can offer a choice at a sub-token
|
|
403
|
+
// range and not only at a whole node.
|
|
404
|
+
func (c *Context) ReportRangeSuggestions(pos, end int, message string, suggestions ...Suggestion) {
|
|
405
|
+
if c.Severity == SeverityOff || c.File == nil {
|
|
406
|
+
return
|
|
407
|
+
}
|
|
408
|
+
pos, end = shimdw.NormalizeLintRange(c.File, pos, end)
|
|
409
|
+
c.collect(&Finding{
|
|
410
|
+
Rule: c.rule.Name(),
|
|
411
|
+
Severity: c.Severity,
|
|
412
|
+
File: c.File,
|
|
413
|
+
Pos: pos,
|
|
414
|
+
End: end,
|
|
415
|
+
Message: message,
|
|
416
|
+
Suggestions: cloneSuggestions(suggestions),
|
|
417
|
+
IsFormat: c.isFormat,
|
|
418
|
+
Tags: c.tags,
|
|
419
|
+
})
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// ReportRelated records a node-scoped finding with related source locations.
|
|
423
|
+
// Each related location's Pos/End is normalized against the current file — the
|
|
424
|
+
// same bounding a range finding gets — so a rule that miscomputed an offset
|
|
425
|
+
// cannot point the editor outside the file.
|
|
426
|
+
func (c *Context) ReportRelated(node *shimast.Node, message string, related ...publicrule.RelatedInformation) {
|
|
427
|
+
if c.Severity == SeverityOff || node == nil {
|
|
428
|
+
return
|
|
429
|
+
}
|
|
430
|
+
pos, end := c.nodeFindingRange(node)
|
|
431
|
+
c.collect(&Finding{
|
|
432
|
+
Rule: c.rule.Name(),
|
|
433
|
+
Severity: c.Severity,
|
|
434
|
+
File: c.File,
|
|
435
|
+
Pos: pos,
|
|
436
|
+
End: end,
|
|
437
|
+
Message: message,
|
|
438
|
+
RelatedInformation: c.normalizeRelated(related),
|
|
439
|
+
IsFormat: c.isFormat,
|
|
440
|
+
Tags: c.tags,
|
|
441
|
+
})
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// ReportRangeRelated records an explicit-range finding with related source
|
|
445
|
+
// locations. Both the primary range and each related range are normalized
|
|
446
|
+
// against the current file.
|
|
447
|
+
func (c *Context) ReportRangeRelated(pos, end int, message string, related ...publicrule.RelatedInformation) {
|
|
448
|
+
if c.Severity == SeverityOff || c.File == nil {
|
|
449
|
+
return
|
|
450
|
+
}
|
|
451
|
+
pos, end = shimdw.NormalizeLintRange(c.File, pos, end)
|
|
452
|
+
c.collect(&Finding{
|
|
453
|
+
Rule: c.rule.Name(),
|
|
454
|
+
Severity: c.Severity,
|
|
455
|
+
File: c.File,
|
|
456
|
+
Pos: pos,
|
|
457
|
+
End: end,
|
|
458
|
+
Message: message,
|
|
459
|
+
RelatedInformation: c.normalizeRelated(related),
|
|
460
|
+
IsFormat: c.isFormat,
|
|
461
|
+
Tags: c.tags,
|
|
371
462
|
})
|
|
372
463
|
}
|
|
373
464
|
|
|
465
|
+
// normalizeRelated copies the caller's related locations and bounds each range
|
|
466
|
+
// to the current file, so the stored Finding owns its slice and every position
|
|
467
|
+
// is already safe for the renderer. Returns nil for an empty input, keeping the
|
|
468
|
+
// Finding field nil rather than a zero-length slice.
|
|
469
|
+
func (c *Context) normalizeRelated(related []publicrule.RelatedInformation) []publicrule.RelatedInformation {
|
|
470
|
+
if len(related) == 0 {
|
|
471
|
+
return nil
|
|
472
|
+
}
|
|
473
|
+
out := make([]publicrule.RelatedInformation, 0, len(related))
|
|
474
|
+
for _, item := range related {
|
|
475
|
+
pos, end := shimdw.NormalizeLintRange(c.File, item.Pos, item.End)
|
|
476
|
+
out = append(out, publicrule.RelatedInformation{
|
|
477
|
+
Pos: pos,
|
|
478
|
+
End: end,
|
|
479
|
+
Message: item.Message,
|
|
480
|
+
})
|
|
481
|
+
}
|
|
482
|
+
return out
|
|
483
|
+
}
|
|
484
|
+
|
|
374
485
|
// cloneTextEdits returns a shallow copy of `edits` so that the caller's
|
|
375
486
|
// variadic slice cannot be mutated through the stored Finding. Returns nil
|
|
376
487
|
// when the input is empty, keeping the Finding.Fix field nil rather than
|
|
@@ -529,7 +640,11 @@ func NewEngineWithResolver(config RuleResolver) *Engine {
|
|
|
529
640
|
fmt.Errorf("@ttsc/lint: invalid options for rule %q: rule does not accept options", name),
|
|
530
641
|
)
|
|
531
642
|
}
|
|
532
|
-
|
|
643
|
+
// A project rule shares the engine-wide checker decision with every file
|
|
644
|
+
// rule, so one that declines the checker must not drag the whole run onto
|
|
645
|
+
// the serial walk. An unmarked rule keeps the conservative default.
|
|
646
|
+
if setting.Declared && setting.Severity != SeverityOff &&
|
|
647
|
+
projectRuleNeedsTypeChecker(name) {
|
|
533
648
|
eng.needsTypeChecker = true
|
|
534
649
|
}
|
|
535
650
|
}
|
|
@@ -554,11 +669,11 @@ func NewEngineWithResolver(config RuleResolver) *Engine {
|
|
|
554
669
|
}
|
|
555
670
|
}
|
|
556
671
|
for _, name := range config.ActiveRuleNames() {
|
|
557
|
-
if _, isProjectRule := registeredProjectRules[name]; isProjectRule {
|
|
558
|
-
continue
|
|
559
|
-
}
|
|
560
672
|
rule, ok := registered.rules[name]
|
|
561
673
|
if !ok {
|
|
674
|
+
if _, isProjectRule := registeredProjectRules[name]; isProjectRule {
|
|
675
|
+
continue
|
|
676
|
+
}
|
|
562
677
|
eng.unknown = append(eng.unknown, name)
|
|
563
678
|
continue
|
|
564
679
|
}
|
|
@@ -869,6 +984,7 @@ func (e *Engine) runFile(
|
|
|
869
984
|
Options: options,
|
|
870
985
|
rule: rule,
|
|
871
986
|
isFormat: isFormatRule(rule),
|
|
987
|
+
tags: ruleDiagnosticTags(rule),
|
|
872
988
|
collect: collect,
|
|
873
989
|
projectResults: results,
|
|
874
990
|
fileMemo: memo,
|
package/linthost/flags_gen.go
CHANGED
|
@@ -17,17 +17,17 @@ var LintFlagAllowList = map[string]bool{
|
|
|
17
17
|
"cwd": true,
|
|
18
18
|
"diagnostics": false,
|
|
19
19
|
"emit": false,
|
|
20
|
-
"
|
|
20
|
+
"extendeddiagnostics": false,
|
|
21
21
|
"file": true,
|
|
22
|
-
"
|
|
22
|
+
"noemit": false,
|
|
23
23
|
"out": true,
|
|
24
|
-
"
|
|
24
|
+
"outdir": true,
|
|
25
25
|
"p": true,
|
|
26
26
|
"plugins-json": true,
|
|
27
27
|
"project": true,
|
|
28
28
|
"project-context-json": true,
|
|
29
29
|
"quiet": false,
|
|
30
|
-
"
|
|
30
|
+
"singlethreaded": false,
|
|
31
31
|
"tsconfig": true,
|
|
32
32
|
"tsgo-args": true,
|
|
33
33
|
"verbose": false,
|