@ttsc/lint 0.18.4 → 0.19.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.
Files changed (188) hide show
  1. package/README.md +125 -43
  2. package/go.mod +5 -4
  3. package/internal/rulecode/rulecode.go +88 -0
  4. package/lib/index.d.ts +10 -1
  5. package/lib/index.js +45 -17
  6. package/lib/index.js.map +1 -1
  7. package/lib/structures/ITtscLintConfig.d.ts +3 -0
  8. package/lib/structures/TtscLintRuleSetting.d.ts +3 -2
  9. package/lib/structures/format/ITtscLintFormat.d.ts +3 -2
  10. package/lib/structures/format/ITtscLintFormatSortImports.d.ts +20 -4
  11. package/lib/structures/rules/ITtscLintBoundariesRuleOptions.d.ts +80 -8
  12. package/lib/structures/rules/ITtscLintBoundariesRules.d.ts +6 -8
  13. package/lib/structures/rules/ITtscLintContributorRules.d.ts +18 -2
  14. package/lib/structures/rules/ITtscLintCoreRuleOptions.d.ts +347 -0
  15. package/lib/structures/rules/ITtscLintCoreRuleOptions.js +3 -0
  16. package/lib/structures/rules/ITtscLintCoreRuleOptions.js.map +1 -0
  17. package/lib/structures/rules/ITtscLintCoreRules.d.ts +101 -67
  18. package/lib/structures/rules/ITtscLintRegexpRules.d.ts +7 -2
  19. package/lib/structures/rules/ITtscLintRuleOptionsMap.d.ts +53 -7
  20. package/lib/structures/rules/ITtscLintRules.d.ts +6 -2
  21. package/lib/structures/rules/ITtscLintSolidRules.d.ts +0 -8
  22. package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.d.ts +206 -0
  23. package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.js +8 -0
  24. package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.js.map +1 -0
  25. package/lib/structures/rules/ITtscLintTypeScriptRules.d.ts +55 -42
  26. package/lib/structures/rules/ITtscLintUnicornRuleOptions.d.ts +276 -0
  27. package/lib/structures/rules/ITtscLintUnicornRuleOptions.js +3 -0
  28. package/lib/structures/rules/ITtscLintUnicornRuleOptions.js.map +1 -0
  29. package/lib/structures/rules/ITtscLintUnicornRules.d.ts +44 -36
  30. package/lib/structures/rules/index.d.ts +3 -0
  31. package/lib/structures/rules/index.js +3 -0
  32. package/lib/structures/rules/index.js.map +1 -1
  33. package/linthost/ast_helpers.go +521 -23
  34. package/linthost/ast_selector_matcher.go +1628 -0
  35. package/linthost/ast_selector_parser.go +628 -0
  36. package/linthost/comment_scan.go +154 -0
  37. package/linthost/compile.go +129 -79
  38. package/linthost/config.go +352 -88
  39. package/linthost/config_format.go +15 -1
  40. package/linthost/contrib_adapter.go +47 -8
  41. package/linthost/declaration_rules.go +12 -11
  42. package/linthost/directives.go +9 -67
  43. package/linthost/dispatch.go +37 -3
  44. package/linthost/engine.go +368 -53
  45. package/linthost/escape_string.go +77 -0
  46. package/linthost/fix.go +133 -23
  47. package/linthost/flags_gen.go +18 -17
  48. package/linthost/format.go +73 -167
  49. package/linthost/format_editor_settings.go +134 -26
  50. package/linthost/grapheme_tables_gen.go +1920 -0
  51. package/linthost/host.go +159 -61
  52. package/linthost/literal_escape_scan.go +117 -0
  53. package/linthost/lsp.go +545 -98
  54. package/linthost/numeric_literal_precision.go +228 -0
  55. package/linthost/polyfill_browserslist.go +1439 -0
  56. package/linthost/polyfill_browserslist_config.go +603 -0
  57. package/linthost/polyfill_corejs.go +750 -0
  58. package/linthost/polyfill_data.go +113 -0
  59. package/linthost/polyfill_data_gen.json +39868 -0
  60. package/linthost/print_nodes_array.go +7 -1
  61. package/linthost/print_nodes_object.go +7 -1
  62. package/linthost/project_engine.go +231 -0
  63. package/linthost/project_rules.go +78 -0
  64. package/linthost/regex_clean.go +191 -0
  65. package/linthost/regex_tree.go +1544 -0
  66. package/linthost/regex_tree_optimizer.go +1685 -0
  67. package/linthost/regexp_cache.go +44 -0
  68. package/linthost/rule_codes.go +105 -0
  69. package/linthost/rule_codes.json +745 -0
  70. package/linthost/rules_ban_ts_comment.go +312 -0
  71. package/linthost/rules_boundaries.go +128 -43
  72. package/linthost/rules_boundaries_dependencies.go +1013 -0
  73. package/linthost/rules_core_extra.go +62 -92
  74. package/linthost/rules_cypress.go +1 -1
  75. package/linthost/rules_default_case.go +87 -11
  76. package/linthost/rules_dupes.go +38 -13
  77. package/linthost/rules_empty.go +188 -20
  78. package/linthost/rules_escape.go +62 -48
  79. package/linthost/rules_format_arrow_parens.go +19 -5
  80. package/linthost/rules_format_bracket_spacing.go +1 -1
  81. package/linthost/rules_format_clause_join.go +1 -1
  82. package/linthost/rules_format_declaration_header.go +31 -19
  83. package/linthost/rules_format_indent.go +1 -1
  84. package/linthost/rules_format_jsdoc.go +1 -1
  85. package/linthost/rules_format_orphan_semi.go +1 -1
  86. package/linthost/rules_format_parameter_properties.go +17 -7
  87. package/linthost/rules_format_print_width.go +1 -1
  88. package/linthost/rules_format_quote_props.go +1 -1
  89. package/linthost/rules_format_quotes.go +1 -1
  90. package/linthost/rules_format_semi.go +106 -79
  91. package/linthost/rules_format_sort_imports.go +79 -45
  92. package/linthost/rules_format_statement_split.go +1 -1
  93. package/linthost/rules_format_ternary_nullish_parens.go +1 -1
  94. package/linthost/rules_format_trailing_comma.go +64 -1
  95. package/linthost/rules_format_whitespace.go +1 -1
  96. package/linthost/rules_functional.go +21 -22
  97. package/linthost/rules_gap.go +80 -3
  98. package/linthost/rules_grouped_accessor_pairs.go +84 -25
  99. package/linthost/rules_guard_for_in.go +64 -147
  100. package/linthost/rules_jsx_a11y.go +232 -22
  101. package/linthost/rules_logic.go +84 -38
  102. package/linthost/rules_nextjs.go +105 -16
  103. package/linthost/rules_no_class_assign.go +70 -0
  104. package/linthost/rules_no_duplicate_imports.go +266 -0
  105. package/linthost/rules_no_else_return.go +156 -38
  106. package/linthost/rules_no_extend_native.go +113 -17
  107. package/linthost/rules_no_fallthrough.go +1013 -0
  108. package/linthost/rules_no_func_assign.go +70 -0
  109. package/linthost/rules_no_import_assign.go +278 -86
  110. package/linthost/rules_no_inner_declarations.go +186 -0
  111. package/linthost/rules_no_loop_func.go +438 -53
  112. package/linthost/rules_no_mixed_operators.go +208 -52
  113. package/linthost/rules_no_param_reassign.go +376 -61
  114. package/linthost/rules_no_promise_executor_return.go +156 -0
  115. package/linthost/rules_no_restricted_imports.go +1013 -57
  116. package/linthost/rules_no_restricted_syntax.go +190 -26
  117. package/linthost/rules_no_useless_constructor.go +6 -36
  118. package/linthost/rules_problems.go +154 -311
  119. package/linthost/rules_promise.go +2578 -109
  120. package/linthost/rules_react_extras.go +71 -45
  121. package/linthost/rules_react_hooks.go +18 -2
  122. package/linthost/rules_react_perf.go +1 -0
  123. package/linthost/rules_react_refresh.go +1 -1
  124. package/linthost/rules_regexp.go +230 -14
  125. package/linthost/rules_security.go +45 -19
  126. package/linthost/rules_self.go +21 -1
  127. package/linthost/rules_solid.go +13 -7
  128. package/linthost/rules_storybook.go +1 -1
  129. package/linthost/rules_suggestions.go +490 -84
  130. package/linthost/rules_testing_library.go +12 -1
  131. package/linthost/rules_ts.go +227 -54
  132. package/linthost/rules_ts_async.go +0 -131
  133. package/linthost/rules_ts_no_base_to_string.go +3 -4
  134. package/linthost/rules_ts_no_misused_promises.go +817 -0
  135. package/linthost/rules_ts_no_restricted_types.go +320 -34
  136. package/linthost/rules_ts_no_unnecessary_qualifier.go +132 -55
  137. package/linthost/rules_ts_no_unsafe_assignment.go +608 -60
  138. package/linthost/rules_ts_require_array_sort_compare.go +5 -5
  139. package/linthost/rules_ts_switch_exhaustiveness_check.go +556 -86
  140. package/linthost/rules_unicorn_better_regex.go +224 -16
  141. package/linthost/rules_unicorn_consistent_destructuring.go +1002 -7
  142. package/linthost/rules_unicorn_consistent_existence_index_check.go +208 -29
  143. package/linthost/rules_unicorn_consistent_function_scoping.go +1041 -8
  144. package/linthost/rules_unicorn_consistent_template_literal_escape.go +178 -7
  145. package/linthost/rules_unicorn_escape_case.go +30 -19
  146. package/linthost/rules_unicorn_filename_case.go +763 -16
  147. package/linthost/rules_unicorn_import_style.go +783 -17
  148. package/linthost/rules_unicorn_isolated_functions.go +934 -14
  149. package/linthost/rules_unicorn_no_hex_escape.go +26 -11
  150. package/linthost/rules_unicorn_no_typeof_undefined.go +234 -25
  151. package/linthost/rules_unicorn_no_unnecessary_polyfills.go +376 -21
  152. package/linthost/rules_unicorn_no_unused_properties.go +756 -19
  153. package/linthost/rules_unicorn_number_literal_case.go +58 -39
  154. package/linthost/rules_unicorn_prefer_at.go +7 -3
  155. package/linthost/rules_unicorn_prefer_number_properties.go +381 -48
  156. package/linthost/rules_unicorn_prefer_optional_catch_binding.go +24 -24
  157. package/linthost/rules_unicorn_prefer_simple_condition_first.go +597 -19
  158. package/linthost/rules_unicorn_prefer_string_raw.go +147 -16
  159. package/linthost/rules_unicorn_prevent_abbreviations.go +1963 -95
  160. package/linthost/rules_unicorn_string_content.go +611 -15
  161. package/linthost/rules_unicorn_switch_case_break_position.go +172 -15
  162. package/linthost/rules_unicorn_template_indent.go +923 -18
  163. package/linthost/rules_unicorn_text_encoding_identifier_case.go +202 -42
  164. package/linthost/rules_unicorn_throw_new_error.go +218 -26
  165. package/linthost/rules_var.go +829 -142
  166. package/linthost/string_length.go +222 -0
  167. package/package.json +4 -3
  168. package/rule/project.go +206 -0
  169. package/rule/rule.go +58 -1
  170. package/src/index.ts +58 -17
  171. package/src/structures/ITtscLintConfig.ts +3 -0
  172. package/src/structures/TtscLintRuleSetting.ts +3 -2
  173. package/src/structures/format/ITtscLintFormat.ts +3 -2
  174. package/src/structures/format/ITtscLintFormatSortImports.ts +21 -4
  175. package/src/structures/rules/ITtscLintBoundariesRuleOptions.ts +122 -8
  176. package/src/structures/rules/ITtscLintBoundariesRules.ts +6 -8
  177. package/src/structures/rules/ITtscLintContributorRules.ts +18 -2
  178. package/src/structures/rules/ITtscLintCoreRuleOptions.ts +465 -0
  179. package/src/structures/rules/ITtscLintCoreRules.ts +122 -67
  180. package/src/structures/rules/ITtscLintRegexpRules.ts +7 -2
  181. package/src/structures/rules/ITtscLintRuleOptionsMap.ts +88 -7
  182. package/src/structures/rules/ITtscLintRules.ts +7 -2
  183. package/src/structures/rules/ITtscLintSolidRules.ts +0 -9
  184. package/src/structures/rules/ITtscLintTypeScriptRuleOptions.ts +248 -0
  185. package/src/structures/rules/ITtscLintTypeScriptRules.ts +64 -42
  186. package/src/structures/rules/ITtscLintUnicornRuleOptions.ts +350 -0
  187. package/src/structures/rules/ITtscLintUnicornRules.ts +60 -36
  188. package/src/structures/rules/index.ts +3 -0
@@ -5,6 +5,7 @@ import (
5
5
  "strings"
6
6
 
7
7
  shimast "github.com/microsoft/typescript-go/shim/ast"
8
+ "golang.org/x/text/language"
8
9
  )
9
10
 
10
11
  type jsxAttr struct {
@@ -456,7 +457,7 @@ func jsxIsFocusable(info jsxElementInfo) bool {
456
457
 
457
458
  func jsxIsNonInteractiveElement(tag string) bool {
458
459
  switch tag {
459
- case "article", "aside", "div", "footer", "header", "li", "main", "nav", "ol", "p", "section", "ul",
460
+ case "article", "aside", "footer", "header", "li", "main", "nav", "ol", "p", "section", "ul",
460
461
  "h1", "h2", "h3", "h4", "h5", "h6":
461
462
  return true
462
463
  }
@@ -573,7 +574,26 @@ var jsxAutocompleteTokens = map[string]bool{
573
574
  "transaction-currency": true, "transaction-amount": true, "language": true, "bday": true,
574
575
  "bday-day": true, "bday-month": true, "bday-year": true, "sex": true, "tel": true,
575
576
  "tel-country-code": true, "tel-national": true, "tel-area-code": true, "tel-local": true,
576
- "tel-extension": true, "impp": true, "url": true, "photo": true,
577
+ "tel-local-prefix": true, "tel-local-suffix": true, "tel-extension": true,
578
+ "impp": true, "url": true, "photo": true,
579
+ }
580
+
581
+ var jsxAutocompleteContactQualifiers = map[string]bool{
582
+ "home": true, "work": true, "mobile": true, "fax": true, "pager": true,
583
+ }
584
+
585
+ var jsxAutocompleteContactPurposes = map[string]bool{
586
+ "email": true, "impp": true, "tel": true, "tel-country-code": true,
587
+ "tel-national": true, "tel-area-code": true, "tel-local": true,
588
+ "tel-local-prefix": true, "tel-local-suffix": true, "tel-extension": true,
589
+ }
590
+
591
+ var jsxHTMLInputTypes = map[string]bool{
592
+ "button": true, "checkbox": true, "color": true, "date": true, "datetime-local": true,
593
+ "email": true, "file": true, "hidden": true, "image": true, "month": true,
594
+ "number": true, "password": true, "radio": true, "range": true, "reset": true,
595
+ "search": true, "submit": true, "tel": true, "text": true, "time": true,
596
+ "url": true, "week": true,
577
597
  }
578
598
 
579
599
  type jsxA11yAltText struct{}
@@ -608,8 +628,10 @@ func (jsxA11yAltText) Check(ctx *Context, node *shimast.Node) {
608
628
 
609
629
  type jsxA11yAnchorHasContent struct{}
610
630
 
611
- func (jsxA11yAnchorHasContent) Name() string { return "jsx-a11y/anchor-has-content" }
612
- func (jsxA11yAnchorHasContent) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindJsxElement} }
631
+ func (jsxA11yAnchorHasContent) Name() string { return "jsx-a11y/anchor-has-content" }
632
+ func (jsxA11yAnchorHasContent) Visits() []shimast.Kind {
633
+ return []shimast.Kind{shimast.KindJsxElement, shimast.KindJsxSelfClosingElement}
634
+ }
613
635
  func (jsxA11yAnchorHasContent) Check(ctx *Context, node *shimast.Node) {
614
636
  info := jsxElementFromNode(node)
615
637
  if info.tag == "a" && !info.spread && !jsxHasAccessibleLabel(info) {
@@ -756,21 +778,123 @@ func (jsxA11yAutocompleteValid) Visits() []shimast.Kind {
756
778
  }
757
779
  func (jsxA11yAutocompleteValid) Check(ctx *Context, node *shimast.Node) {
758
780
  info := jsxElementFromNode(node)
759
- attr, ok := jsxKnownAttr(info.attrs, "autoComplete")
781
+ if info.tag != "input" {
782
+ return
783
+ }
784
+ attr, ok := info.attrs["autoComplete"]
760
785
  if !ok {
761
- attr, ok = jsxKnownAttr(info.attrs, "autocomplete")
786
+ attr, ok = info.attrs["autocomplete"]
762
787
  }
763
- if !ok || strings.TrimSpace(attr.value) == "" {
788
+ if !ok {
764
789
  return
765
790
  }
766
- for _, token := range strings.Fields(strings.ToLower(attr.value)) {
767
- if strings.HasPrefix(token, "section-") || token == "shipping" || token == "billing" || token == "home" || token == "work" || token == "mobile" || token == "fax" || token == "pager" {
768
- continue
791
+ value, ok := jsxStringLiteralAttrValue(attr)
792
+ if !ok || strings.TrimSpace(value) == "" {
793
+ return
794
+ }
795
+ purpose, valid := jsxAutocompletePurpose(value)
796
+ if !valid {
797
+ ctx.Report(attr.node, "autocomplete contains an invalid token sequence.")
798
+ return
799
+ }
800
+ if !jsxAutocompletePurposeAllowsInputType(purpose, jsxAutocompleteInputType(info)) {
801
+ ctx.Report(attr.node, "autocomplete token is not valid for this input type.")
802
+ }
803
+ }
804
+
805
+ func jsxStringLiteralAttrValue(attr jsxAttr) (string, bool) {
806
+ if attr.node == nil || attr.boolean {
807
+ return "", false
808
+ }
809
+ jsx := attr.node.AsJsxAttribute()
810
+ if jsx == nil || jsx.Initializer == nil {
811
+ return "", false
812
+ }
813
+ switch jsx.Initializer.Kind {
814
+ case shimast.KindStringLiteral, shimast.KindNoSubstitutionTemplateLiteral:
815
+ return stringLiteralText(jsx.Initializer), true
816
+ case shimast.KindJsxExpression:
817
+ expression := jsx.Initializer.AsJsxExpression()
818
+ if expression != nil && expression.Expression != nil && (expression.Expression.Kind == shimast.KindStringLiteral || expression.Expression.Kind == shimast.KindNoSubstitutionTemplateLiteral) {
819
+ return stringLiteralText(expression.Expression), true
769
820
  }
770
- if !jsxAutocompleteTokens[token] {
771
- ctx.Report(attr.node, "autocomplete contains an invalid token.")
772
- return
821
+ }
822
+ return "", false
823
+ }
824
+
825
+ func jsxAutocompletePurpose(value string) (string, bool) {
826
+ tokens := strings.Fields(strings.ToLower(value))
827
+ if len(tokens) == 1 && (tokens[0] == "on" || tokens[0] == "off") {
828
+ return tokens[0], true
829
+ }
830
+ index := 0
831
+ if index < len(tokens) && strings.HasPrefix(tokens[index], "section-") {
832
+ if len(tokens[index]) == len("section-") {
833
+ return "", false
834
+ }
835
+ index++
836
+ }
837
+ if index < len(tokens) && (tokens[index] == "shipping" || tokens[index] == "billing") {
838
+ index++
839
+ }
840
+ qualifier := ""
841
+ if index < len(tokens) && jsxAutocompleteContactQualifiers[tokens[index]] {
842
+ qualifier = tokens[index]
843
+ index++
844
+ }
845
+ if index >= len(tokens) {
846
+ return "", false
847
+ }
848
+ purpose := tokens[index]
849
+ index++
850
+ if index < len(tokens) {
851
+ if index != len(tokens)-1 || tokens[index] != "webauthn" {
852
+ return "", false
773
853
  }
854
+ index++
855
+ }
856
+ if purpose == "on" || purpose == "off" || !jsxAutocompleteTokens[purpose] {
857
+ return "", false
858
+ }
859
+ if qualifier != "" && !jsxAutocompleteContactPurposes[purpose] {
860
+ return "", false
861
+ }
862
+ return purpose, index == len(tokens)
863
+ }
864
+
865
+ func jsxAutocompleteInputType(info jsxElementInfo) string {
866
+ attr, ok := jsxKnownAttr(info.attrs, "type")
867
+ if !ok {
868
+ return "text"
869
+ }
870
+ inputType := strings.ToLower(strings.TrimSpace(attr.value))
871
+ if !jsxHTMLInputTypes[inputType] {
872
+ return "text"
873
+ }
874
+ return inputType
875
+ }
876
+
877
+ func jsxAutocompletePurposeAllowsInputType(purpose string, inputType string) bool {
878
+ if purpose == "" || purpose == "on" || purpose == "off" || inputType == "text" {
879
+ return true
880
+ }
881
+ switch purpose {
882
+ case "email", "username":
883
+ return inputType == "email" || inputType == "search"
884
+ case "url", "photo", "impp":
885
+ return inputType == "url" || inputType == "search"
886
+ case "tel", "tel-country-code", "tel-national", "tel-area-code", "tel-local", "tel-local-prefix", "tel-local-suffix", "tel-extension":
887
+ return inputType == "tel" || inputType == "search"
888
+ case "new-password", "current-password":
889
+ return inputType == "password" || inputType == "search"
890
+ case "bday":
891
+ return inputType == "date" || inputType == "search"
892
+ case "cc-exp":
893
+ return inputType == "month" || inputType == "tel" || inputType == "search"
894
+ case "cc-number", "cc-exp-month", "cc-exp-year", "cc-csc", "transaction-amount", "bday-day", "bday-month", "bday-year":
895
+ return inputType == "number" || inputType == "tel" || inputType == "search"
896
+ default:
897
+ return false
774
898
  }
775
899
  }
776
900
 
@@ -806,7 +930,7 @@ type jsxA11yHeadingHasContent struct{}
806
930
 
807
931
  func (jsxA11yHeadingHasContent) Name() string { return "jsx-a11y/heading-has-content" }
808
932
  func (jsxA11yHeadingHasContent) Visits() []shimast.Kind {
809
- return []shimast.Kind{shimast.KindJsxElement}
933
+ return []shimast.Kind{shimast.KindJsxElement, shimast.KindJsxSelfClosingElement}
810
934
  }
811
935
  func (jsxA11yHeadingHasContent) Check(ctx *Context, node *shimast.Node) {
812
936
  info := jsxElementFromNode(node)
@@ -826,8 +950,55 @@ func (jsxA11yHtmlHasLang) Visits() []shimast.Kind {
826
950
  }
827
951
  func (jsxA11yHtmlHasLang) Check(ctx *Context, node *shimast.Node) {
828
952
  info := jsxElementFromNode(node)
829
- if info.tag == "html" && !info.spread && !jsxHasAttr(info.attrs, "lang") {
830
- ctx.Report(info.opening, "The html element must have a lang attribute.")
953
+ if info.tag != "html" {
954
+ return
955
+ }
956
+ if !jsxHasAttr(info.attrs, "lang") {
957
+ if !info.spread {
958
+ ctx.Report(info.opening, "The html element must have a lang attribute.")
959
+ }
960
+ return
961
+ }
962
+ if attr := info.attrs["lang"]; jsxA11yHtmlLangIsStaticallyFalsy(attr) {
963
+ ctx.Report(attr.node, "The html element must have a non-empty lang attribute.")
964
+ }
965
+ }
966
+
967
+ func jsxA11yHtmlLangIsStaticallyFalsy(attr jsxAttr) bool {
968
+ if attr.node == nil || attr.boolean {
969
+ return false
970
+ }
971
+ jsx := attr.node.AsJsxAttribute()
972
+ if jsx == nil || jsx.Initializer == nil {
973
+ return false
974
+ }
975
+ if jsx.Initializer.Kind == shimast.KindStringLiteral || jsx.Initializer.Kind == shimast.KindNoSubstitutionTemplateLiteral {
976
+ return strings.TrimSpace(stringLiteralText(jsx.Initializer)) == ""
977
+ }
978
+ if jsx.Initializer.Kind != shimast.KindJsxExpression {
979
+ return false
980
+ }
981
+ expression := jsx.Initializer.AsJsxExpression()
982
+ if expression == nil || expression.Expression == nil {
983
+ return true
984
+ }
985
+ switch expression.Expression.Kind {
986
+ case shimast.KindStringLiteral, shimast.KindNoSubstitutionTemplateLiteral:
987
+ return strings.TrimSpace(stringLiteralText(expression.Expression)) == ""
988
+ case shimast.KindFalseKeyword, shimast.KindNullKeyword:
989
+ return true
990
+ case shimast.KindNumericLiteral:
991
+ value := strings.ReplaceAll(numericLiteralText(expression.Expression), "_", "")
992
+ if integer, err := strconv.ParseInt(value, 0, 64); err == nil {
993
+ return integer == 0
994
+ }
995
+ if unsigned, err := strconv.ParseUint(value, 0, 64); err == nil {
996
+ return unsigned == 0
997
+ }
998
+ number, err := strconv.ParseFloat(value, 64)
999
+ return err == nil && number == 0
1000
+ default:
1001
+ return identifierText(expression.Expression) == "undefined"
831
1002
  }
832
1003
  }
833
1004
 
@@ -910,21 +1081,60 @@ func (jsxA11yLang) Visits() []shimast.Kind {
910
1081
  }
911
1082
  func (jsxA11yLang) Check(ctx *Context, node *shimast.Node) {
912
1083
  info := jsxElementFromNode(node)
913
- attr, ok := jsxKnownAttr(info.attrs, "lang")
1084
+ if info.tag != "html" {
1085
+ return
1086
+ }
1087
+ attr, ok := info.attrs["lang"]
914
1088
  if !ok {
915
1089
  return
916
1090
  }
917
- value := strings.TrimSpace(attr.value)
918
- parts := strings.Split(value, "-")
919
- if value == "" || len(parts[0]) < 2 || len(parts[0]) > 3 {
1091
+ if jsxA11yLangIsInvalid(attr) {
920
1092
  ctx.Report(attr.node, "lang must be a valid BCP 47 language tag.")
921
1093
  }
922
1094
  }
923
1095
 
1096
+ func jsxA11yLangIsInvalid(attr jsxAttr) bool {
1097
+ if attr.node == nil || attr.boolean {
1098
+ return true
1099
+ }
1100
+ jsx := attr.node.AsJsxAttribute()
1101
+ if jsx == nil || jsx.Initializer == nil {
1102
+ return true
1103
+ }
1104
+ validate := func(value string) bool {
1105
+ if value != strings.TrimSpace(value) {
1106
+ return true
1107
+ }
1108
+ _, err := language.Parse(value)
1109
+ return err != nil
1110
+ }
1111
+ switch jsx.Initializer.Kind {
1112
+ case shimast.KindStringLiteral, shimast.KindNoSubstitutionTemplateLiteral:
1113
+ return validate(stringLiteralText(jsx.Initializer))
1114
+ case shimast.KindJsxExpression:
1115
+ expression := jsx.Initializer.AsJsxExpression()
1116
+ if expression == nil || expression.Expression == nil {
1117
+ return true
1118
+ }
1119
+ switch expression.Expression.Kind {
1120
+ case shimast.KindStringLiteral, shimast.KindNoSubstitutionTemplateLiteral:
1121
+ return validate(stringLiteralText(expression.Expression))
1122
+ case shimast.KindTrueKeyword, shimast.KindFalseKeyword, shimast.KindNumericLiteral:
1123
+ return true
1124
+ default:
1125
+ return identifierText(expression.Expression) == "undefined"
1126
+ }
1127
+ default:
1128
+ return false
1129
+ }
1130
+ }
1131
+
924
1132
  type jsxA11yMediaHasCaption struct{}
925
1133
 
926
- func (jsxA11yMediaHasCaption) Name() string { return "jsx-a11y/media-has-caption" }
927
- func (jsxA11yMediaHasCaption) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindJsxElement} }
1134
+ func (jsxA11yMediaHasCaption) Name() string { return "jsx-a11y/media-has-caption" }
1135
+ func (jsxA11yMediaHasCaption) Visits() []shimast.Kind {
1136
+ return []shimast.Kind{shimast.KindJsxElement, shimast.KindJsxSelfClosingElement}
1137
+ }
928
1138
  func (jsxA11yMediaHasCaption) Check(ctx *Context, node *shimast.Node) {
929
1139
  info := jsxElementFromNode(node)
930
1140
  if (info.tag == "audio" || info.tag == "video") && !info.spread && !jsxHasTrackCaption(info.children) {
@@ -49,23 +49,7 @@ func (noExtraBooleanCast) Check(ctx *Context, node *shimast.Node) {
49
49
  ctx.Report(node, message)
50
50
  return
51
51
  }
52
- src := ctx.File.Text()
53
- argStart := shimscanner.SkipTrivia(src, arg.Pos())
54
- argEnd := arg.End()
55
- if argStart < 0 || argStart >= argEnd || argEnd > len(src) {
56
- ctx.Report(node, message)
57
- return
58
- }
59
- editPos := shimscanner.SkipTrivia(src, node.Pos())
60
- if editPos < 0 || editPos >= node.End() {
61
- ctx.Report(node, message)
62
- return
63
- }
64
- ctx.ReportFix(
65
- node,
66
- message,
67
- TextEdit{Pos: editPos, End: node.End(), Text: src[argStart:argEnd]},
68
- )
52
+ reportBooleanCastFix(ctx, node, arg, message)
69
53
  case shimast.KindPrefixUnaryExpression:
70
54
  outer := node.AsPrefixUnaryExpression()
71
55
  if outer == nil || outer.Operator != shimast.KindExclamationToken {
@@ -83,28 +67,88 @@ func (noExtraBooleanCast) Check(ctx *Context, node *shimast.Node) {
83
67
  return
84
68
  }
85
69
  message := "Redundant double negation."
86
- if inner.Operand == nil {
87
- ctx.Report(node, message)
88
- return
89
- }
90
- src := ctx.File.Text()
91
- inStart := shimscanner.SkipTrivia(src, inner.Operand.Pos())
92
- inEnd := inner.Operand.End()
93
- if inStart < 0 || inStart >= inEnd || inEnd > len(src) {
94
- ctx.Report(node, message)
95
- return
70
+ reportBooleanCastFix(ctx, node, inner.Operand, message)
71
+ }
72
+ }
73
+
74
+ // reportBooleanCastFix reports a redundant boolean cast covering `node` and,
75
+ // when safe, attaches the autofix that splices the inner expression's source
76
+ // text over the whole cast. Two hazards can make the raw splice unsafe, and
77
+ // both mirror upstream ESLint's fixer (#362):
78
+ //
79
+ // - Precedence. In the two expression-level boolean contexts the rule
80
+ // accepts — `!` operand and ternary condition — a replacement that binds
81
+ // no tighter than the context re-associates: `!Boolean(a && b)` spliced
82
+ // bare becomes `!a && b`, which is `(!a) && b`. The replacement is
83
+ // wrapped in parentheses when the inner expression's precedence is at or
84
+ // below the context's floor. Statement conditions (`if`/`while`/`do`/
85
+ // `for`) need no wrap: the statement's own parentheses contain the
86
+ // result, as do explicit parentheses around the cast itself.
87
+ // - Comments. The splice keeps only the inner expression's text, so a
88
+ // comment anywhere else in the replaced span (`!Boolean(/* why */ ok)`)
89
+ // would be silently deleted. The fix is declined — report only — when
90
+ // the replaced span contains a comment outside the kept text.
91
+ func reportBooleanCastFix(ctx *Context, node, inner *shimast.Node, message string) {
92
+ if inner == nil {
93
+ ctx.Report(node, message)
94
+ return
95
+ }
96
+ src := ctx.File.Text()
97
+ keepStart := shimscanner.SkipTrivia(src, inner.Pos())
98
+ keepEnd := inner.End()
99
+ if keepStart < 0 || keepStart >= keepEnd || keepEnd > len(src) {
100
+ ctx.Report(node, message)
101
+ return
102
+ }
103
+ editPos := shimscanner.SkipTrivia(src, node.Pos())
104
+ if editPos < 0 || editPos >= node.End() || node.End() > len(src) {
105
+ ctx.Report(node, message)
106
+ return
107
+ }
108
+ if hasCommentBetween(src, editPos, keepStart) ||
109
+ hasCommentBetween(src, keepEnd, node.End()) {
110
+ ctx.Report(node, message)
111
+ return
112
+ }
113
+ text := src[keepStart:keepEnd]
114
+ if floor, bounded := booleanContextPrecedenceFloor(node); bounded &&
115
+ shimast.GetExpressionPrecedence(inner) <= floor {
116
+ text = "(" + text + ")"
117
+ }
118
+ ctx.ReportFix(node, message, TextEdit{Pos: editPos, End: node.End(), Text: text})
119
+ }
120
+
121
+ // booleanContextPrecedenceFloor returns the precedence at or below which a
122
+ // replacement spliced over `node` re-associates with its surroundings, and
123
+ // whether such a floor applies at all.
124
+ //
125
+ // Only the two expression-level boolean contexts are bounded: an `!` operand
126
+ // must bind tighter than a unary expression (`!(a && b)`, and the grammar
127
+ // even rejects `!x ** 2` outright), and a ternary condition must bind
128
+ // tighter than the conditional itself (`(x = f()) ? a : b`,
129
+ // `(a ? b : c) ? x : y`). The statement conditions `isInBooleanContext`
130
+ // accepts are unbounded — their mandatory parentheses already contain the
131
+ // replacement — and so is a cast explicitly wrapped in parentheses, because
132
+ // the direct parent is then the ParenthesizedExpression, not the `!` or
133
+ // ternary beyond it.
134
+ func booleanContextPrecedenceFloor(node *shimast.Node) (shimast.OperatorPrecedence, bool) {
135
+ parent := node.Parent
136
+ if parent == nil {
137
+ return shimast.OperatorPrecedenceInvalid, false
138
+ }
139
+ switch parent.Kind {
140
+ case shimast.KindPrefixUnaryExpression:
141
+ prefix := parent.AsPrefixUnaryExpression()
142
+ if prefix != nil && prefix.Operator == shimast.KindExclamationToken {
143
+ return shimast.OperatorPrecedenceUnary, true
96
144
  }
97
- editPos := shimscanner.SkipTrivia(src, node.Pos())
98
- if editPos < 0 || editPos >= node.End() {
99
- ctx.Report(node, message)
100
- return
145
+ case shimast.KindConditionalExpression:
146
+ cond := parent.AsConditionalExpression()
147
+ if cond != nil && cond.Condition == node {
148
+ return shimast.OperatorPrecedenceConditional, true
101
149
  }
102
- ctx.ReportFix(
103
- node,
104
- message,
105
- TextEdit{Pos: editPos, End: node.End(), Text: src[inStart:inEnd]},
106
- )
107
150
  }
151
+ return shimast.OperatorPrecedenceInvalid, false
108
152
  }
109
153
 
110
154
  // isInBooleanContext walks up the parent chain to determine whether the
@@ -267,7 +311,9 @@ func (useIsNaN) Check(ctx *Context, node *shimast.Node) {
267
311
  }
268
312
 
269
313
  // validTypeof: typeof expressions can only be compared to known type
270
- // strings. Catches `typeof x === "stirng"`.
314
+ // strings. Catches `typeof x === "stirng"`. Scoped to the four equality
315
+ // operators, like ESLint's own OPERATORS set — a relational comparison such as
316
+ // `typeof x < "m"` orders two strings rather than naming a type.
271
317
  // https://eslint.org/docs/latest/rules/valid-typeof
272
318
  type validTypeof struct{}
273
319
 
@@ -278,7 +324,7 @@ func (validTypeof) Check(ctx *Context, node *shimast.Node) {
278
324
  if expr == nil || expr.OperatorToken == nil {
279
325
  return
280
326
  }
281
- if !isComparisonOperator(expr.OperatorToken.Kind) {
327
+ if !isEqualityOperator(expr.OperatorToken.Kind) {
282
328
  return
283
329
  }
284
330
  left := stripParens(expr.Left)
@@ -79,32 +79,121 @@ func nextjsInlineScriptID(ctx *Context, file *shimast.Node) {
79
79
  }
80
80
 
81
81
  func nextjsNextScriptForGA(ctx *Context, file *shimast.Node) {
82
- imports := nextjsCollectImports(file)
83
- if len(imports.scriptFromNextScript) == 0 {
84
- return
85
- }
86
- walkDescendants(file, func(node *shimast.Node) {
87
- if node.Kind != shimast.KindJsxElement && node.Kind != shimast.KindJsxSelfClosingElement {
88
- return
89
- }
90
- opening := nextjsOpeningLike(node)
91
- if !imports.scriptFromNextScript[nextjsJSXName(opening)] {
82
+ nextjsWalkOpeningLike(file, func(opening *shimast.Node) {
83
+ if nextjsJSXName(opening) != "script" {
92
84
  return
93
85
  }
94
86
  src := nextjsJSXAttrString(opening, "src")
95
- if strings.Contains(src, "googletagmanager.com/gtag/js") || strings.Contains(src, "googletagmanager.com/gtm.js") {
87
+ if strings.Contains(src, "www.google-analytics.com/analytics.js") ||
88
+ strings.Contains(src, "www.googletagmanager.com/gtag/js") {
96
89
  ctx.Report(opening, "Use Next.js Google Analytics helpers instead of hand-written gtag scripts.")
97
90
  return
98
91
  }
99
- if node.Kind == shimast.KindJsxElement {
100
- text := nodeText(ctx.File, node)
101
- if strings.Contains(text, "gtag(") || strings.Contains(text, "GTM-") {
102
- ctx.Report(opening, "Use Next.js Google Analytics helpers instead of hand-written gtag scripts.")
103
- }
92
+ inline := nextjsDangerouslySetInnerHTMLSource(opening)
93
+ if strings.Contains(inline, "www.google-analytics.com/analytics.js") ||
94
+ strings.Contains(inline, "www.googletagmanager.com/gtm.js") {
95
+ ctx.Report(opening, "Use Next.js Google Analytics helpers instead of hand-written gtag scripts.")
104
96
  }
105
97
  })
106
98
  }
107
99
 
100
+ // nextjsDangerouslySetInnerHTMLSource returns the final static `__html`
101
+ // payload from `dangerouslySetInnerHTML={{ __html: "..." }}`. Object-literal
102
+ // properties are inspected from last to first so an earlier spread or dynamic
103
+ // property cannot obscure a later static assignment, while any later write
104
+ // that might replace `__html` keeps the payload unknown.
105
+ func nextjsDangerouslySetInnerHTMLSource(opening *shimast.Node) string {
106
+ attrNode := nextjsJSXAttr(opening, "dangerouslySetInnerHTML")
107
+ if attrNode == nil {
108
+ return ""
109
+ }
110
+ attr := attrNode.AsJsxAttribute()
111
+ if attr == nil || attr.Initializer == nil || attr.Initializer.Kind != shimast.KindJsxExpression {
112
+ return ""
113
+ }
114
+ container := attr.Initializer.AsJsxExpression()
115
+ if container == nil {
116
+ return ""
117
+ }
118
+ expression := stripParens(container.Expression)
119
+ if expression == nil || expression.Kind != shimast.KindObjectLiteralExpression {
120
+ return ""
121
+ }
122
+ object := expression.AsObjectLiteralExpression()
123
+ if object == nil || object.Properties == nil {
124
+ return ""
125
+ }
126
+ properties := object.Properties.Nodes
127
+ for i := len(properties) - 1; i >= 0; i-- {
128
+ propertyNode := properties[i]
129
+ if propertyNode == nil {
130
+ return ""
131
+ }
132
+ if propertyNode.Kind == shimast.KindSpreadAssignment {
133
+ return ""
134
+ }
135
+
136
+ switch propertyNode.Kind {
137
+ case shimast.KindPropertyAssignment:
138
+ property := propertyNode.AsPropertyAssignment()
139
+ if property == nil {
140
+ return ""
141
+ }
142
+ name, known := nextjsStaticPropertyName(property.Name())
143
+ if !known {
144
+ return ""
145
+ }
146
+ if name == "__html" {
147
+ return stringLiteralText(stripParens(property.Initializer))
148
+ }
149
+ case shimast.KindShorthandPropertyAssignment,
150
+ shimast.KindMethodDeclaration,
151
+ shimast.KindGetAccessor,
152
+ shimast.KindSetAccessor:
153
+ name, known := nextjsStaticPropertyName(propertyNode.Name())
154
+ if !known {
155
+ return ""
156
+ }
157
+ if name == "__html" {
158
+ return ""
159
+ }
160
+ default:
161
+ return ""
162
+ }
163
+ }
164
+ return ""
165
+ }
166
+
167
+ func nextjsStaticPropertyName(node *shimast.Node) (string, bool) {
168
+ if node == nil {
169
+ return "", false
170
+ }
171
+ switch node.Kind {
172
+ case shimast.KindIdentifier:
173
+ return identifierText(node), true
174
+ case shimast.KindStringLiteral, shimast.KindNoSubstitutionTemplateLiteral:
175
+ return stringLiteralText(node), true
176
+ case shimast.KindNumericLiteral, shimast.KindBigIntLiteral:
177
+ return numericLiteralText(node), true
178
+ case shimast.KindComputedPropertyName:
179
+ computed := node.AsComputedPropertyName()
180
+ if computed == nil {
181
+ return "", false
182
+ }
183
+ expression := stripParens(computed.Expression)
184
+ if expression == nil {
185
+ return "", false
186
+ }
187
+ switch expression.Kind {
188
+ case shimast.KindStringLiteral, shimast.KindNoSubstitutionTemplateLiteral:
189
+ return stringLiteralText(expression), true
190
+ case shimast.KindNumericLiteral, shimast.KindBigIntLiteral:
191
+ return numericLiteralText(expression), true
192
+ }
193
+ }
194
+ return "", false
195
+ }
196
+
108
197
  func nextjsNoAssignModuleVariable(ctx *Context, file *shimast.Node) {
109
198
  walkDescendants(file, func(node *shimast.Node) {
110
199
  if node.Kind != shimast.KindVariableDeclaration {
@@ -0,0 +1,70 @@
1
+ // noClassAssign rejects writes to bindings introduced by class declarations
2
+ // and named class expressions. It follows checker symbols instead of names,
3
+ // so references inside the class and in its surrounding scope resolve while
4
+ // parameter, catch, block, and sibling-scope shadows remain independent.
5
+ // https://eslint.org/docs/latest/rules/no-class-assign
6
+ package linthost
7
+
8
+ import shimast "github.com/microsoft/typescript-go/shim/ast"
9
+
10
+ type noClassAssign struct{}
11
+
12
+ func (noClassAssign) Name() string { return "no-class-assign" }
13
+ func (noClassAssign) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindSourceFile} }
14
+ func (noClassAssign) NeedsTypeChecker() bool { return true }
15
+ func (noClassAssign) Check(ctx *Context, node *shimast.Node) {
16
+ if ctx == nil || ctx.Checker == nil || node == nil {
17
+ return
18
+ }
19
+
20
+ classSymbols := make(map[*shimast.Symbol]struct{})
21
+ walkDescendants(node, func(child *shimast.Node) {
22
+ name := noClassAssignDeclarationName(child)
23
+ symbol := canonicalValueSymbol(ctx, name)
24
+ if symbol != nil {
25
+ classSymbols[symbol] = struct{}{}
26
+ }
27
+ })
28
+ if len(classSymbols) == 0 {
29
+ return
30
+ }
31
+
32
+ reported := make(map[*shimast.Node]struct{})
33
+ walkDescendants(node, func(child *shimast.Node) {
34
+ for _, target := range writeTargetIdentifiers(child) {
35
+ if _, duplicate := reported[target]; duplicate {
36
+ continue
37
+ }
38
+ symbol := canonicalValueSymbol(ctx, target)
39
+ if _, isClass := classSymbols[symbol]; !isClass {
40
+ continue
41
+ }
42
+ reported[target] = struct{}{}
43
+ ctx.Report(target, "'"+identifierText(target)+"' is a class.")
44
+ }
45
+ })
46
+ }
47
+
48
+ // noClassAssignDeclarationName returns the value-binding name introduced by a
49
+ // class declaration or named class expression. Anonymous expressions and
50
+ // anonymous default-export declarations introduce no writable local name.
51
+ func noClassAssignDeclarationName(node *shimast.Node) *shimast.Node {
52
+ if node == nil {
53
+ return nil
54
+ }
55
+ switch node.Kind {
56
+ case shimast.KindClassDeclaration:
57
+ if declaration := node.AsClassDeclaration(); declaration != nil {
58
+ return declaration.Name()
59
+ }
60
+ case shimast.KindClassExpression:
61
+ if expression := node.AsClassExpression(); expression != nil {
62
+ return expression.Name()
63
+ }
64
+ }
65
+ return nil
66
+ }
67
+
68
+ func init() {
69
+ Register(noClassAssign{})
70
+ }