@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.
Files changed (63) hide show
  1. package/README.md +1 -1
  2. package/lib/index.js +26 -4
  3. package/lib/index.js.map +1 -1
  4. package/lib/structures/format/ITtscLintFormat.d.ts +14 -10
  5. package/lib/structures/rules/ITtscLintRegexpRules.d.ts +24 -9
  6. package/lib/structures/rules/ITtscLintSecurityRules.d.ts +18 -0
  7. package/lib/structures/rules/ITtscLintSolidRules.d.ts +22 -2
  8. package/lib/structures/rules/ITtscLintStorybookRules.d.ts +21 -0
  9. package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.d.ts +10 -1
  10. package/lib/structures/rules/ITtscLintTypeScriptRules.d.ts +13 -2
  11. package/linthost/compile.go +9 -4
  12. package/linthost/config.go +18 -14
  13. package/linthost/contrib_adapter.go +57 -0
  14. package/linthost/dispatch.go +5 -1
  15. package/linthost/display_width.go +179 -41
  16. package/linthost/engine.go +120 -4
  17. package/linthost/flags_gen.go +4 -4
  18. package/linthost/hints.go +207 -0
  19. package/linthost/host.go +68 -0
  20. package/linthost/lsp.go +148 -29
  21. package/linthost/print_dispatch.go +24 -0
  22. package/linthost/print_nodes_array.go +88 -15
  23. package/linthost/print_nodes_call.go +38 -31
  24. package/linthost/print_nodes_control_flow.go +319 -0
  25. package/linthost/print_nodes_function.go +140 -13
  26. package/linthost/print_nodes_list.go +22 -10
  27. package/linthost/project_engine.go +18 -1
  28. package/linthost/project_rules.go +47 -0
  29. package/linthost/rule_docs.go +114 -0
  30. package/linthost/rules_boundaries.go +80 -2
  31. package/linthost/rules_format_bracket_spacing.go +18 -6
  32. package/linthost/rules_format_clause_join.go +11 -8
  33. package/linthost/rules_format_indent.go +155 -4
  34. package/linthost/rules_format_print_width.go +12 -35
  35. package/linthost/rules_format_quote_props.go +88 -27
  36. package/linthost/rules_format_statement_split.go +81 -0
  37. package/linthost/rules_format_trailing_comma.go +62 -94
  38. package/linthost/rules_gap.go +26 -17
  39. package/linthost/rules_jsdoc.go +54 -0
  40. package/linthost/rules_logic.go +30 -15
  41. package/linthost/rules_no_redeclare.go +12 -3
  42. package/linthost/rules_promise.go +37 -15
  43. package/linthost/rules_regexp.go +443 -49
  44. package/linthost/rules_security.go +243 -4
  45. package/linthost/rules_solid.go +430 -10
  46. package/linthost/rules_storybook.go +255 -10
  47. package/linthost/rules_suggestions.go +60 -39
  48. package/linthost/rules_ts.go +7 -0
  49. package/linthost/rules_ts_async.go +80 -2
  50. package/linthost/rules_ts_extra.go +21 -7
  51. package/linthost/serve.go +318 -0
  52. package/linthost/width_tables_gen.go +219 -0
  53. package/package.json +2 -2
  54. package/rule/hint.go +142 -0
  55. package/rule/rule.go +197 -1
  56. package/src/index.ts +35 -4
  57. package/src/structures/format/ITtscLintFormat.ts +14 -10
  58. package/src/structures/rules/ITtscLintRegexpRules.ts +24 -9
  59. package/src/structures/rules/ITtscLintSecurityRules.ts +18 -0
  60. package/src/structures/rules/ITtscLintSolidRules.ts +22 -2
  61. package/src/structures/rules/ITtscLintStorybookRules.ts +21 -0
  62. package/src/structures/rules/ITtscLintTypeScriptRuleOptions.ts +10 -1
  63. package/src/structures/rules/ITtscLintTypeScriptRules.ts +13 -2
@@ -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 strings.HasPrefix(source, "solid-js") {
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 || bindings.Kind != shimast.KindNamedImports {
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 strings.HasPrefix(source, "solid-js") {
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
- if correct := solidPreferredSource(imported); correct != "" && correct != source {
259
- ctx.Report(specNode, "Import Solid API from its canonical module.")
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.Report(attr, "Use Solid's `class` prop instead of `className`.")
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.Report(attr, "Use Solid's `for` prop instead of `htmlFor`.")
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.callName(call)
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