@ttsc/lint 0.19.3 → 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_solid.go
CHANGED
|
@@ -4,6 +4,8 @@ import (
|
|
|
4
4
|
"strings"
|
|
5
5
|
|
|
6
6
|
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
7
|
+
shimscanner "github.com/microsoft/typescript-go/shim/scanner"
|
|
8
|
+
publicrule "github.com/samchon/ttsc/packages/lint/rule"
|
|
7
9
|
)
|
|
8
10
|
|
|
9
11
|
type solidRule struct {
|
|
@@ -11,6 +13,29 @@ type solidRule struct {
|
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
func (r solidRule) Name() string { return "solid/" + r.name }
|
|
16
|
+
|
|
17
|
+
// DiagnosticTags marks `solid/no-react-deps` findings as unnecessary code so an
|
|
18
|
+
// editor greys the dependency array out.
|
|
19
|
+
//
|
|
20
|
+
// The rule reports the dependency-array literal and nothing around it, and
|
|
21
|
+
// Solid tracks dependencies automatically, so the array is inert: deleting
|
|
22
|
+
// exactly the reported range is the whole resolution, which is what greying
|
|
23
|
+
// tells the author to do.
|
|
24
|
+
//
|
|
25
|
+
// The marker is rule-level, so a rule whose findings do not all mean "delete
|
|
26
|
+
// this" cannot take it. `solid/no-react-specific-props` is the near miss — its
|
|
27
|
+
// `key` arm does mean deletion, but its `className` and `htmlFor` arms mean
|
|
28
|
+
// "rename this", and one tag cannot say both.
|
|
29
|
+
func (r solidRule) DiagnosticTags() []publicrule.DiagnosticTag {
|
|
30
|
+
if r.name == "no-react-deps" {
|
|
31
|
+
return []publicrule.DiagnosticTag{publicrule.DiagnosticTagUnnecessary}
|
|
32
|
+
}
|
|
33
|
+
return nil
|
|
34
|
+
}
|
|
35
|
+
func (r solidRule) NeedsTypeChecker() bool {
|
|
36
|
+
return r.name == "no-react-deps"
|
|
37
|
+
}
|
|
38
|
+
|
|
14
39
|
func (r solidRule) Visits() []shimast.Kind {
|
|
15
40
|
return []shimast.Kind{shimast.KindSourceFile}
|
|
16
41
|
}
|
|
@@ -80,6 +105,7 @@ type solidState struct {
|
|
|
80
105
|
declared map[string]bool
|
|
81
106
|
importedFrom map[string]string
|
|
82
107
|
solidImport map[string]string
|
|
108
|
+
solidSymbols map[string]*shimast.Symbol
|
|
83
109
|
signals map[string]bool
|
|
84
110
|
}
|
|
85
111
|
|
|
@@ -88,6 +114,7 @@ func collectSolidState(ctx *Context) *solidState {
|
|
|
88
114
|
declared: map[string]bool{},
|
|
89
115
|
importedFrom: map[string]string{},
|
|
90
116
|
solidImport: map[string]string{},
|
|
117
|
+
solidSymbols: map[string]*shimast.Symbol{},
|
|
91
118
|
signals: map[string]bool{},
|
|
92
119
|
}
|
|
93
120
|
if ctx == nil || ctx.File == nil {
|
|
@@ -98,7 +125,7 @@ func collectSolidState(ctx *Context) *solidState {
|
|
|
98
125
|
continue
|
|
99
126
|
}
|
|
100
127
|
state.imports = append(state.imports, stmt)
|
|
101
|
-
state.collectImport(stmt)
|
|
128
|
+
state.collectImport(ctx, stmt)
|
|
102
129
|
}
|
|
103
130
|
walkDescendants(ctx.File.AsNode(), func(child *shimast.Node) {
|
|
104
131
|
if child == nil {
|
|
@@ -132,13 +159,13 @@ func collectSolidState(ctx *Context) *solidState {
|
|
|
132
159
|
return state
|
|
133
160
|
}
|
|
134
161
|
|
|
135
|
-
func (s *solidState) collectImport(node *shimast.Node) {
|
|
162
|
+
func (s *solidState) collectImport(ctx *Context, node *shimast.Node) {
|
|
136
163
|
decl := node.AsImportDeclaration()
|
|
137
164
|
if decl == nil {
|
|
138
165
|
return
|
|
139
166
|
}
|
|
140
167
|
source := stringLiteralText(decl.ModuleSpecifier)
|
|
141
|
-
if
|
|
168
|
+
if isSolidModuleSource(source) {
|
|
142
169
|
s.hasSolid = true
|
|
143
170
|
}
|
|
144
171
|
if decl.ImportClause == nil {
|
|
@@ -151,9 +178,30 @@ func (s *solidState) collectImport(node *shimast.Node) {
|
|
|
151
178
|
if name := identifierText(clause.Name()); name != "" {
|
|
152
179
|
s.declared[name] = true
|
|
153
180
|
s.importedFrom[name] = source
|
|
181
|
+
if isSolidModuleSource(source) {
|
|
182
|
+
s.solidSymbols[name] = canonicalValueSymbol(ctx, clause.Name())
|
|
183
|
+
}
|
|
154
184
|
}
|
|
155
185
|
bindings := clause.NamedBindings
|
|
156
|
-
if bindings == nil
|
|
186
|
+
if bindings == nil {
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
if bindings.Kind == shimast.KindNamespaceImport {
|
|
190
|
+
namespace := bindings.AsNamespaceImport()
|
|
191
|
+
if namespace == nil {
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
local := identifierText(namespace.Name())
|
|
195
|
+
if local != "" {
|
|
196
|
+
s.declared[local] = true
|
|
197
|
+
s.importedFrom[local] = source
|
|
198
|
+
if isSolidModuleSource(source) {
|
|
199
|
+
s.solidSymbols[local] = canonicalValueSymbol(ctx, namespace.Name())
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
if bindings.Kind != shimast.KindNamedImports {
|
|
157
205
|
return
|
|
158
206
|
}
|
|
159
207
|
named := bindings.AsNamedImports()
|
|
@@ -175,8 +223,9 @@ func (s *solidState) collectImport(node *shimast.Node) {
|
|
|
175
223
|
}
|
|
176
224
|
s.declared[local] = true
|
|
177
225
|
s.importedFrom[local] = source
|
|
178
|
-
if
|
|
226
|
+
if isSolidModuleSource(source) {
|
|
179
227
|
s.solidImport[local] = imported
|
|
228
|
+
s.solidSymbols[local] = canonicalValueSymbol(ctx, spec.Name())
|
|
180
229
|
}
|
|
181
230
|
}
|
|
182
231
|
}
|
|
@@ -255,11 +304,309 @@ func (s *solidState) reportImports(ctx *Context) {
|
|
|
255
304
|
imported = identifierText(spec.Name())
|
|
256
305
|
}
|
|
257
306
|
}
|
|
258
|
-
|
|
259
|
-
|
|
307
|
+
correct := solidPreferredSource(imported)
|
|
308
|
+
if correct == "" || correct == source {
|
|
309
|
+
continue
|
|
260
310
|
}
|
|
311
|
+
message := "Import `" + imported + "` from `" + correct + "`."
|
|
312
|
+
edits := solidImportSourceEdits(
|
|
313
|
+
ctx.File,
|
|
314
|
+
clause,
|
|
315
|
+
named,
|
|
316
|
+
decl.ModuleSpecifier,
|
|
317
|
+
correct,
|
|
318
|
+
specNode,
|
|
319
|
+
s.solidNamedImportFrom(correct, clause.IsTypeOnly()),
|
|
320
|
+
)
|
|
321
|
+
ctx.ReportFix(specNode, message, edits...)
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// solidNamedImportFrom returns the file's named-import list for `source`, or
|
|
327
|
+
// nil when the file has none. It is how the fix finds a declaration to move a
|
|
328
|
+
// misrouted specifier INTO, which is what upstream's `appendImports` does and
|
|
329
|
+
// what the rule's published description means by merging.
|
|
330
|
+
//
|
|
331
|
+
// A declaration carrying a default binding is skipped: appending there is legal
|
|
332
|
+
// but rewrites a line the user did not ask about, and the synthesized path
|
|
333
|
+
// covers the case without touching it.
|
|
334
|
+
//
|
|
335
|
+
// `typeOnly` must match the declaration the specifier is leaving. A value
|
|
336
|
+
// import appended into `import type { … }` becomes unusable as a value
|
|
337
|
+
// (TS1361), and a type import appended into a value declaration survives into
|
|
338
|
+
// the emitted JavaScript under `verbatimModuleSyntax`. Neither is a formatting
|
|
339
|
+
// difference, so the two kinds never share a destination.
|
|
340
|
+
func (s *solidState) solidNamedImportFrom(source string, typeOnly bool) *shimast.NamedImports {
|
|
341
|
+
for _, node := range s.imports {
|
|
342
|
+
decl := node.AsImportDeclaration()
|
|
343
|
+
if decl == nil || decl.ImportClause == nil {
|
|
344
|
+
continue
|
|
261
345
|
}
|
|
346
|
+
if stringLiteralText(decl.ModuleSpecifier) != source {
|
|
347
|
+
continue
|
|
348
|
+
}
|
|
349
|
+
clause := decl.ImportClause.AsImportClause()
|
|
350
|
+
if clause == nil || clause.Name() != nil || clause.IsTypeOnly() != typeOnly ||
|
|
351
|
+
clause.NamedBindings == nil ||
|
|
352
|
+
clause.NamedBindings.Kind != shimast.KindNamedImports {
|
|
353
|
+
continue
|
|
354
|
+
}
|
|
355
|
+
if named := clause.NamedBindings.AsNamedImports(); named != nil &&
|
|
356
|
+
named.Elements != nil && len(named.Elements.Nodes) > 0 {
|
|
357
|
+
return named
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return nil
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// solidImportSourceEdits relocates one misrouted specifier to its canonical
|
|
364
|
+
// module.
|
|
365
|
+
//
|
|
366
|
+
// Three shapes, because a specifier can be alone or not and the destination can
|
|
367
|
+
// exist or not:
|
|
368
|
+
//
|
|
369
|
+
// 1. The specifier is the declaration's only binding, so the declaration IS the
|
|
370
|
+
// import: rewrite its module specifier in place. The narrowest edit, and the
|
|
371
|
+
// only one the rule used to make.
|
|
372
|
+
// 2. The specifier has siblings and a declaration from the correct source
|
|
373
|
+
// already exists: cut the specifier out and append it there, which is the
|
|
374
|
+
// merge the rule's description promises and upstream's `appendImports`
|
|
375
|
+
// performs.
|
|
376
|
+
// 3. The specifier has siblings and no such declaration exists: cut it out and
|
|
377
|
+
// synthesize one directly above the declaration it left.
|
|
378
|
+
//
|
|
379
|
+
// Returning nil for every shape but the first is what made the rule report the
|
|
380
|
+
// most ordinary import in a Solid file — `import { createEffect, render } from
|
|
381
|
+
// "solid-js"` — with a message and no fix.
|
|
382
|
+
func solidImportSourceEdits(
|
|
383
|
+
file *shimast.SourceFile,
|
|
384
|
+
clause *shimast.ImportClause,
|
|
385
|
+
named *shimast.NamedImports,
|
|
386
|
+
moduleSpecifier *shimast.Node,
|
|
387
|
+
correct string,
|
|
388
|
+
specNode *shimast.Node,
|
|
389
|
+
destination *shimast.NamedImports,
|
|
390
|
+
) []TextEdit {
|
|
391
|
+
if clause == nil || named == nil || named.Elements == nil || file == nil {
|
|
392
|
+
return nil
|
|
393
|
+
}
|
|
394
|
+
// "Alone" means alone among the NAMED bindings. A default binding beside them
|
|
395
|
+
// does not make the specifier a sibling of anything, but it does decide how
|
|
396
|
+
// much has to be cut, which is why the two are tracked apart.
|
|
397
|
+
alone := len(named.Elements.Nodes) == 1
|
|
398
|
+
bare := clause.Name() == nil
|
|
399
|
+
// The destination is consulted BEFORE the in-place rewrite, or the rewrite
|
|
400
|
+
// wins on a file that already imports from the correct module and leaves two
|
|
401
|
+
// declarations of it — the duplicate the rule's own description promises to
|
|
402
|
+
// avoid.
|
|
403
|
+
if destination != nil {
|
|
404
|
+
insert, ok := solidAppendPoint(file, destination)
|
|
405
|
+
if !ok {
|
|
406
|
+
return nil
|
|
407
|
+
}
|
|
408
|
+
cut, text, ok := solidBindingCut(file, clause, named, specNode)
|
|
409
|
+
if !ok {
|
|
410
|
+
return nil
|
|
411
|
+
}
|
|
412
|
+
// Disjoint: the cut lies in this declaration and the append in another.
|
|
413
|
+
return []TextEdit{cut, {Pos: insert, End: insert, Text: ", " + text}}
|
|
414
|
+
}
|
|
415
|
+
if alone && bare {
|
|
416
|
+
// The declaration IS the import, so retarget it where it stands. The
|
|
417
|
+
// narrowest edit available, and the only one the rule used to make.
|
|
418
|
+
pos, end, ok := solidQuotedTextRange(file, moduleSpecifier)
|
|
419
|
+
if !ok {
|
|
420
|
+
return nil
|
|
421
|
+
}
|
|
422
|
+
return []TextEdit{{Pos: pos, End: end, Text: correct}}
|
|
423
|
+
}
|
|
424
|
+
cut, text, ok := solidBindingCut(file, clause, named, specNode)
|
|
425
|
+
if !ok {
|
|
426
|
+
return nil
|
|
427
|
+
}
|
|
428
|
+
declaration, ok := solidDeclarationStart(file, clause)
|
|
429
|
+
if !ok {
|
|
430
|
+
return nil
|
|
431
|
+
}
|
|
432
|
+
quote := solidQuoteCharacter(file, moduleSpecifier)
|
|
433
|
+
// A type-only declaration synthesizes a type-only one. Dropping the keyword
|
|
434
|
+
// emits a runtime import under `verbatimModuleSyntax` for a symbol that has
|
|
435
|
+
// no runtime existence.
|
|
436
|
+
keyword := "import "
|
|
437
|
+
if clause.IsTypeOnly() {
|
|
438
|
+
keyword = "import type "
|
|
439
|
+
}
|
|
440
|
+
line := keyword + "{ " + text + " } from " + quote + correct + quote + ";\n"
|
|
441
|
+
return []TextEdit{{Pos: declaration, End: declaration, Text: line}, cut}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// solidBindingCut removes the misrouted specifier from its declaration and
|
|
445
|
+
// reports the text removed, in whichever of three shapes the declaration is.
|
|
446
|
+
//
|
|
447
|
+
// With named siblings it takes the specifier and one comma. Alone beside a
|
|
448
|
+
// DEFAULT binding it takes the braces too, because `import Solid, {} from` is
|
|
449
|
+
// not what anyone meant. Alone with no default the specifier IS the whole
|
|
450
|
+
// declaration, so the declaration goes — leaving `import {} from "solid-js";`
|
|
451
|
+
// behind would be a statement importing nothing.
|
|
452
|
+
func solidBindingCut(
|
|
453
|
+
file *shimast.SourceFile,
|
|
454
|
+
clause *shimast.ImportClause,
|
|
455
|
+
named *shimast.NamedImports,
|
|
456
|
+
specNode *shimast.Node,
|
|
457
|
+
) (TextEdit, string, bool) {
|
|
458
|
+
if len(named.Elements.Nodes) > 1 {
|
|
459
|
+
return solidSpecifierCut(file, named, specNode)
|
|
460
|
+
}
|
|
461
|
+
pos, end := tokenRange(file, specNode)
|
|
462
|
+
if pos < 0 || end < pos || end > len(file.Text()) {
|
|
463
|
+
return TextEdit{}, "", false
|
|
464
|
+
}
|
|
465
|
+
text := file.Text()[pos:end]
|
|
466
|
+
if name := clause.Name(); name != nil {
|
|
467
|
+
from := name.End()
|
|
468
|
+
to := named.AsNode().End()
|
|
469
|
+
if from < 0 || to > len(file.Text()) || from >= to {
|
|
470
|
+
return TextEdit{}, "", false
|
|
471
|
+
}
|
|
472
|
+
return TextEdit{Pos: from, End: to}, text, true
|
|
473
|
+
}
|
|
474
|
+
from, to, ok := solidDeclarationRange(file, clause)
|
|
475
|
+
if !ok {
|
|
476
|
+
return TextEdit{}, "", false
|
|
477
|
+
}
|
|
478
|
+
return TextEdit{Pos: from, End: to}, text, true
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// solidDeclarationRange bounds the whole import declaration owning `clause`,
|
|
482
|
+
// including the line break after it so removing the declaration does not leave
|
|
483
|
+
// a blank line where it stood.
|
|
484
|
+
func solidDeclarationRange(file *shimast.SourceFile, clause *shimast.ImportClause) (int, int, bool) {
|
|
485
|
+
node := clause.AsNode()
|
|
486
|
+
if node == nil || node.Parent == nil {
|
|
487
|
+
return 0, 0, false
|
|
488
|
+
}
|
|
489
|
+
text := file.Text()
|
|
490
|
+
pos := shimscanner.SkipTrivia(text, node.Parent.Pos())
|
|
491
|
+
end := node.Parent.End()
|
|
492
|
+
if pos < 0 || end < pos || end > len(text) {
|
|
493
|
+
return 0, 0, false
|
|
494
|
+
}
|
|
495
|
+
if end < len(text) && text[end] == '\r' {
|
|
496
|
+
end++
|
|
497
|
+
}
|
|
498
|
+
if end < len(text) && text[end] == '\n' {
|
|
499
|
+
end++
|
|
500
|
+
}
|
|
501
|
+
return pos, end, true
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// solidSpecifierCut removes one specifier from a named-import list along with
|
|
505
|
+
// the comma that joined it, and reports the text it removed so a caller can put
|
|
506
|
+
// it somewhere else.
|
|
507
|
+
//
|
|
508
|
+
// The comma taken is the one BEFORE the specifier when it has a predecessor,
|
|
509
|
+
// and the one after when it is first. Taking the wrong side leaves either a
|
|
510
|
+
// leading or a doubled comma, both of which are parse errors rather than
|
|
511
|
+
// cosmetic damage.
|
|
512
|
+
func solidSpecifierCut(
|
|
513
|
+
file *shimast.SourceFile,
|
|
514
|
+
named *shimast.NamedImports,
|
|
515
|
+
specNode *shimast.Node,
|
|
516
|
+
) (TextEdit, string, bool) {
|
|
517
|
+
nodes := named.Elements.Nodes
|
|
518
|
+
index := -1
|
|
519
|
+
for i, node := range nodes {
|
|
520
|
+
if node == specNode {
|
|
521
|
+
index = i
|
|
522
|
+
break
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
if index < 0 || len(nodes) < 2 {
|
|
526
|
+
return TextEdit{}, "", false
|
|
527
|
+
}
|
|
528
|
+
pos, end := tokenRange(file, specNode)
|
|
529
|
+
if pos < 0 || end < pos {
|
|
530
|
+
return TextEdit{}, "", false
|
|
531
|
+
}
|
|
532
|
+
text := file.Text()[pos:end]
|
|
533
|
+
src := file.Text()
|
|
534
|
+
if index > 0 {
|
|
535
|
+
previousEnd := nodes[index-1].End()
|
|
536
|
+
if previousEnd < 0 || previousEnd > pos {
|
|
537
|
+
return TextEdit{}, "", false
|
|
538
|
+
}
|
|
539
|
+
return TextEdit{Pos: previousEnd, End: end}, text, true
|
|
540
|
+
}
|
|
541
|
+
nextPos := shimscanner.SkipTrivia(src, nodes[index+1].Pos())
|
|
542
|
+
if nextPos < end || nextPos > len(src) {
|
|
543
|
+
return TextEdit{}, "", false
|
|
544
|
+
}
|
|
545
|
+
return TextEdit{Pos: pos, End: nextPos}, text, true
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// solidAppendPoint returns the offset just past a named-import list's last
|
|
549
|
+
// specifier, where a relocated one is appended.
|
|
550
|
+
func solidAppendPoint(file *shimast.SourceFile, named *shimast.NamedImports) (int, bool) {
|
|
551
|
+
nodes := named.Elements.Nodes
|
|
552
|
+
if len(nodes) == 0 {
|
|
553
|
+
return 0, false
|
|
262
554
|
}
|
|
555
|
+
end := nodes[len(nodes)-1].End()
|
|
556
|
+
if end < 0 || end > len(file.Text()) {
|
|
557
|
+
return 0, false
|
|
558
|
+
}
|
|
559
|
+
return end, true
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// solidDeclarationStart returns the offset of the `import` keyword owning
|
|
563
|
+
// `clause`, which is where a synthesized declaration is inserted so it lands on
|
|
564
|
+
// its own line directly above.
|
|
565
|
+
func solidDeclarationStart(file *shimast.SourceFile, clause *shimast.ImportClause) (int, bool) {
|
|
566
|
+
node := clause.AsNode()
|
|
567
|
+
if node == nil || node.Parent == nil {
|
|
568
|
+
return 0, false
|
|
569
|
+
}
|
|
570
|
+
pos := shimscanner.SkipTrivia(file.Text(), node.Parent.Pos())
|
|
571
|
+
if pos < 0 || pos > len(file.Text()) {
|
|
572
|
+
return 0, false
|
|
573
|
+
}
|
|
574
|
+
return pos, true
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// solidQuoteCharacter returns the quote the file already used for this module
|
|
578
|
+
// specifier, so a synthesized import matches the surrounding style instead of
|
|
579
|
+
// fighting `format/quotes`.
|
|
580
|
+
func solidQuoteCharacter(file *shimast.SourceFile, moduleSpecifier *shimast.Node) string {
|
|
581
|
+
pos, end := tokenRange(file, moduleSpecifier)
|
|
582
|
+
if pos < 0 || end-pos < 2 {
|
|
583
|
+
return "\""
|
|
584
|
+
}
|
|
585
|
+
if quote := file.Text()[pos]; quote == '\'' || quote == '"' {
|
|
586
|
+
return string(quote)
|
|
587
|
+
}
|
|
588
|
+
return "\""
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// solidQuotedTextRange returns the byte range of the text inside a string
|
|
592
|
+
// literal's quotes, so a rewrite replaces the contents and leaves the
|
|
593
|
+
// surrounding quote characters exactly as the author wrote them. Reports false
|
|
594
|
+
// for anything that is not a plainly quoted literal, such as a
|
|
595
|
+
// parse-recovered node with no closing quote.
|
|
596
|
+
func solidQuotedTextRange(file *shimast.SourceFile, node *shimast.Node) (int, int, bool) {
|
|
597
|
+
pos, end := tokenRange(file, node)
|
|
598
|
+
if pos < 0 || end-pos < 2 {
|
|
599
|
+
return 0, 0, false
|
|
600
|
+
}
|
|
601
|
+
src := file.Text()
|
|
602
|
+
quote := src[pos]
|
|
603
|
+
if quote != '"' && quote != '\'' {
|
|
604
|
+
return 0, 0, false
|
|
605
|
+
}
|
|
606
|
+
if src[end-1] != quote {
|
|
607
|
+
return 0, 0, false
|
|
608
|
+
}
|
|
609
|
+
return pos + 1, end - 1, true
|
|
263
610
|
}
|
|
264
611
|
|
|
265
612
|
func (s *solidState) reportNoDestructure(ctx *Context) {
|
|
@@ -386,15 +733,49 @@ func (s *solidState) reportReactSpecificProps(ctx *Context) {
|
|
|
386
733
|
}
|
|
387
734
|
switch solidJSXAttrName(attr) {
|
|
388
735
|
case "className":
|
|
389
|
-
ctx.
|
|
736
|
+
ctx.ReportFix(
|
|
737
|
+
attr,
|
|
738
|
+
"Use Solid's `class` prop instead of `className`.",
|
|
739
|
+
solidAttrRenameEdits(ctx.File, attr, "class")...,
|
|
740
|
+
)
|
|
390
741
|
case "htmlFor":
|
|
391
|
-
ctx.
|
|
742
|
+
ctx.ReportFix(
|
|
743
|
+
attr,
|
|
744
|
+
"Use Solid's `for` prop instead of `htmlFor`.",
|
|
745
|
+
solidAttrRenameEdits(ctx.File, attr, "for")...,
|
|
746
|
+
)
|
|
392
747
|
case "key":
|
|
393
748
|
ctx.Report(attr, "DOM elements in Solid do not need React-style `key` props.")
|
|
394
749
|
}
|
|
395
750
|
}
|
|
396
751
|
}
|
|
397
752
|
|
|
753
|
+
// solidAttrRenameEdits rewrites a JSX attribute's name token and nothing else.
|
|
754
|
+
//
|
|
755
|
+
// `className` and `class` mean the same thing to a Solid DOM element, as do
|
|
756
|
+
// `htmlFor` and `for`, so the rename is a pure 1:1 substitution and safe to
|
|
757
|
+
// impose. Leaving the value untouched is what makes it safe for every value
|
|
758
|
+
// shape at once: a string, an expression container, and a shorthand boolean
|
|
759
|
+
// attribute all keep whatever follows the name.
|
|
760
|
+
//
|
|
761
|
+
// The `key` arm has no counterpart here on purpose. Solid DOM elements do not
|
|
762
|
+
// consume `key` at all, so the resolution is deletion, not a rename, and the
|
|
763
|
+
// deletion has to take the surrounding whitespace with it to leave valid JSX.
|
|
764
|
+
//
|
|
765
|
+
// Returns nil when the name token cannot be located, which downgrades the
|
|
766
|
+
// caller to a plain diagnostic.
|
|
767
|
+
func solidAttrRenameEdits(file *shimast.SourceFile, attrNode *shimast.Node, name string) []TextEdit {
|
|
768
|
+
attr := attrNode.AsJsxAttribute()
|
|
769
|
+
if attr == nil {
|
|
770
|
+
return nil
|
|
771
|
+
}
|
|
772
|
+
pos, end := tokenRange(file, attr.Name())
|
|
773
|
+
if pos < 0 {
|
|
774
|
+
return nil
|
|
775
|
+
}
|
|
776
|
+
return []TextEdit{{Pos: pos, End: end, Text: name}}
|
|
777
|
+
}
|
|
778
|
+
|
|
398
779
|
func (s *solidState) reportUnknownNamespaces(ctx *Context) {
|
|
399
780
|
for _, attr := range s.jsxAttrs {
|
|
400
781
|
name := solidJSXAttrName(attr)
|
|
@@ -419,7 +800,7 @@ func (s *solidState) reportReactDeps(ctx *Context) {
|
|
|
419
800
|
if call == nil || call.Arguments == nil || len(call.Arguments.Nodes) != 2 {
|
|
420
801
|
continue
|
|
421
802
|
}
|
|
422
|
-
name := s.
|
|
803
|
+
name := s.solidTrackedCallName(ctx, call)
|
|
423
804
|
if name != "createEffect" && name != "createMemo" {
|
|
424
805
|
continue
|
|
425
806
|
}
|
|
@@ -431,6 +812,41 @@ func (s *solidState) reportReactDeps(ctx *Context) {
|
|
|
431
812
|
}
|
|
432
813
|
}
|
|
433
814
|
|
|
815
|
+
// solidTrackedCallName returns the canonical Solid name only when the call is
|
|
816
|
+
// proven to come from a Solid module binding. The rule carries an
|
|
817
|
+
// `Unnecessary` tag, so a same-named local helper cannot be treated as a Solid
|
|
818
|
+
// primitive merely because the file imports some other Solid symbol.
|
|
819
|
+
func (s *solidState) solidTrackedCallName(ctx *Context, call *shimast.CallExpression) string {
|
|
820
|
+
if ctx == nil || call == nil {
|
|
821
|
+
return ""
|
|
822
|
+
}
|
|
823
|
+
expr := stripParens(call.Expression)
|
|
824
|
+
if expr == nil {
|
|
825
|
+
return ""
|
|
826
|
+
}
|
|
827
|
+
if name := identifierText(expr); name != "" {
|
|
828
|
+
if s.solidSymbols[name] == nil || canonicalValueSymbol(ctx, expr) != s.solidSymbols[name] {
|
|
829
|
+
return ""
|
|
830
|
+
}
|
|
831
|
+
return s.solidImport[name]
|
|
832
|
+
}
|
|
833
|
+
if expr.Kind != shimast.KindPropertyAccessExpression {
|
|
834
|
+
return ""
|
|
835
|
+
}
|
|
836
|
+
access := expr.AsPropertyAccessExpression()
|
|
837
|
+
if access == nil {
|
|
838
|
+
return ""
|
|
839
|
+
}
|
|
840
|
+
object := stripParens(access.Expression)
|
|
841
|
+
local := identifierText(object)
|
|
842
|
+
if !isSolidModuleSource(s.importedFrom[local]) ||
|
|
843
|
+
s.solidSymbols[local] == nil ||
|
|
844
|
+
canonicalValueSymbol(ctx, object) != s.solidSymbols[local] {
|
|
845
|
+
return ""
|
|
846
|
+
}
|
|
847
|
+
return identifierText(access.Name())
|
|
848
|
+
}
|
|
849
|
+
|
|
434
850
|
func (s *solidState) reportProxyAPIs(ctx *Context) {
|
|
435
851
|
for _, node := range s.imports {
|
|
436
852
|
decl := node.AsImportDeclaration()
|
|
@@ -1036,6 +1452,10 @@ func isSolidSource(source string) bool {
|
|
|
1036
1452
|
return source == "solid-js" || source == "solid-js/web" || source == "solid-js/store"
|
|
1037
1453
|
}
|
|
1038
1454
|
|
|
1455
|
+
func isSolidModuleSource(source string) bool {
|
|
1456
|
+
return source == "solid-js" || strings.HasPrefix(source, "solid-js/")
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1039
1459
|
func solidIsDOMTag(name string) bool {
|
|
1040
1460
|
if name == "" {
|
|
1041
1461
|
return false
|