@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
package/linthost/rules_regexp.go
CHANGED
|
@@ -18,14 +18,40 @@ import (
|
|
|
18
18
|
type regexpSourceRule struct {
|
|
19
19
|
name string
|
|
20
20
|
check func(regexpLiteralParts) bool
|
|
21
|
+
// repair turns an accepted finding into the correction the check already
|
|
22
|
+
// located. A rule without one stays diagnostic-only.
|
|
23
|
+
repair func(regexpLiteralParts) regexpRepair
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
type regexpLiteralParts struct {
|
|
27
|
+
// start is the literal's offset in the source file. Repairs are computed in
|
|
28
|
+
// literal-relative coordinates and lifted onto the file through it.
|
|
29
|
+
start int
|
|
24
30
|
raw string
|
|
25
31
|
pattern string
|
|
26
32
|
flags string
|
|
27
33
|
}
|
|
28
34
|
|
|
35
|
+
// patternOffset is where the pattern begins inside `raw`, past the opening `/`.
|
|
36
|
+
func (regexpLiteralParts) patternOffset() int { return 1 }
|
|
37
|
+
|
|
38
|
+
// flagsOffset is where the flag run begins inside `raw`, past the closing `/`.
|
|
39
|
+
func (parts regexpLiteralParts) flagsOffset() int { return len(parts.pattern) + 2 }
|
|
40
|
+
|
|
41
|
+
// regexpRepair is the correction a regexp rule computed for one literal.
|
|
42
|
+
//
|
|
43
|
+
// Its edits are relative to the literal's own text, so a rule never handles
|
|
44
|
+
// file coordinates; `Check` validates and lifts the whole repair in one place.
|
|
45
|
+
type regexpRepair struct {
|
|
46
|
+
// message replaces regexpRuleMessage when non-empty, for a rule whose
|
|
47
|
+
// finding can name the exact thing it located.
|
|
48
|
+
message string
|
|
49
|
+
// fix is the one correct rewrite, applied by `ttsc fix`.
|
|
50
|
+
fix []TextEdit
|
|
51
|
+
// suggestions are competing rewrites only the author can choose between.
|
|
52
|
+
suggestions []Suggestion
|
|
53
|
+
}
|
|
54
|
+
|
|
29
55
|
func (r regexpSourceRule) Name() string { return r.name }
|
|
30
56
|
func (regexpSourceRule) Visits() []shimast.Kind {
|
|
31
57
|
return []shimast.Kind{shimast.KindRegularExpressionLiteral}
|
|
@@ -35,7 +61,80 @@ func (r regexpSourceRule) Check(ctx *Context, node *shimast.Node) {
|
|
|
35
61
|
if !ok || r.check == nil || !r.check(parts) {
|
|
36
62
|
return
|
|
37
63
|
}
|
|
38
|
-
|
|
64
|
+
message := regexpRuleMessage(r.name)
|
|
65
|
+
if r.repair == nil {
|
|
66
|
+
ctx.Report(node, message)
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
repair := r.repair(parts)
|
|
70
|
+
if repair.message != "" {
|
|
71
|
+
message = repair.message
|
|
72
|
+
}
|
|
73
|
+
suggestions := make([]Suggestion, 0, len(repair.suggestions))
|
|
74
|
+
for _, suggestion := range repair.suggestions {
|
|
75
|
+
edits := parts.acceptEdits(suggestion.Edits)
|
|
76
|
+
if len(edits) == 0 {
|
|
77
|
+
continue
|
|
78
|
+
}
|
|
79
|
+
suggestions = append(suggestions, Suggestion{Title: suggestion.Title, Edits: edits})
|
|
80
|
+
}
|
|
81
|
+
ctx.ReportFixSuggestions(node, message, parts.acceptEdits(repair.fix), suggestions...)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// acceptEdits validates one candidate rewrite of this literal and lifts it into
|
|
85
|
+
// file coordinates, or returns nil to leave the finding without that edit.
|
|
86
|
+
//
|
|
87
|
+
// The gate is the compiler's own regexp parser applied to the rewritten literal
|
|
88
|
+
// as a whole. Every repair here is a splice into live regex syntax, where a
|
|
89
|
+
// locally correct edit can still leave a pattern the engine rejects: `/{1,}/`
|
|
90
|
+
// carries no atom, so its brace run is an Annex B literal rather than a
|
|
91
|
+
// quantifier, and rewriting it to `/+/` yields "nothing to repeat". Rules stay
|
|
92
|
+
// free to compute the ideal edit and cannot emit one that fails to parse.
|
|
93
|
+
//
|
|
94
|
+
// Validity is not equivalence. That a rewrite still parses says nothing about
|
|
95
|
+
// whether it matches the same strings, so preserving semantics remains each
|
|
96
|
+
// rule's own burden -- this only keeps a syntactically broken edit off disk.
|
|
97
|
+
func (parts regexpLiteralParts) acceptEdits(edits []TextEdit) []TextEdit {
|
|
98
|
+
rewritten, ok := applyRegexpLiteralEdits(parts.raw, edits)
|
|
99
|
+
if !ok || rewritten == parts.raw {
|
|
100
|
+
return nil
|
|
101
|
+
}
|
|
102
|
+
if !shimscanner.IsValidRegularExpressionLiteral(rewritten) {
|
|
103
|
+
return nil
|
|
104
|
+
}
|
|
105
|
+
shifted := make([]TextEdit, 0, len(edits))
|
|
106
|
+
for _, edit := range edits {
|
|
107
|
+
shifted = append(shifted, TextEdit{
|
|
108
|
+
Pos: parts.start + edit.Pos,
|
|
109
|
+
End: parts.start + edit.End,
|
|
110
|
+
Text: edit.Text,
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
return shifted
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// applyRegexpLiteralEdits splices literal-relative edits into `raw`. It reports
|
|
117
|
+
// false for an empty, out-of-bounds, or overlapping edit set rather than
|
|
118
|
+
// producing text from a half-applied rewrite.
|
|
119
|
+
func applyRegexpLiteralEdits(raw string, edits []TextEdit) (string, bool) {
|
|
120
|
+
if len(edits) == 0 {
|
|
121
|
+
return "", false
|
|
122
|
+
}
|
|
123
|
+
ordered := make([]TextEdit, len(edits))
|
|
124
|
+
copy(ordered, edits)
|
|
125
|
+
sort.SliceStable(ordered, func(i, j int) bool { return ordered[i].Pos < ordered[j].Pos })
|
|
126
|
+
boundary := 0
|
|
127
|
+
for _, edit := range ordered {
|
|
128
|
+
if edit.Pos < boundary || edit.End < edit.Pos || edit.End > len(raw) {
|
|
129
|
+
return "", false
|
|
130
|
+
}
|
|
131
|
+
boundary = edit.End
|
|
132
|
+
}
|
|
133
|
+
out := raw
|
|
134
|
+
for i := len(ordered) - 1; i >= 0; i-- {
|
|
135
|
+
out = out[:ordered[i].Pos] + ordered[i].Text + out[ordered[i].End:]
|
|
136
|
+
}
|
|
137
|
+
return out, true
|
|
39
138
|
}
|
|
40
139
|
|
|
41
140
|
type regexpNoUselessEscapeAlias struct{}
|
|
@@ -53,8 +152,11 @@ func (regexpNoUselessEscapeAlias) Check(ctx *Context, node *shimast.Node) {
|
|
|
53
152
|
}
|
|
54
153
|
|
|
55
154
|
func parseRegexpLiteralParts(ctx *Context, node *shimast.Node) (regexpLiteralParts, bool) {
|
|
155
|
+
// `nodeText` and `tokenRange` skip the same leading trivia, so the text a
|
|
156
|
+
// repair reasons about always begins at exactly `start` in the file.
|
|
157
|
+
start, _ := tokenRange(ctx.File, node)
|
|
56
158
|
raw := nodeText(ctx.File, node)
|
|
57
|
-
if len(raw) < 2 || raw[0] != '/' {
|
|
159
|
+
if start < 0 || len(raw) < 2 || raw[0] != '/' {
|
|
58
160
|
return regexpLiteralParts{}, false
|
|
59
161
|
}
|
|
60
162
|
closing := strings.LastIndexByte(raw, '/')
|
|
@@ -62,6 +164,7 @@ func parseRegexpLiteralParts(ctx *Context, node *shimast.Node) (regexpLiteralPar
|
|
|
62
164
|
return regexpLiteralParts{}, false
|
|
63
165
|
}
|
|
64
166
|
return regexpLiteralParts{
|
|
167
|
+
start: start,
|
|
65
168
|
raw: raw,
|
|
66
169
|
pattern: raw[1:closing],
|
|
67
170
|
flags: raw[closing+1:],
|
|
@@ -184,40 +287,34 @@ func regexpHasEmptyCharacterClass(parts regexpLiteralParts) bool {
|
|
|
184
287
|
return false
|
|
185
288
|
}
|
|
186
289
|
|
|
187
|
-
func
|
|
188
|
-
return
|
|
189
|
-
return min == 0 && (!hasComma || max == 0)
|
|
190
|
-
})
|
|
290
|
+
func regexpQuantifierIsZero(quantifier regexpQuantifier) bool {
|
|
291
|
+
return quantifier.min == 0 && (!quantifier.hasComma || quantifier.max == 0)
|
|
191
292
|
}
|
|
192
293
|
|
|
193
|
-
func
|
|
194
|
-
return
|
|
195
|
-
return hasComma && min == max && min >= 0
|
|
196
|
-
})
|
|
294
|
+
func regexpQuantifierIsTwoNums(quantifier regexpQuantifier) bool {
|
|
295
|
+
return quantifier.hasComma && quantifier.min == quantifier.max && quantifier.min >= 0
|
|
197
296
|
}
|
|
198
297
|
|
|
199
|
-
func
|
|
200
|
-
return
|
|
201
|
-
return !hasComma && min == 1 && max == -1
|
|
202
|
-
})
|
|
298
|
+
func regexpQuantifierIsUseless(quantifier regexpQuantifier) bool {
|
|
299
|
+
return !quantifier.hasComma && quantifier.min == 1 && quantifier.max == -1
|
|
203
300
|
}
|
|
204
301
|
|
|
205
|
-
func
|
|
206
|
-
return
|
|
207
|
-
return hasComma && min == 1 && max == -1
|
|
208
|
-
})
|
|
302
|
+
func regexpQuantifierIsPlus(quantifier regexpQuantifier) bool {
|
|
303
|
+
return quantifier.hasComma && quantifier.min == 1 && quantifier.max == -1
|
|
209
304
|
}
|
|
210
305
|
|
|
211
|
-
func
|
|
212
|
-
return
|
|
213
|
-
return hasComma && min == 0 && max == -1
|
|
214
|
-
})
|
|
306
|
+
func regexpQuantifierIsStar(quantifier regexpQuantifier) bool {
|
|
307
|
+
return quantifier.hasComma && quantifier.min == 0 && quantifier.max == -1
|
|
215
308
|
}
|
|
216
309
|
|
|
217
|
-
func
|
|
218
|
-
return
|
|
219
|
-
|
|
220
|
-
|
|
310
|
+
func regexpQuantifierIsQuestion(quantifier regexpQuantifier) bool {
|
|
311
|
+
return quantifier.hasComma && quantifier.min == 0 && quantifier.max == 1
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
func regexpQuantifierCheck(accept func(regexpQuantifier) bool) func(regexpLiteralParts) bool {
|
|
315
|
+
return func(parts regexpLiteralParts) bool {
|
|
316
|
+
return scanRegexpQuantifiers(parts.pattern, accept)
|
|
317
|
+
}
|
|
221
318
|
}
|
|
222
319
|
|
|
223
320
|
func regexpNeedsUnicodeFlag(parts regexpLiteralParts) bool {
|
|
@@ -229,11 +326,75 @@ func regexpNeedsUnicodeSetsFlag(parts regexpLiteralParts) bool {
|
|
|
229
326
|
}
|
|
230
327
|
|
|
231
328
|
func regexpFlagsUnsorted(parts regexpLiteralParts) bool {
|
|
232
|
-
|
|
329
|
+
return regexpSortedFlags(parts.flags) != parts.flags
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
func regexpSortedFlags(flags string) string {
|
|
333
|
+
sorted := []byte(flags)
|
|
233
334
|
sort.SliceStable(sorted, func(i, j int) bool {
|
|
234
335
|
return regexpFlagOrder(sorted[i]) < regexpFlagOrder(sorted[j])
|
|
235
336
|
})
|
|
236
|
-
return string(sorted)
|
|
337
|
+
return string(sorted)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// regexpSortFlagsRepair hands back the sorted flag string the check already
|
|
341
|
+
// built to decide the finding. A permutation of the flag run cannot change what
|
|
342
|
+
// the literal matches, so this is a fix rather than a suggestion.
|
|
343
|
+
func regexpSortFlagsRepair(parts regexpLiteralParts) regexpRepair {
|
|
344
|
+
return regexpRepair{fix: []TextEdit{parts.flagEdit(regexpSortedFlags(parts.flags))}}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// flagEdit replaces this literal's whole flag run.
|
|
348
|
+
func (parts regexpLiteralParts) flagEdit(flags string) TextEdit {
|
|
349
|
+
return TextEdit{
|
|
350
|
+
Pos: parts.flagsOffset(),
|
|
351
|
+
End: parts.flagsOffset() + len(parts.flags),
|
|
352
|
+
Text: flags,
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// regexpFlagsWith inserts `flag` at its canonical `dgimsuvy` position without
|
|
357
|
+
// reordering the flags already present, so adding one flag never silently does
|
|
358
|
+
// `regexp/sort-flags`' job on an unsorted literal.
|
|
359
|
+
func regexpFlagsWith(flags string, flag byte) string {
|
|
360
|
+
if strings.IndexByte(flags, flag) >= 0 {
|
|
361
|
+
return flags
|
|
362
|
+
}
|
|
363
|
+
order := regexpFlagOrder(flag)
|
|
364
|
+
for i := 0; i < len(flags); i++ {
|
|
365
|
+
if regexpFlagOrder(flags[i]) > order {
|
|
366
|
+
return flags[:i] + string(flag) + flags[i:]
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return flags + string(flag)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// regexpUnicodeFlagRepair offers `u` and `v` as competing suggestions.
|
|
373
|
+
//
|
|
374
|
+
// Both satisfy the rule and neither is the obvious answer: `u` is the widely
|
|
375
|
+
// supported mode, `v` the stricter ES2024 superset. Both also change what the
|
|
376
|
+
// pattern matches -- surrogate pairs stop being two independent code units --
|
|
377
|
+
// so this is never applied automatically by `ttsc fix`.
|
|
378
|
+
func regexpUnicodeFlagRepair(parts regexpLiteralParts) regexpRepair {
|
|
379
|
+
return regexpRepair{suggestions: []Suggestion{
|
|
380
|
+
{Title: "Add the `u` flag.", Edits: []TextEdit{parts.flagEdit(regexpFlagsWith(parts.flags, 'u'))}},
|
|
381
|
+
{Title: "Add the `v` flag.", Edits: []TextEdit{parts.flagEdit(regexpFlagsWith(parts.flags, 'v'))}},
|
|
382
|
+
}}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// regexpUnicodeSetsFlagRepair offers the single `v` rewrite, replacing `u`
|
|
386
|
+
// where the literal already carries it. It stays a suggestion for the same
|
|
387
|
+
// reason as regexpUnicodeFlagRepair: `v` is a stricter mode with its own
|
|
388
|
+
// matching semantics, not a spelling of the existing pattern.
|
|
389
|
+
func regexpUnicodeSetsFlagRepair(parts regexpLiteralParts) regexpRepair {
|
|
390
|
+
title, flags := "Add the `v` flag.", parts.flags
|
|
391
|
+
if strings.IndexByte(flags, 'u') >= 0 {
|
|
392
|
+
title = "Replace the `u` flag with `v`."
|
|
393
|
+
flags = strings.Replace(flags, "u", "", 1)
|
|
394
|
+
}
|
|
395
|
+
return regexpRepair{suggestions: []Suggestion{
|
|
396
|
+
{Title: title, Edits: []TextEdit{parts.flagEdit(regexpFlagsWith(flags, 'v'))}},
|
|
397
|
+
}}
|
|
237
398
|
}
|
|
238
399
|
|
|
239
400
|
func regexpFlagOrder(flag byte) int {
|
|
@@ -257,22 +418,65 @@ func regexpFlagOrder(flag byte) int {
|
|
|
257
418
|
// A literal the parser rejects yields no finding at all: the rule tells people
|
|
258
419
|
// to delete a flag, so it stays silent whenever it cannot see the whole pattern.
|
|
259
420
|
func regexpHasUselessFlag(parts regexpLiteralParts) bool {
|
|
421
|
+
return regexpUselessFlags(parts) != ""
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// regexpUselessFlags returns every flag the literal carries that its own
|
|
425
|
+
// pattern can never exercise, in canonical order.
|
|
426
|
+
func regexpUselessFlags(parts regexpLiteralParts) string {
|
|
260
427
|
ignoreCase := strings.Contains(parts.flags, "i")
|
|
261
428
|
multiline := strings.Contains(parts.flags, "m")
|
|
262
429
|
if !ignoreCase && !multiline {
|
|
263
|
-
return
|
|
430
|
+
return ""
|
|
264
431
|
}
|
|
265
432
|
parsed, err := regexParseLiteral(parts.raw)
|
|
266
433
|
if err != nil {
|
|
267
|
-
return
|
|
434
|
+
return ""
|
|
268
435
|
}
|
|
436
|
+
useless := make([]byte, 0, 2)
|
|
269
437
|
if ignoreCase && !regexpNodeIsCaseVariant(parsed.Body, strings.ContainsAny(parts.flags, "uv")) {
|
|
270
|
-
|
|
438
|
+
useless = append(useless, 'i')
|
|
271
439
|
}
|
|
272
440
|
if multiline && !regexpNodeHasLineAnchor(parsed.Body) {
|
|
273
|
-
|
|
441
|
+
useless = append(useless, 'm')
|
|
274
442
|
}
|
|
275
|
-
return
|
|
443
|
+
return string(useless)
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// regexpUselessFlagRepair deletes the dead flags the analysis named. The
|
|
447
|
+
// analysis is one-sided -- anything it cannot settle counts as using the flag
|
|
448
|
+
// -- so a flag it does reach here is provably inert and the deletion is a fix
|
|
449
|
+
// rather than a suggestion.
|
|
450
|
+
func regexpUselessFlagRepair(parts regexpLiteralParts) regexpRepair {
|
|
451
|
+
useless := regexpUselessFlags(parts)
|
|
452
|
+
if useless == "" {
|
|
453
|
+
return regexpRepair{}
|
|
454
|
+
}
|
|
455
|
+
var edits []TextEdit
|
|
456
|
+
for i := 0; i < len(parts.flags); i++ {
|
|
457
|
+
if strings.IndexByte(useless, parts.flags[i]) < 0 {
|
|
458
|
+
continue
|
|
459
|
+
}
|
|
460
|
+
edits = append(edits, TextEdit{
|
|
461
|
+
Pos: parts.flagsOffset() + i,
|
|
462
|
+
End: parts.flagsOffset() + i + 1,
|
|
463
|
+
})
|
|
464
|
+
}
|
|
465
|
+
return regexpRepair{message: regexpUselessFlagMessage(useless), fix: edits}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// regexpUselessFlagMessage names the flags rather than leaving the reader to
|
|
469
|
+
// rediscover which one the analysis found inert. Only `i` and `m` are ever
|
|
470
|
+
// judged, so the list is one or two entries.
|
|
471
|
+
func regexpUselessFlagMessage(useless string) string {
|
|
472
|
+
quoted := make([]string, 0, len(useless))
|
|
473
|
+
for i := 0; i < len(useless); i++ {
|
|
474
|
+
quoted = append(quoted, "`"+string(useless[i])+"`")
|
|
475
|
+
}
|
|
476
|
+
if len(quoted) == 1 {
|
|
477
|
+
return "Unexpected useless regular expression flag " + quoted[0] + "."
|
|
478
|
+
}
|
|
479
|
+
return "Unexpected useless regular expression flags " + strings.Join(quoted, " and ") + "."
|
|
276
480
|
}
|
|
277
481
|
|
|
278
482
|
func regexpHasPreferD(parts regexpLiteralParts) bool {
|
|
@@ -341,7 +545,22 @@ func scanRegexpPattern(pattern string, visit func(pattern string, i int) bool) b
|
|
|
341
545
|
return false
|
|
342
546
|
}
|
|
343
547
|
|
|
344
|
-
|
|
548
|
+
// regexpQuantifier is one `{...}` count quantifier located in a pattern.
|
|
549
|
+
//
|
|
550
|
+
// The span travels with the bounds because every quantifier-shorthand rule in
|
|
551
|
+
// this family answers "is this quantifier redundant?" and "what replaces it?"
|
|
552
|
+
// from the same scan.
|
|
553
|
+
type regexpQuantifier struct {
|
|
554
|
+
// start and end bracket `{`..`}` inclusive-exclusive, relative to the
|
|
555
|
+
// pattern rather than to the whole literal.
|
|
556
|
+
start int
|
|
557
|
+
end int
|
|
558
|
+
min int
|
|
559
|
+
max int
|
|
560
|
+
hasComma bool
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
func scanRegexpQuantifiers(pattern string, visit func(regexpQuantifier) bool) bool {
|
|
345
564
|
return scanRegexpPattern(pattern, func(pattern string, i int) bool {
|
|
346
565
|
if pattern[i] != '{' {
|
|
347
566
|
return false
|
|
@@ -365,10 +584,81 @@ func scanRegexpQuantifiers(pattern string, visit func(min, max int, hasComma boo
|
|
|
365
584
|
} else {
|
|
366
585
|
min = parseRegexpQuantifierNumber(body)
|
|
367
586
|
}
|
|
368
|
-
return min >= 0 && visit(
|
|
587
|
+
return min >= 0 && visit(regexpQuantifier{
|
|
588
|
+
start: i,
|
|
589
|
+
end: end + 1,
|
|
590
|
+
min: min,
|
|
591
|
+
max: max,
|
|
592
|
+
hasComma: hasComma,
|
|
593
|
+
})
|
|
369
594
|
})
|
|
370
595
|
}
|
|
371
596
|
|
|
597
|
+
// regexpQuantifierRepair builds the repair shared by the quantifier-shorthand
|
|
598
|
+
// rules: every `{...}` the rule accepts is rewritten by `rewrite`, and the
|
|
599
|
+
// whole set travels as one atomic fix so a literal is never half-canonicalized.
|
|
600
|
+
//
|
|
601
|
+
// `rewrite` may decline an individual quantifier whose neighbours make the
|
|
602
|
+
// rewrite unsafe; the remaining ones still apply.
|
|
603
|
+
func regexpQuantifierRepair(
|
|
604
|
+
accept func(regexpQuantifier) bool,
|
|
605
|
+
rewrite func(pattern string, quantifier regexpQuantifier) (string, bool),
|
|
606
|
+
) func(regexpLiteralParts) regexpRepair {
|
|
607
|
+
return func(parts regexpLiteralParts) regexpRepair {
|
|
608
|
+
var edits []TextEdit
|
|
609
|
+
scanRegexpQuantifiers(parts.pattern, func(quantifier regexpQuantifier) bool {
|
|
610
|
+
if !accept(quantifier) {
|
|
611
|
+
return false
|
|
612
|
+
}
|
|
613
|
+
if text, ok := rewrite(parts.pattern, quantifier); ok {
|
|
614
|
+
edits = append(edits, TextEdit{
|
|
615
|
+
Pos: parts.patternOffset() + quantifier.start,
|
|
616
|
+
End: parts.patternOffset() + quantifier.end,
|
|
617
|
+
Text: text,
|
|
618
|
+
})
|
|
619
|
+
}
|
|
620
|
+
return false
|
|
621
|
+
})
|
|
622
|
+
return regexpRepair{fix: edits}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// regexpQuantifierSymbol rewrites a count quantifier to its one-character
|
|
627
|
+
// shorthand. `{1,}` and `+` bind identically, so a trailing lazy `?` survives
|
|
628
|
+
// the swap unchanged.
|
|
629
|
+
func regexpQuantifierSymbol(symbol string) func(string, regexpQuantifier) (string, bool) {
|
|
630
|
+
return func(string, regexpQuantifier) (string, bool) { return symbol, true }
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// regexpQuantifierExactCount collapses `{n,n}` to `{n}`. The braces stay, so
|
|
634
|
+
// nothing can fuse with a neighbouring token.
|
|
635
|
+
func regexpQuantifierExactCount(_ string, quantifier regexpQuantifier) (string, bool) {
|
|
636
|
+
return "{" + strconv.Itoa(quantifier.min) + "}", true
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// regexpQuantifierDrop deletes a `{1}` that repeats its atom exactly once.
|
|
640
|
+
//
|
|
641
|
+
// Two following characters make the deletion unsafe, and neither is caught by
|
|
642
|
+
// re-parsing the result, because both rewrites still parse:
|
|
643
|
+
//
|
|
644
|
+
// - `?`, `*`, `+`, or `{`: the braces are the quantifier and the `?` in
|
|
645
|
+
// `/a{1}?/` only makes it lazy, so dropping them turns "exactly one" into
|
|
646
|
+
// "zero or one".
|
|
647
|
+
// - A digit: the braces separate a backreference or octal escape from a
|
|
648
|
+
// digit, and `/\1{1}2/` would fuse into `\12`, backreference twelve.
|
|
649
|
+
func regexpQuantifierDrop(pattern string, quantifier regexpQuantifier) (string, bool) {
|
|
650
|
+
if quantifier.end >= len(pattern) {
|
|
651
|
+
return "", true
|
|
652
|
+
}
|
|
653
|
+
switch next := pattern[quantifier.end]; {
|
|
654
|
+
case next == '?', next == '*', next == '+', next == '{':
|
|
655
|
+
return "", false
|
|
656
|
+
case next >= '0' && next <= '9':
|
|
657
|
+
return "", false
|
|
658
|
+
}
|
|
659
|
+
return "", true
|
|
660
|
+
}
|
|
661
|
+
|
|
372
662
|
func parseRegexpQuantifierNumber(text string) int {
|
|
373
663
|
if text == "" {
|
|
374
664
|
return -1
|
|
@@ -385,7 +675,20 @@ func parseRegexpQuantifierNumber(text string) int {
|
|
|
385
675
|
return value
|
|
386
676
|
}
|
|
387
677
|
|
|
388
|
-
|
|
678
|
+
// regexpClassSpan is one character class located in a pattern.
|
|
679
|
+
type regexpClassSpan struct {
|
|
680
|
+
// start and end bracket `[`..`]` inclusive-exclusive, relative to the
|
|
681
|
+
// pattern rather than to the whole literal.
|
|
682
|
+
start int
|
|
683
|
+
end int
|
|
684
|
+
content string
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// walkRegexpCharacterClassSpans visits every character class in source order,
|
|
688
|
+
// stopping early when visit returns true and reporting whether it did. A `[`
|
|
689
|
+
// that opens no class -- an escaped bracket, or one with no closing `]` -- is
|
|
690
|
+
// skipped, which is what keeps `/\[0-9]/` from being read as a digit class.
|
|
691
|
+
func walkRegexpCharacterClassSpans(pattern string, visit func(regexpClassSpan) bool) bool {
|
|
389
692
|
for i := 0; i < len(pattern); i++ {
|
|
390
693
|
if pattern[i] == '\\' {
|
|
391
694
|
i++
|
|
@@ -401,7 +704,7 @@ func walkRegexpCharacterClasses(pattern string, visit func(content string) bool)
|
|
|
401
704
|
continue
|
|
402
705
|
}
|
|
403
706
|
if pattern[j] == ']' {
|
|
404
|
-
if visit(pattern[start:j]) {
|
|
707
|
+
if visit(regexpClassSpan{start: i, end: j + 1, content: pattern[start:j]}) {
|
|
405
708
|
return true
|
|
406
709
|
}
|
|
407
710
|
i = j
|
|
@@ -412,6 +715,47 @@ func walkRegexpCharacterClasses(pattern string, visit func(content string) bool)
|
|
|
412
715
|
return false
|
|
413
716
|
}
|
|
414
717
|
|
|
718
|
+
func walkRegexpCharacterClasses(pattern string, visit func(content string) bool) bool {
|
|
719
|
+
return walkRegexpCharacterClassSpans(pattern, func(span regexpClassSpan) bool {
|
|
720
|
+
return visit(span.content)
|
|
721
|
+
})
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// regexpClassShorthandRepair rewrites every character class spelled exactly as
|
|
725
|
+
// one of `spellings` into `shorthand`.
|
|
726
|
+
//
|
|
727
|
+
// The shorthand always begins with a backslash, so it cannot fuse with the
|
|
728
|
+
// atom before it the way a bare character could, and it is a complete atom, so
|
|
729
|
+
// a following quantifier keeps applying to the same thing.
|
|
730
|
+
//
|
|
731
|
+
// The class walk is stricter than the substring test that decides the finding:
|
|
732
|
+
// it will not see a spelled-out class nested inside a `v`-mode class, so such a
|
|
733
|
+
// literal is reported without a fix rather than rewritten through a bracket
|
|
734
|
+
// that is not the class boundary.
|
|
735
|
+
func regexpClassShorthandRepair(
|
|
736
|
+
shorthand string,
|
|
737
|
+
spellings ...string,
|
|
738
|
+
) func(regexpLiteralParts) regexpRepair {
|
|
739
|
+
return func(parts regexpLiteralParts) regexpRepair {
|
|
740
|
+
var edits []TextEdit
|
|
741
|
+
walkRegexpCharacterClassSpans(parts.pattern, func(span regexpClassSpan) bool {
|
|
742
|
+
for _, spelling := range spellings {
|
|
743
|
+
if span.content != spelling {
|
|
744
|
+
continue
|
|
745
|
+
}
|
|
746
|
+
edits = append(edits, TextEdit{
|
|
747
|
+
Pos: parts.patternOffset() + span.start,
|
|
748
|
+
End: parts.patternOffset() + span.end,
|
|
749
|
+
Text: shorthand,
|
|
750
|
+
})
|
|
751
|
+
break
|
|
752
|
+
}
|
|
753
|
+
return false
|
|
754
|
+
})
|
|
755
|
+
return regexpRepair{fix: edits}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
415
759
|
func classHasRange(content string) bool {
|
|
416
760
|
for i := 1; i+1 < len(content); i++ {
|
|
417
761
|
if content[i] == '\\' {
|
|
@@ -611,16 +955,66 @@ func init() {
|
|
|
611
955
|
}})
|
|
612
956
|
Register(regexpSourceRule{name: "regexp/no-useless-character-class", check: regexpHasUselessCharacterClass})
|
|
613
957
|
Register(regexpNoUselessEscapeAlias{})
|
|
614
|
-
Register(regexpSourceRule{
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
Register(regexpSourceRule{
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
Register(regexpSourceRule{
|
|
625
|
-
|
|
958
|
+
Register(regexpSourceRule{
|
|
959
|
+
name: "regexp/no-useless-flag",
|
|
960
|
+
check: regexpHasUselessFlag,
|
|
961
|
+
repair: regexpUselessFlagRepair,
|
|
962
|
+
})
|
|
963
|
+
Register(regexpSourceRule{
|
|
964
|
+
name: "regexp/no-useless-quantifier",
|
|
965
|
+
check: regexpQuantifierCheck(regexpQuantifierIsUseless),
|
|
966
|
+
repair: regexpQuantifierRepair(regexpQuantifierIsUseless, regexpQuantifierDrop),
|
|
967
|
+
})
|
|
968
|
+
Register(regexpSourceRule{
|
|
969
|
+
name: "regexp/no-useless-two-nums-quantifier",
|
|
970
|
+
check: regexpQuantifierCheck(regexpQuantifierIsTwoNums),
|
|
971
|
+
repair: regexpQuantifierRepair(regexpQuantifierIsTwoNums, regexpQuantifierExactCount),
|
|
972
|
+
})
|
|
973
|
+
// `regexp/no-zero-quantifier` stays diagnostic-only: `{0}` says the atom
|
|
974
|
+
// never matches, so the correction is to delete the atom or repair the
|
|
975
|
+
// bound, and the rule computes neither.
|
|
976
|
+
Register(regexpSourceRule{
|
|
977
|
+
name: "regexp/no-zero-quantifier",
|
|
978
|
+
check: regexpQuantifierCheck(regexpQuantifierIsZero),
|
|
979
|
+
})
|
|
980
|
+
Register(regexpSourceRule{
|
|
981
|
+
name: "regexp/prefer-d",
|
|
982
|
+
check: regexpHasPreferD,
|
|
983
|
+
repair: regexpClassShorthandRepair("\\d", "0-9"),
|
|
984
|
+
})
|
|
985
|
+
Register(regexpSourceRule{
|
|
986
|
+
name: "regexp/prefer-plus-quantifier",
|
|
987
|
+
check: regexpQuantifierCheck(regexpQuantifierIsPlus),
|
|
988
|
+
repair: regexpQuantifierRepair(regexpQuantifierIsPlus, regexpQuantifierSymbol("+")),
|
|
989
|
+
})
|
|
990
|
+
Register(regexpSourceRule{
|
|
991
|
+
name: "regexp/prefer-question-quantifier",
|
|
992
|
+
check: regexpQuantifierCheck(regexpQuantifierIsQuestion),
|
|
993
|
+
repair: regexpQuantifierRepair(regexpQuantifierIsQuestion, regexpQuantifierSymbol("?")),
|
|
994
|
+
})
|
|
995
|
+
Register(regexpSourceRule{
|
|
996
|
+
name: "regexp/prefer-star-quantifier",
|
|
997
|
+
check: regexpQuantifierCheck(regexpQuantifierIsStar),
|
|
998
|
+
repair: regexpQuantifierRepair(regexpQuantifierIsStar, regexpQuantifierSymbol("*")),
|
|
999
|
+
})
|
|
1000
|
+
Register(regexpSourceRule{
|
|
1001
|
+
name: "regexp/prefer-w",
|
|
1002
|
+
check: regexpHasPreferW,
|
|
1003
|
+
repair: regexpClassShorthandRepair("\\w", "A-Za-z0-9_", "a-zA-Z0-9_"),
|
|
1004
|
+
})
|
|
1005
|
+
Register(regexpSourceRule{
|
|
1006
|
+
name: "regexp/require-unicode-regexp",
|
|
1007
|
+
check: regexpNeedsUnicodeFlag,
|
|
1008
|
+
repair: regexpUnicodeFlagRepair,
|
|
1009
|
+
})
|
|
1010
|
+
Register(regexpSourceRule{
|
|
1011
|
+
name: "regexp/require-unicode-sets-regexp",
|
|
1012
|
+
check: regexpNeedsUnicodeSetsFlag,
|
|
1013
|
+
repair: regexpUnicodeSetsFlagRepair,
|
|
1014
|
+
})
|
|
1015
|
+
Register(regexpSourceRule{
|
|
1016
|
+
name: "regexp/sort-flags",
|
|
1017
|
+
check: regexpFlagsUnsorted,
|
|
1018
|
+
repair: regexpSortFlagsRepair,
|
|
1019
|
+
})
|
|
626
1020
|
}
|