@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
|
@@ -12,6 +12,8 @@ import (
|
|
|
12
12
|
"unicode"
|
|
13
13
|
|
|
14
14
|
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
15
|
+
shimscanner "github.com/microsoft/typescript-go/shim/scanner"
|
|
16
|
+
publicrule "github.com/samchon/ttsc/packages/lint/rule"
|
|
15
17
|
)
|
|
16
18
|
|
|
17
19
|
type storybookAwaitInteractions struct{}
|
|
@@ -89,6 +91,14 @@ func (storybookDefaultExports) Check(ctx *Context, node *shimast.Node) {
|
|
|
89
91
|
type storybookHierarchySeparator struct{}
|
|
90
92
|
|
|
91
93
|
func (storybookHierarchySeparator) Name() string { return "storybook/hierarchy-separator" }
|
|
94
|
+
|
|
95
|
+
// DiagnosticTags strikes the separator through: `|` is Storybook's superseded
|
|
96
|
+
// hierarchy separator, and a title that uses it still renders — Storybook
|
|
97
|
+
// keeps reading it — so the finding is exactly "this still works, migrate off
|
|
98
|
+
// it", never "delete this title property".
|
|
99
|
+
func (storybookHierarchySeparator) DiagnosticTags() []publicrule.DiagnosticTag {
|
|
100
|
+
return []publicrule.DiagnosticTag{publicrule.DiagnosticTagDeprecated}
|
|
101
|
+
}
|
|
92
102
|
func (storybookHierarchySeparator) Visits() []shimast.Kind {
|
|
93
103
|
return []shimast.Kind{shimast.KindSourceFile}
|
|
94
104
|
}
|
|
@@ -101,7 +111,74 @@ func (storybookHierarchySeparator) Check(ctx *Context, node *shimast.Node) {
|
|
|
101
111
|
if !ok || !strings.Contains(storybookLiteralString(value), "|") {
|
|
102
112
|
return
|
|
103
113
|
}
|
|
104
|
-
|
|
114
|
+
const message = "Deprecated hierarchy separator in title property."
|
|
115
|
+
pos, end, located := storybookLiteralPipeRange(ctx.File, value)
|
|
116
|
+
if !located {
|
|
117
|
+
// The decoded title carries the separator, so the finding is real even when
|
|
118
|
+
// the raw spelling that produced it cannot be bounded. Report the property
|
|
119
|
+
// rather than dropping the diagnostic: a wider strikethrough is an
|
|
120
|
+
// imprecision, a missing diagnostic is a rule that stopped working.
|
|
121
|
+
ctx.Report(prop, message)
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
ctx.ReportRange(pos, end, message)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// storybookLiteralPipeRange returns the raw source range of one `|` that the
|
|
128
|
+
// parser decoded from a title literal. The tagged diagnostic must cover the
|
|
129
|
+
// deprecated separator itself, not the still-live title property around it.
|
|
130
|
+
//
|
|
131
|
+
// A pipe can be written directly or as an active `\\|`, `\\x7C`, `\\u007C`, or
|
|
132
|
+
// `\\u{7C}` escape. Every spelling below evaluates to the same separator, and
|
|
133
|
+
// reporting its complete raw escape keeps the editor range valid for each.
|
|
134
|
+
func storybookLiteralPipeRange(file *shimast.SourceFile, node *shimast.Node) (int, int, bool) {
|
|
135
|
+
pos, end := tokenRange(file, node)
|
|
136
|
+
if pos < 0 {
|
|
137
|
+
return 0, 0, false
|
|
138
|
+
}
|
|
139
|
+
src := file.Text()
|
|
140
|
+
for index := pos; index < end; {
|
|
141
|
+
if src[index] == '|' {
|
|
142
|
+
return index, index + 1, true
|
|
143
|
+
}
|
|
144
|
+
if src[index] != '\\' {
|
|
145
|
+
index++
|
|
146
|
+
continue
|
|
147
|
+
}
|
|
148
|
+
start := index
|
|
149
|
+
for index < end && src[index] == '\\' {
|
|
150
|
+
index++
|
|
151
|
+
}
|
|
152
|
+
if (index-start)%2 == 0 || index >= end {
|
|
153
|
+
continue
|
|
154
|
+
}
|
|
155
|
+
active := index - 1
|
|
156
|
+
switch src[index] {
|
|
157
|
+
case '|':
|
|
158
|
+
return active, index + 1, true
|
|
159
|
+
case 'x':
|
|
160
|
+
if index+2 < end && hexDigit(src[index+1])*16+hexDigit(src[index+2]) == int('|') {
|
|
161
|
+
return active, index + 3, true
|
|
162
|
+
}
|
|
163
|
+
case 'u':
|
|
164
|
+
if index+1 < end && src[index+1] == '{' {
|
|
165
|
+
close := index + 2
|
|
166
|
+
value := 0
|
|
167
|
+
for close < end && hexDigit(src[close]) >= 0 {
|
|
168
|
+
value = value*16 + hexDigit(src[close])
|
|
169
|
+
close++
|
|
170
|
+
}
|
|
171
|
+
if close > index+2 && close < end && src[close] == '}' && value == int('|') {
|
|
172
|
+
return active, close + 1, true
|
|
173
|
+
}
|
|
174
|
+
} else if index+4 < end &&
|
|
175
|
+
hexDigit(src[index+1])*4096+hexDigit(src[index+2])*256+hexDigit(src[index+3])*16+hexDigit(src[index+4]) == int('|') {
|
|
176
|
+
return active, index + 5, true
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
index++
|
|
180
|
+
}
|
|
181
|
+
return 0, 0, false
|
|
105
182
|
}
|
|
106
183
|
|
|
107
184
|
type storybookMetaInlineProperties struct{}
|
|
@@ -140,18 +217,34 @@ func (storybookMetaSatisfiesType) Check(ctx *Context, node *shimast.Node) {
|
|
|
140
217
|
type storybookNoRedundantStoryName struct{}
|
|
141
218
|
|
|
142
219
|
func (storybookNoRedundantStoryName) Name() string { return "storybook/no-redundant-story-name" }
|
|
220
|
+
|
|
221
|
+
// DiagnosticTags greys the redundant annotation out. Both arms of this rule
|
|
222
|
+
// report an annotation that restates the name Storybook already derives from
|
|
223
|
+
// the export identifier — the object property in one, the standalone
|
|
224
|
+
// `Story.storyName =` assignment in the other — and each is reported at
|
|
225
|
+
// exactly the range whose deletion is the whole resolution.
|
|
226
|
+
func (storybookNoRedundantStoryName) DiagnosticTags() []publicrule.DiagnosticTag {
|
|
227
|
+
return []publicrule.DiagnosticTag{publicrule.DiagnosticTagUnnecessary}
|
|
228
|
+
}
|
|
143
229
|
func (storybookNoRedundantStoryName) Visits() []shimast.Kind {
|
|
144
230
|
return []shimast.Kind{shimast.KindSourceFile}
|
|
145
231
|
}
|
|
146
232
|
func (storybookNoRedundantStoryName) Check(ctx *Context, node *shimast.Node) {
|
|
233
|
+
namedExports := map[string]bool{}
|
|
147
234
|
for _, story := range storybookNamedExports(ctx.File) {
|
|
235
|
+
namedExports[story.Name] = true
|
|
148
236
|
if story.Init == nil || story.Init.Kind != shimast.KindObjectLiteralExpression {
|
|
149
237
|
continue
|
|
150
238
|
}
|
|
151
239
|
for _, propName := range []string{"name", "storyName"} {
|
|
152
240
|
prop, value, ok := storybookObjectProperty(story.Init, propName)
|
|
153
241
|
if ok && storybookLiteralString(value) == storybookNameFromExport(story.Name) {
|
|
154
|
-
|
|
242
|
+
pos, end, removable := storybookRemovablePropertyRange(ctx.File, prop)
|
|
243
|
+
if removable {
|
|
244
|
+
ctx.ReportRange(pos, end, "Named exports should not use a redundant story name annotation.")
|
|
245
|
+
} else {
|
|
246
|
+
ctx.Report(prop, "Named exports should not use a redundant story name annotation.")
|
|
247
|
+
}
|
|
155
248
|
}
|
|
156
249
|
}
|
|
157
250
|
}
|
|
@@ -164,12 +257,47 @@ func (storybookNoRedundantStoryName) Check(ctx *Context, node *shimast.Node) {
|
|
|
164
257
|
return
|
|
165
258
|
}
|
|
166
259
|
objectName, propName := storybookPropertyAccessParts(expr.Left)
|
|
167
|
-
if
|
|
168
|
-
|
|
260
|
+
if namedExports[objectName] &&
|
|
261
|
+
propName == "storyName" &&
|
|
262
|
+
storybookLiteralString(expr.Right) == storybookNameFromExport(objectName) {
|
|
263
|
+
if child.Parent == nil ||
|
|
264
|
+
child.Parent.Kind != shimast.KindExpressionStatement ||
|
|
265
|
+
child.Parent.Parent == nil ||
|
|
266
|
+
child.Parent.Parent.Kind != shimast.KindSourceFile {
|
|
267
|
+
return
|
|
268
|
+
}
|
|
269
|
+
// The Unnecessary tag is rule-wide. Only a standalone assignment can be
|
|
270
|
+
// removed as a whole; an assignment nested in another expression still
|
|
271
|
+
// contributes its value, while one in a nested scope can refer to a
|
|
272
|
+
// same-named shadow instead of the exported story.
|
|
273
|
+
ctx.Report(child.Parent, "Named exports should not use a redundant story name annotation.")
|
|
169
274
|
}
|
|
170
275
|
})
|
|
171
276
|
}
|
|
172
277
|
|
|
278
|
+
// storybookRemovablePropertyRange returns the complete removable grain of an
|
|
279
|
+
// object property. A PropertyAssignment node stops before its trailing comma;
|
|
280
|
+
// when one is present, include it (and any trivia before it) so deleting
|
|
281
|
+
// exactly the faded `Unnecessary` range leaves valid object syntax.
|
|
282
|
+
//
|
|
283
|
+
// A comment sitting between the property and its comma is therefore faded with
|
|
284
|
+
// them. That is deliberate, not an oversight: the alternative — stopping at the
|
|
285
|
+
// comment and leaving the comma behind — produces `{ /* note */, }`, which does
|
|
286
|
+
// not parse. Only one of the two can hold, and a range whose deletion always
|
|
287
|
+
// leaves valid syntax is the contract the Unnecessary tag makes.
|
|
288
|
+
func storybookRemovablePropertyRange(file *shimast.SourceFile, property *shimast.Node) (int, int, bool) {
|
|
289
|
+
pos, end := tokenRange(file, property)
|
|
290
|
+
if pos < 0 {
|
|
291
|
+
return 0, 0, false
|
|
292
|
+
}
|
|
293
|
+
source := file.Text()
|
|
294
|
+
comma := shimscanner.SkipTrivia(source, end)
|
|
295
|
+
if comma < len(source) && source[comma] == ',' {
|
|
296
|
+
end = comma + 1
|
|
297
|
+
}
|
|
298
|
+
return pos, end, true
|
|
299
|
+
}
|
|
300
|
+
|
|
173
301
|
type storybookNoRendererPackages struct{}
|
|
174
302
|
|
|
175
303
|
func (storybookNoRendererPackages) Name() string { return "storybook/no-renderer-packages" }
|
|
@@ -181,27 +309,106 @@ func (storybookNoRendererPackages) Check(ctx *Context, node *shimast.Node) {
|
|
|
181
309
|
if decl == nil {
|
|
182
310
|
return
|
|
183
311
|
}
|
|
184
|
-
|
|
185
|
-
|
|
312
|
+
frameworks, ok := storybookRendererPackages[storybookLiteralString(decl.ModuleSpecifier)]
|
|
313
|
+
if !ok || len(frameworks) == 0 {
|
|
314
|
+
return
|
|
315
|
+
}
|
|
316
|
+
message := "Do not import Storybook renderer packages directly. Use a framework package instead: " +
|
|
317
|
+
storybookDisjunction(frameworks) + "."
|
|
318
|
+
pos, end, quoted := storybookQuotedTextRange(ctx.File, decl.ModuleSpecifier)
|
|
319
|
+
if !quoted {
|
|
320
|
+
ctx.Report(node, message)
|
|
321
|
+
return
|
|
322
|
+
}
|
|
323
|
+
suggestions := make([]Suggestion, 0, len(frameworks))
|
|
324
|
+
for _, framework := range frameworks {
|
|
325
|
+
suggestions = append(suggestions, Suggestion{
|
|
326
|
+
Title: "Import from `" + framework + "`.",
|
|
327
|
+
Edits: []TextEdit{{Pos: pos, End: end, Text: framework}},
|
|
328
|
+
})
|
|
329
|
+
}
|
|
330
|
+
ctx.ReportFixSuggestions(node, message, nil, suggestions...)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// storybookDisjunction renders a package list the way upstream's
|
|
334
|
+
// `Intl.ListFormat("en-US", {type: "disjunction"})` does: "a", "a or b",
|
|
335
|
+
// "a, b, or c".
|
|
336
|
+
func storybookDisjunction(items []string) string {
|
|
337
|
+
quoted := make([]string, 0, len(items))
|
|
338
|
+
for _, item := range items {
|
|
339
|
+
quoted = append(quoted, "`"+item+"`")
|
|
340
|
+
}
|
|
341
|
+
switch len(quoted) {
|
|
342
|
+
case 0:
|
|
343
|
+
return ""
|
|
344
|
+
case 1:
|
|
345
|
+
return quoted[0]
|
|
346
|
+
case 2:
|
|
347
|
+
return quoted[0] + " or " + quoted[1]
|
|
348
|
+
default:
|
|
349
|
+
return strings.Join(quoted[:len(quoted)-1], ", ") + ", or " + quoted[len(quoted)-1]
|
|
186
350
|
}
|
|
187
351
|
}
|
|
188
352
|
|
|
353
|
+
// storybookQuotedTextRange returns the byte range of the text inside a string
|
|
354
|
+
// literal's quotes, so a rewrite replaces the contents and leaves the
|
|
355
|
+
// surrounding quote characters exactly as the author wrote them. Reports false
|
|
356
|
+
// for anything that is not a plainly quoted literal, such as a
|
|
357
|
+
// parse-recovered node with no closing quote.
|
|
358
|
+
func storybookQuotedTextRange(file *shimast.SourceFile, node *shimast.Node) (int, int, bool) {
|
|
359
|
+
pos, end := tokenRange(file, node)
|
|
360
|
+
if pos < 0 || end-pos < 2 {
|
|
361
|
+
return 0, 0, false
|
|
362
|
+
}
|
|
363
|
+
src := file.Text()
|
|
364
|
+
quote := src[pos]
|
|
365
|
+
if quote != '"' && quote != '\'' {
|
|
366
|
+
return 0, 0, false
|
|
367
|
+
}
|
|
368
|
+
if src[end-1] != quote {
|
|
369
|
+
return 0, 0, false
|
|
370
|
+
}
|
|
371
|
+
return pos + 1, end - 1, true
|
|
372
|
+
}
|
|
373
|
+
|
|
189
374
|
type storybookNoStoriesOf struct{}
|
|
190
375
|
|
|
191
376
|
func (storybookNoStoriesOf) Name() string { return "storybook/no-stories-of" }
|
|
377
|
+
|
|
378
|
+
// DiagnosticTags strikes the `storiesOf` import specifier through. The builder
|
|
379
|
+
// API is deprecated in favor of CSF but still runs, and the whole reported
|
|
380
|
+
// range is the specifier that names it, so migration — not deletion — is the
|
|
381
|
+
// instruction the editor should render.
|
|
382
|
+
func (storybookNoStoriesOf) DiagnosticTags() []publicrule.DiagnosticTag {
|
|
383
|
+
return []publicrule.DiagnosticTag{publicrule.DiagnosticTagDeprecated}
|
|
384
|
+
}
|
|
192
385
|
func (storybookNoStoriesOf) Visits() []shimast.Kind {
|
|
193
386
|
return []shimast.Kind{shimast.KindImportSpecifier}
|
|
194
387
|
}
|
|
195
388
|
func (storybookNoStoriesOf) Check(ctx *Context, node *shimast.Node) {
|
|
196
389
|
spec := node.AsImportSpecifier()
|
|
197
|
-
if spec != nil &&
|
|
198
|
-
|
|
390
|
+
if spec != nil &&
|
|
391
|
+
storybookImportSpecifierImportedName(spec) == "storiesOf" &&
|
|
392
|
+
isStorybookStoriesOfModule(storybookImportSpecifierModule(node)) {
|
|
393
|
+
reported := spec.Name()
|
|
394
|
+
if spec.PropertyName != nil {
|
|
395
|
+
reported = spec.PropertyName
|
|
396
|
+
}
|
|
397
|
+
ctx.Report(reported, "storiesOf is deprecated and should not be used.")
|
|
199
398
|
}
|
|
200
399
|
}
|
|
201
400
|
|
|
202
401
|
type storybookNoTitlePropertyInMeta struct{}
|
|
203
402
|
|
|
204
403
|
func (storybookNoTitlePropertyInMeta) Name() string { return "storybook/no-title-property-in-meta" }
|
|
404
|
+
|
|
405
|
+
// DiagnosticTags greys the `title` property out. CSF3 derives a story's title
|
|
406
|
+
// from the file's location on disk, so the property is dead weight rather than
|
|
407
|
+
// a wrong value, and the reported range is the property itself: removing
|
|
408
|
+
// exactly what the editor fades is the resolution.
|
|
409
|
+
func (storybookNoTitlePropertyInMeta) DiagnosticTags() []publicrule.DiagnosticTag {
|
|
410
|
+
return []publicrule.DiagnosticTag{publicrule.DiagnosticTagUnnecessary}
|
|
411
|
+
}
|
|
205
412
|
func (storybookNoTitlePropertyInMeta) Visits() []shimast.Kind {
|
|
206
413
|
return []shimast.Kind{shimast.KindSourceFile}
|
|
207
414
|
}
|
|
@@ -211,7 +418,12 @@ func (storybookNoTitlePropertyInMeta) Check(ctx *Context, node *shimast.Node) {
|
|
|
211
418
|
return
|
|
212
419
|
}
|
|
213
420
|
if prop, _, ok := storybookObjectProperty(meta.Object, "title"); ok {
|
|
214
|
-
|
|
421
|
+
pos, end, removable := storybookRemovablePropertyRange(ctx.File, prop)
|
|
422
|
+
if removable {
|
|
423
|
+
ctx.ReportRange(pos, end, "CSF3 does not need a title in meta.")
|
|
424
|
+
} else {
|
|
425
|
+
ctx.Report(prop, "CSF3 does not need a title in meta.")
|
|
426
|
+
}
|
|
215
427
|
}
|
|
216
428
|
}
|
|
217
429
|
|
|
@@ -370,6 +582,16 @@ var storybookRendererPackages = map[string][]string{
|
|
|
370
582
|
"@storybook/web-components": {"@storybook/web-components-vite", "@storybook/web-components-webpack5"},
|
|
371
583
|
}
|
|
372
584
|
|
|
585
|
+
var storybookStoriesOfModules = []string{
|
|
586
|
+
"@storybook/react",
|
|
587
|
+
"@storybook/vue",
|
|
588
|
+
"@storybook/vue3",
|
|
589
|
+
"@storybook/angular",
|
|
590
|
+
"@storybook/svelte",
|
|
591
|
+
"@storybook/html",
|
|
592
|
+
"@storybook/web-components",
|
|
593
|
+
}
|
|
594
|
+
|
|
373
595
|
func storybookDefaultMeta(file *shimast.SourceFile) *storybookMetaInfo {
|
|
374
596
|
if file == nil || file.Statements == nil {
|
|
375
597
|
return nil
|
|
@@ -756,7 +978,30 @@ func storybookDescriptorsMatch(descriptors []storybookDescriptor, name string) b
|
|
|
756
978
|
}
|
|
757
979
|
|
|
758
980
|
func storybookHasStoriesOfImport(file *shimast.SourceFile) bool {
|
|
759
|
-
return storybookHasNamedImport(file, "storiesOf",
|
|
981
|
+
return storybookHasNamedImport(file, "storiesOf", storybookStoriesOfModules...)
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
func isStorybookStoriesOfModule(module string) bool {
|
|
985
|
+
for _, candidate := range storybookStoriesOfModules {
|
|
986
|
+
if module == candidate {
|
|
987
|
+
return true
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
return false
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
func storybookImportSpecifierModule(node *shimast.Node) string {
|
|
994
|
+
for current := node; current != nil; current = current.Parent {
|
|
995
|
+
if current.Kind != shimast.KindImportDeclaration {
|
|
996
|
+
continue
|
|
997
|
+
}
|
|
998
|
+
decl := current.AsImportDeclaration()
|
|
999
|
+
if decl == nil {
|
|
1000
|
+
return ""
|
|
1001
|
+
}
|
|
1002
|
+
return storybookLiteralString(decl.ModuleSpecifier)
|
|
1003
|
+
}
|
|
1004
|
+
return ""
|
|
760
1005
|
}
|
|
761
1006
|
|
|
762
1007
|
func storybookHasNamedImport(file *shimast.SourceFile, name string, modules ...string) bool {
|
|
@@ -185,11 +185,20 @@ func (noExtraBind) Check(ctx *Context, node *shimast.Node) {
|
|
|
185
185
|
return
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
ctx.
|
|
188
|
+
message := "The function binding is unnecessary."
|
|
189
|
+
edits, imposable := noExtraBindFixEdits(ctx, node, call, member, receiver)
|
|
190
|
+
switch {
|
|
191
|
+
case len(edits) == 0:
|
|
192
|
+
ctx.Report(node, message)
|
|
193
|
+
case imposable:
|
|
194
|
+
ctx.ReportFix(node, message, edits...)
|
|
195
|
+
default:
|
|
196
|
+
ctx.ReportSuggestion(
|
|
197
|
+
node,
|
|
198
|
+
message,
|
|
199
|
+
"Remove the unnecessary binding, discarding the comment inside it.",
|
|
200
|
+
edits...,
|
|
201
|
+
)
|
|
193
202
|
}
|
|
194
203
|
}
|
|
195
204
|
|
|
@@ -296,22 +305,28 @@ func noExtraBindThisBelongsToFunction(node, root *shimast.Node) bool {
|
|
|
296
305
|
|
|
297
306
|
// noExtraBindFixEdits removes the member access and its one-argument call as
|
|
298
307
|
// separate ranges. Parentheses and comments before the member operator remain
|
|
299
|
-
// byte-for-byte intact.
|
|
300
|
-
//
|
|
308
|
+
// byte-for-byte intact.
|
|
309
|
+
//
|
|
310
|
+
// No edit exists at all when the removal would change what the program does —
|
|
311
|
+
// evaluating the receiver may have effects — or when the ranges are degenerate;
|
|
312
|
+
// both return `(nil, false)`. When the edit is well-formed but a comment lies
|
|
313
|
+
// inside the discarded syntax, it returns `(edits, false)`: imposing silent
|
|
314
|
+
// comment loss is unacceptable, yet the caller can still offer the removal as
|
|
315
|
+
// an opt-in suggestion. Only `(edits, true)` may be applied automatically.
|
|
301
316
|
func noExtraBindFixEdits(
|
|
302
317
|
ctx *Context,
|
|
303
318
|
callNode *shimast.Node,
|
|
304
319
|
call *shimast.CallExpression,
|
|
305
320
|
member *shimast.Node,
|
|
306
321
|
receiver *shimast.Node,
|
|
307
|
-
) []TextEdit {
|
|
322
|
+
) (edits []TextEdit, imposable bool) {
|
|
308
323
|
if ctx == nil || ctx.File == nil || callNode == nil || call == nil || call.Expression == nil || member == nil ||
|
|
309
324
|
!noExtraBindReceiverIsSideEffectFree(receiver) {
|
|
310
|
-
return nil
|
|
325
|
+
return nil, false
|
|
311
326
|
}
|
|
312
327
|
parts, ok := referenceMemberParts(member)
|
|
313
328
|
if !ok || parts.object == nil {
|
|
314
|
-
return nil
|
|
329
|
+
return nil, false
|
|
315
330
|
}
|
|
316
331
|
|
|
317
332
|
src := ctx.File.Text()
|
|
@@ -320,15 +335,12 @@ func noExtraBindFixEdits(
|
|
|
320
335
|
callStart := shimscanner.SkipTrivia(src, call.Expression.End())
|
|
321
336
|
callEnd := callNode.End()
|
|
322
337
|
if memberStart < 0 || memberStart >= memberEnd || memberEnd > callStart || callStart >= callEnd || callEnd > len(src) {
|
|
323
|
-
return nil
|
|
324
|
-
}
|
|
325
|
-
if hasCommentBetween(src, memberStart, callEnd) {
|
|
326
|
-
return nil
|
|
338
|
+
return nil, false
|
|
327
339
|
}
|
|
328
340
|
return []TextEdit{
|
|
329
341
|
{Pos: memberStart, End: memberEnd, Text: ""},
|
|
330
342
|
{Pos: callStart, End: callEnd, Text: ""},
|
|
331
|
-
}
|
|
343
|
+
}, !hasCommentBetween(src, memberStart, callEnd)
|
|
332
344
|
}
|
|
333
345
|
|
|
334
346
|
func noExtraBindReceiverIsSideEffectFree(node *shimast.Node) bool {
|
|
@@ -1312,19 +1324,22 @@ func reportUselessRenameFix(ctx *Context, node, propertyName, name *shimast.Node
|
|
|
1312
1324
|
ctx.Report(node, message)
|
|
1313
1325
|
return
|
|
1314
1326
|
}
|
|
1327
|
+
edit := TextEdit{Pos: propertyName.End(), End: name.End(), Text: ""}
|
|
1315
1328
|
// A comment between the two names (`{ a as /* c */ a }`) sits inside the
|
|
1316
|
-
// deleted rename tail;
|
|
1317
|
-
//
|
|
1318
|
-
//
|
|
1329
|
+
// deleted rename tail; imposing that loss is unacceptable, so the autofix
|
|
1330
|
+
// declines. Mirrors ESLint no-useless-rename's `commentsExistBetween` guard.
|
|
1331
|
+
// The collapse is still what the author wants, so the same edit is offered
|
|
1332
|
+
// as an opt-in suggestion whose title states that the comment goes with it.
|
|
1319
1333
|
if hasCommentBetween(ctx.File.Text(), propertyName.End(), name.End()) {
|
|
1320
|
-
ctx.
|
|
1334
|
+
ctx.ReportSuggestion(
|
|
1335
|
+
node,
|
|
1336
|
+
message,
|
|
1337
|
+
"Remove the redundant rename, discarding the comment inside it.",
|
|
1338
|
+
edit,
|
|
1339
|
+
)
|
|
1321
1340
|
return
|
|
1322
1341
|
}
|
|
1323
|
-
ctx.ReportFix(
|
|
1324
|
-
node,
|
|
1325
|
-
message,
|
|
1326
|
-
TextEdit{Pos: propertyName.End(), End: name.End(), Text: ""},
|
|
1327
|
-
)
|
|
1342
|
+
ctx.ReportFix(node, message, edit)
|
|
1328
1343
|
}
|
|
1329
1344
|
|
|
1330
1345
|
// objectShorthand: `{ x: x }` → `{ x }`.
|
|
@@ -1351,17 +1366,20 @@ func (objectShorthand) Check(ctx *Context, node *shimast.Node) {
|
|
|
1351
1366
|
// that range.
|
|
1352
1367
|
//
|
|
1353
1368
|
// A comment inside that range (`{ x: /* c */ x }`) would be dropped by the
|
|
1354
|
-
// deletion, so
|
|
1355
|
-
//
|
|
1369
|
+
// deletion, so the autofix declines. Mirrors ESLint object-shorthand's
|
|
1370
|
+
// `commentsExistBetween` guard. The shorthand rewrite is still correct, so
|
|
1371
|
+
// the same edit is offered as an opt-in suggestion that names the loss.
|
|
1372
|
+
edit := TextEdit{Pos: prop.Name().End(), End: prop.Initializer.End(), Text: ""}
|
|
1356
1373
|
if hasCommentBetween(ctx.File.Text(), prop.Name().End(), prop.Initializer.End()) {
|
|
1357
|
-
ctx.
|
|
1374
|
+
ctx.ReportSuggestion(
|
|
1375
|
+
node,
|
|
1376
|
+
"Expected property shorthand.",
|
|
1377
|
+
"Use property shorthand, discarding the comment before the value.",
|
|
1378
|
+
edit,
|
|
1379
|
+
)
|
|
1358
1380
|
return
|
|
1359
1381
|
}
|
|
1360
|
-
ctx.ReportFix(
|
|
1361
|
-
node,
|
|
1362
|
-
"Expected property shorthand.",
|
|
1363
|
-
TextEdit{Pos: prop.Name().End(), End: prop.Initializer.End(), Text: ""},
|
|
1364
|
-
)
|
|
1382
|
+
ctx.ReportFix(node, "Expected property shorthand.", edit)
|
|
1365
1383
|
}
|
|
1366
1384
|
|
|
1367
1385
|
// operatorAssignment: `x = x + 1` → `x += 1`.
|
|
@@ -1494,21 +1512,24 @@ func (preferTemplate) Check(ctx *Context, node *shimast.Node) {
|
|
|
1494
1512
|
ctx.Report(node, message)
|
|
1495
1513
|
return
|
|
1496
1514
|
}
|
|
1515
|
+
edit := TextEdit{Pos: editPos, End: node.End(), Text: template}
|
|
1497
1516
|
// A comment in an operator seam (`"a" + /* c */ b`) is dropped by the
|
|
1498
|
-
// template rebuild, so
|
|
1517
|
+
// template rebuild, so the autofix declines and the rendered literal is
|
|
1518
|
+
// offered as an opt-in suggestion that names the loss. Mirrors ESLint
|
|
1499
1519
|
// prefer-template's `commentsExistBetween` guard. Only the seams are scanned:
|
|
1500
1520
|
// operand interiors are copied verbatim (their comments survive) and string
|
|
1501
1521
|
// contents are cooked, so scanning the whole span would misread a `//` or
|
|
1502
1522
|
// `/*` inside a string literal (`"https://" + host`) as a comment.
|
|
1503
1523
|
if concatOperandSeamsHaveComment(src, operands) {
|
|
1504
|
-
ctx.
|
|
1524
|
+
ctx.ReportSuggestion(
|
|
1525
|
+
node,
|
|
1526
|
+
message,
|
|
1527
|
+
"Use a template literal, discarding the comments between the operands.",
|
|
1528
|
+
edit,
|
|
1529
|
+
)
|
|
1505
1530
|
return
|
|
1506
1531
|
}
|
|
1507
|
-
ctx.ReportFix(
|
|
1508
|
-
node,
|
|
1509
|
-
message,
|
|
1510
|
-
TextEdit{Pos: editPos, End: node.End(), Text: template},
|
|
1511
|
-
)
|
|
1532
|
+
ctx.ReportFix(node, message, edit)
|
|
1512
1533
|
}
|
|
1513
1534
|
|
|
1514
1535
|
// concatOperandSeamsHaveComment reports whether a comment sits in any seam
|
package/linthost/rules_ts.go
CHANGED
|
@@ -147,6 +147,13 @@ func (noNamespace) Check(ctx *Context, node *shimast.Node) {
|
|
|
147
147
|
if decl.Name().Kind == shimast.KindStringLiteral {
|
|
148
148
|
return
|
|
149
149
|
}
|
|
150
|
+
// Skip `declare global { … }`. It is spelled with the `global` keyword
|
|
151
|
+
// rather than `namespace`/`module`, and it is the only way to augment a
|
|
152
|
+
// global interface — ES module syntax has no equivalent, so the advice this
|
|
153
|
+
// rule gives cannot be followed. Upstream exempts it for the same reason.
|
|
154
|
+
if decl.Keyword == shimast.KindGlobalKeyword {
|
|
155
|
+
return
|
|
156
|
+
}
|
|
150
157
|
ctx.Report(node, "ES2015 module syntax is preferred over namespaces.")
|
|
151
158
|
}
|
|
152
159
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
//
|
|
7
7
|
// Implemented here:
|
|
8
8
|
// - typescript/require-await
|
|
9
|
-
// (
|
|
9
|
+
// (type-aware; async function with no `await` that also returns no promise)
|
|
10
10
|
// - typescript/use-unknown-in-catch-callback-variable
|
|
11
11
|
// (type-aware; `.catch(err)` / `.then(_, err)` must annotate `unknown`)
|
|
12
12
|
// - typescript/only-throw-error
|
|
@@ -24,13 +24,20 @@ import (
|
|
|
24
24
|
// refactor artifact. typescript-eslint:
|
|
25
25
|
// https://typescript-eslint.io/rules/require-await/
|
|
26
26
|
//
|
|
27
|
-
//
|
|
27
|
+
// The walker stops at nested function-like boundaries so an
|
|
28
28
|
// `await` inside an inner non-async closure does not count toward the
|
|
29
29
|
// outer function. Async generators are exempt — they satisfy the
|
|
30
30
|
// async-keyword contract through `yield`.
|
|
31
31
|
type requireAwait struct{}
|
|
32
32
|
|
|
33
33
|
func (requireAwait) Name() string { return "typescript/require-await" }
|
|
34
|
+
|
|
35
|
+
// NeedsTypeChecker: upstream states the rule as "async functions which do not
|
|
36
|
+
// return promises and have no await expression", and that it "uses type
|
|
37
|
+
// information to allow promise-returning functions to be marked as async
|
|
38
|
+
// without containing an await expression". Whether a returned expression is
|
|
39
|
+
// thenable is not visible in the syntax.
|
|
40
|
+
func (requireAwait) NeedsTypeChecker() bool { return true }
|
|
34
41
|
func (requireAwait) Visits() []shimast.Kind {
|
|
35
42
|
return []shimast.Kind{
|
|
36
43
|
shimast.KindFunctionDeclaration,
|
|
@@ -53,6 +60,9 @@ func (requireAwait) Check(ctx *Context, node *shimast.Node) {
|
|
|
53
60
|
if requireAwaitBodyHasAwait(body) {
|
|
54
61
|
return
|
|
55
62
|
}
|
|
63
|
+
if requireAwaitReturnsThenable(ctx, body) {
|
|
64
|
+
return
|
|
65
|
+
}
|
|
56
66
|
startPos := keywordStart(ctx.File, node, "async")
|
|
57
67
|
if startPos < 0 {
|
|
58
68
|
ctx.Report(node, "Async function has no `await` expression.")
|
|
@@ -99,6 +109,16 @@ func requireAwaitBodyHasAwait(body *shimast.Node) bool {
|
|
|
99
109
|
found = true
|
|
100
110
|
return
|
|
101
111
|
}
|
|
112
|
+
// `for await (const x of source)` awaits every step of the iteration. It
|
|
113
|
+
// is spelled as a for-of carrying an await modifier rather than as an
|
|
114
|
+
// AwaitExpression, so a walker that looks only for the expression form
|
|
115
|
+
// reports a function whose sole suspend point is the loop.
|
|
116
|
+
if n.Kind == shimast.KindForOfStatement {
|
|
117
|
+
if stmt := n.AsForInOrOfStatement(); stmt != nil && stmt.AwaitModifier != nil {
|
|
118
|
+
found = true
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
}
|
|
102
122
|
n.ForEachChild(func(child *shimast.Node) bool {
|
|
103
123
|
walk(child)
|
|
104
124
|
return false
|
|
@@ -266,3 +286,61 @@ func init() {
|
|
|
266
286
|
Register(useUnknownInCatchCallbackVariable{})
|
|
267
287
|
Register(onlyThrowError{})
|
|
268
288
|
}
|
|
289
|
+
|
|
290
|
+
// requireAwaitReturnsThenable reports whether the function body hands a
|
|
291
|
+
// thenable back to its caller.
|
|
292
|
+
//
|
|
293
|
+
// Upstream exempts these deliberately: `async function f() { return g() }`
|
|
294
|
+
// marks the async contract for callers and forwards the inner promise, which
|
|
295
|
+
// is a use of `async` rather than a leftover from a refactor. The walk stops at
|
|
296
|
+
// nested function-like scopes for the same reason the await walk does — an
|
|
297
|
+
// inner closure's `return` belongs to that closure.
|
|
298
|
+
//
|
|
299
|
+
// A concise arrow body (`async () => g()`) is the returned expression itself
|
|
300
|
+
// rather than a block, and is classified directly.
|
|
301
|
+
func requireAwaitReturnsThenable(ctx *Context, body *shimast.Node) bool {
|
|
302
|
+
if ctx == nil || ctx.Checker == nil || body == nil {
|
|
303
|
+
return false
|
|
304
|
+
}
|
|
305
|
+
if body.Kind != shimast.KindBlock {
|
|
306
|
+
return requireAwaitExpressionIsThenable(ctx, body)
|
|
307
|
+
}
|
|
308
|
+
thenable := false
|
|
309
|
+
var walk func(*shimast.Node)
|
|
310
|
+
walk = func(n *shimast.Node) {
|
|
311
|
+
if thenable || n == nil {
|
|
312
|
+
return
|
|
313
|
+
}
|
|
314
|
+
if n != body && isFunctionLikeKind(n) {
|
|
315
|
+
return
|
|
316
|
+
}
|
|
317
|
+
if n.Kind == shimast.KindReturnStatement {
|
|
318
|
+
if stmt := n.AsReturnStatement(); stmt != nil && requireAwaitExpressionIsThenable(ctx, stmt.Expression) {
|
|
319
|
+
thenable = true
|
|
320
|
+
return
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
n.ForEachChild(func(child *shimast.Node) bool {
|
|
324
|
+
walk(child)
|
|
325
|
+
return false
|
|
326
|
+
})
|
|
327
|
+
}
|
|
328
|
+
walk(body)
|
|
329
|
+
return thenable
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// requireAwaitExpressionIsThenable classifies one returned expression with the
|
|
333
|
+
// same awaitability test `await-thenable` uses, so the two rules agree on what
|
|
334
|
+
// counts as a promise. Anything the classifier does not rule out entirely is
|
|
335
|
+
// treated as thenable: the exemption must not depend on proving a union is
|
|
336
|
+
// thenable in every branch.
|
|
337
|
+
func requireAwaitExpressionIsThenable(ctx *Context, expr *shimast.Node) bool {
|
|
338
|
+
if expr == nil {
|
|
339
|
+
return false
|
|
340
|
+
}
|
|
341
|
+
t := ctx.Checker.GetTypeAtLocation(expr)
|
|
342
|
+
if t == nil {
|
|
343
|
+
return false
|
|
344
|
+
}
|
|
345
|
+
return classifyPromiseAwaitability(ctx.Checker, expr, t) != promiseAwaitabilityNever
|
|
346
|
+
}
|