@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
@@ -1,60 +1,346 @@
1
- // AST-only baseline of typescript-eslint's `no-restricted-types`.
2
- //
3
- // The upstream rule is fully configurable users supply a map from
4
- // banned type name to a custom message and (optionally) a rewrite hint.
5
- // Without the rule-options plumbing, this baseline ships a fixed
6
- // default set that covers the five built-in wrapper / global types
7
- // that almost every team forbids: `Object`, `Function`, `Number`,
8
- // `String`, and `Boolean`. The dedicated `typescript/no-unsafe-function-type`
9
- // and `typescript/no-wrapper-object-types` rules also cover some of
10
- // these, but `no-restricted-types` is the documented umbrella users
11
- // reach for when they want a single "ban these names" knob.
1
+ // typescript/no-restricted-types implements typescript-eslint's configurable
2
+ // spelling policy for type syntax. The rule has no implicit restrictions: a
3
+ // type is rejected only when its whitespace-normalized source spelling is an
4
+ // enabled key in the configured `types` map.
12
5
  //
13
6
  // https://typescript-eslint.io/rules/no-restricted-types/
14
7
  package linthost
15
8
 
16
9
  import (
10
+ "bytes"
11
+ "encoding/json"
12
+ "errors"
13
+ "fmt"
14
+ "io"
15
+ "strings"
16
+ "unicode"
17
+
17
18
  shimast "github.com/microsoft/typescript-go/shim/ast"
18
19
  )
19
20
 
20
- type noRestrictedTypes struct{}
21
+ type noRestrictedTypes struct{ optionsRule }
22
+
23
+ type noRestrictedTypeConfig struct {
24
+ enabled bool
25
+ message string
26
+ fixWith string
27
+ suggestions []string
28
+ }
21
29
 
22
- // noRestrictedTypesDefault is the default set surfaced by the rule
23
- // when no config is supplied. Each entry pairs the banned identifier
24
- // with a short, actionable replacement hint.
25
- var noRestrictedTypesDefault = map[string]string{
26
- "Object": "Use `object` (any non-primitive) or a specific shape like `Record<string, unknown>` instead of `Object`.",
27
- "Function": "Declare the specific call signature instead of the unsafe `Function` type.",
28
- "Number": "Use the lowercase primitive `number` instead of the wrapper type `Number`.",
29
- "String": "Use the lowercase primitive `string` instead of the wrapper type `String`.",
30
- "Boolean": "Use the lowercase primitive `boolean` instead of the wrapper type `Boolean`.",
30
+ type noRestrictedTypeObjectConfig struct {
31
+ Message json.RawMessage `json:"message"`
32
+ FixWith json.RawMessage `json:"fixWith"`
33
+ Suggest json.RawMessage `json:"suggest"`
31
34
  }
32
35
 
33
36
  func (noRestrictedTypes) Name() string { return "typescript/no-restricted-types" }
34
37
  func (noRestrictedTypes) Visits() []shimast.Kind {
35
- return []shimast.Kind{shimast.KindTypeReference}
38
+ // Decode the options once per file, then inspect only the upstream rule's
39
+ // supported type-syntax surfaces during one AST walk.
40
+ return []shimast.Kind{shimast.KindSourceFile}
36
41
  }
42
+
43
+ func (noRestrictedTypes) ValidateOptions(raw json.RawMessage) error {
44
+ _, err := decodeNoRestrictedTypesOptions(raw)
45
+ return err
46
+ }
47
+
37
48
  func (noRestrictedTypes) Check(ctx *Context, node *shimast.Node) {
38
- ref := node.AsTypeReferenceNode()
39
- if ref == nil || ref.TypeName == nil {
49
+ configured, err := decodeNoRestrictedTypesOptions(ctx.Options)
50
+ if err != nil || len(configured) == 0 {
51
+ // Real config-loading paths reject malformed settings through
52
+ // ValidateOptions. A directly constructed resolver still fails safely.
40
53
  return
41
54
  }
42
- name := identifierText(ref.TypeName)
43
- if name == "" {
55
+ walkDescendants(node, func(candidate *shimast.Node) {
56
+ checkNoRestrictedTypeNode(ctx, candidate, configured)
57
+ })
58
+ }
59
+
60
+ func checkNoRestrictedTypeNode(
61
+ ctx *Context,
62
+ node *shimast.Node,
63
+ configured map[string]noRestrictedTypeConfig,
64
+ ) {
65
+ if node == nil {
44
66
  return
45
67
  }
46
- message, banned := noRestrictedTypesDefault[name]
47
- if !banned {
68
+ switch node.Kind {
69
+ case shimast.KindTypeReference:
70
+ ref := node.AsTypeReferenceNode()
71
+ if ref == nil || ref.TypeName == nil {
72
+ return
73
+ }
74
+ reportRestrictedType(ctx, ref.TypeName, configured)
75
+ if ref.TypeArguments != nil && len(ref.TypeArguments.Nodes) != 0 {
76
+ reportRestrictedType(ctx, node, configured)
77
+ }
78
+ case shimast.KindExpressionWithTypeArguments:
79
+ if !isRestrictedTypeHeritage(node) {
80
+ return
81
+ }
82
+ heritage := node.AsExpressionWithTypeArguments()
83
+ if heritage == nil || heritage.Expression == nil {
84
+ return
85
+ }
86
+ reportRestrictedType(ctx, heritage.Expression, configured)
87
+ if heritage.TypeArguments != nil && len(heritage.TypeArguments.Nodes) != 0 {
88
+ reportRestrictedType(ctx, node, configured)
89
+ }
90
+ case shimast.KindTupleType:
91
+ tuple := node.AsTupleTypeNode()
92
+ if tuple != nil && tuple.Elements != nil && len(tuple.Elements.Nodes) == 0 {
93
+ reportRestrictedType(ctx, node, configured)
94
+ }
95
+ case shimast.KindTypeLiteral:
96
+ literal := node.AsTypeLiteralNode()
97
+ if literal != nil && literal.Members != nil && len(literal.Members.Nodes) == 0 {
98
+ reportRestrictedType(ctx, node, configured)
99
+ }
100
+ case shimast.KindNullKeyword:
101
+ // TypeScript-Go shares NullKeyword between the runtime literal and the
102
+ // token wrapped by a null LiteralType. typescript-estree exposes only the
103
+ // latter as TSNullKeyword to this rule.
104
+ if node.Parent != nil && node.Parent.Kind == shimast.KindLiteralType {
105
+ reportRestrictedType(ctx, node, configured)
106
+ }
107
+ case shimast.KindVoidKeyword:
108
+ // Exclude the runtime `void expression` operator; only the type keyword is
109
+ // a typescript-eslint TSVoidKeyword listener target.
110
+ if node.Parent == nil || node.Parent.Kind != shimast.KindVoidExpression {
111
+ reportRestrictedType(ctx, node, configured)
112
+ }
113
+ case shimast.KindBigIntKeyword,
114
+ shimast.KindBooleanKeyword,
115
+ shimast.KindNeverKeyword,
116
+ shimast.KindNumberKeyword,
117
+ shimast.KindObjectKeyword,
118
+ shimast.KindStringKeyword,
119
+ shimast.KindSymbolKeyword,
120
+ shimast.KindUndefinedKeyword,
121
+ shimast.KindUnknownKeyword:
122
+ reportRestrictedType(ctx, node, configured)
123
+ }
124
+ }
125
+
126
+ // isRestrictedTypeHeritage mirrors the two upstream listener surfaces:
127
+ // TSClassImplements and TSInterfaceHeritage. A class `extends` expression is a
128
+ // runtime value expression and is deliberately excluded, although type
129
+ // references nested in its type arguments are still visited independently.
130
+ func isRestrictedTypeHeritage(node *shimast.Node) bool {
131
+ clauseNode := node.Parent
132
+ if clauseNode == nil || clauseNode.Kind != shimast.KindHeritageClause {
133
+ return false
134
+ }
135
+ clause := clauseNode.AsHeritageClause()
136
+ if clause == nil {
137
+ return false
138
+ }
139
+ if clause.Token == shimast.KindImplementsKeyword {
140
+ return true
141
+ }
142
+ return clause.Token == shimast.KindExtendsKeyword &&
143
+ clauseNode.Parent != nil && clauseNode.Parent.Kind == shimast.KindInterfaceDeclaration
144
+ }
145
+
146
+ func reportRestrictedType(
147
+ ctx *Context,
148
+ node *shimast.Node,
149
+ configured map[string]noRestrictedTypeConfig,
150
+ ) {
151
+ name := normalizeRestrictedTypeName(nodeText(ctx.File, node))
152
+ restriction, ok := configured[name]
153
+ if name == "" || !ok || !restriction.enabled {
48
154
  return
49
155
  }
50
- // Shadow guard: if the user has declared a same-named `type` /
51
- // `interface` / `class` at file scope, their identifier is NOT the
52
- // global wrapper. Re-use the wrapper-types helper so the two rules
53
- // stay consistent about what counts as a shadow.
54
- if fileShadowsWrapperName(ctx.File, name) {
156
+
157
+ message := fmt.Sprintf("Don't use `%s` as a type.", name)
158
+ if restriction.message != "" {
159
+ message += " " + restriction.message
160
+ }
161
+ pos, end := tokenRange(ctx.File, node)
162
+ if pos < 0 {
163
+ ctx.Report(node, message)
55
164
  return
56
165
  }
57
- ctx.Report(node, message)
166
+
167
+ var fix []TextEdit
168
+ if restriction.fixWith != "" {
169
+ fix = []TextEdit{{Pos: pos, End: end, Text: restriction.fixWith}}
170
+ }
171
+ suggestions := make([]Suggestion, 0, len(restriction.suggestions))
172
+ for _, replacement := range restriction.suggestions {
173
+ suggestions = append(suggestions, Suggestion{
174
+ Title: fmt.Sprintf("Replace `%s` with `%s`.", name, replacement),
175
+ Edits: []TextEdit{{Pos: pos, End: end, Text: replacement}},
176
+ })
177
+ }
178
+ ctx.ReportFixSuggestions(node, message, fix, suggestions...)
179
+ }
180
+
181
+ // decodeNoRestrictedTypesOptions validates the complete public option shape
182
+ // and preserves object declaration order. Whitespace-equivalent keys collapse
183
+ // exactly as they do in the upstream Map constructor, with the later entry
184
+ // winning deterministically.
185
+ func decodeNoRestrictedTypesOptions(raw json.RawMessage) (map[string]noRestrictedTypeConfig, error) {
186
+ configured := map[string]noRestrictedTypeConfig{}
187
+ if len(bytes.TrimSpace(raw)) == 0 {
188
+ return configured, nil
189
+ }
190
+
191
+ decoder := json.NewDecoder(bytes.NewReader(raw))
192
+ token, err := decoder.Token()
193
+ if err != nil {
194
+ return nil, fmt.Errorf("options must be an object: %w", err)
195
+ }
196
+ if token != json.Delim('{') {
197
+ return nil, errors.New("options must be an object")
198
+ }
199
+
200
+ var types json.RawMessage
201
+ for decoder.More() {
202
+ token, err = decoder.Token()
203
+ if err != nil {
204
+ return nil, err
205
+ }
206
+ key, ok := token.(string)
207
+ if !ok {
208
+ return nil, errors.New("option keys must be strings")
209
+ }
210
+ if key != "types" {
211
+ return nil, fmt.Errorf("unknown option %q", key)
212
+ }
213
+ if err := decoder.Decode(&types); err != nil {
214
+ return nil, fmt.Errorf("decode options.types: %w", err)
215
+ }
216
+ }
217
+ if err := closeRestrictedTypesObject(decoder, "options"); err != nil {
218
+ return nil, err
219
+ }
220
+ if len(types) == 0 {
221
+ return configured, nil
222
+ }
223
+ return decodeRestrictedTypesMap(types)
224
+ }
225
+
226
+ func decodeRestrictedTypesMap(raw json.RawMessage) (map[string]noRestrictedTypeConfig, error) {
227
+ configured := map[string]noRestrictedTypeConfig{}
228
+ decoder := json.NewDecoder(bytes.NewReader(raw))
229
+ token, err := decoder.Token()
230
+ if err != nil {
231
+ return nil, fmt.Errorf("options.types must be an object: %w", err)
232
+ }
233
+ if token != json.Delim('{') {
234
+ return nil, errors.New("options.types must be an object")
235
+ }
236
+ for decoder.More() {
237
+ token, err = decoder.Token()
238
+ if err != nil {
239
+ return nil, err
240
+ }
241
+ key, ok := token.(string)
242
+ if !ok {
243
+ return nil, errors.New("options.types keys must be strings")
244
+ }
245
+ var value json.RawMessage
246
+ if err := decoder.Decode(&value); err != nil {
247
+ return nil, fmt.Errorf("decode options.types[%q]: %w", key, err)
248
+ }
249
+ restriction, err := decodeRestrictedTypeConfig(value)
250
+ if err != nil {
251
+ return nil, fmt.Errorf("options.types[%q]: %w", key, err)
252
+ }
253
+ configured[normalizeRestrictedTypeName(key)] = restriction
254
+ }
255
+ if err := closeRestrictedTypesObject(decoder, "options.types"); err != nil {
256
+ return nil, err
257
+ }
258
+ return configured, nil
259
+ }
260
+
261
+ func decodeRestrictedTypeConfig(raw json.RawMessage) (noRestrictedTypeConfig, error) {
262
+ trimmed := bytes.TrimSpace(raw)
263
+ if len(trimmed) == 0 {
264
+ return noRestrictedTypeConfig{}, errors.New("restriction must not be empty")
265
+ }
266
+ if bytes.Equal(trimmed, []byte("null")) {
267
+ return noRestrictedTypeConfig{}, nil
268
+ }
269
+ if bytes.Equal(trimmed, []byte("true")) || bytes.Equal(trimmed, []byte("false")) {
270
+ return noRestrictedTypeConfig{enabled: bytes.Equal(trimmed, []byte("true"))}, nil
271
+ }
272
+ if trimmed[0] == '"' {
273
+ var message string
274
+ if err := json.Unmarshal(trimmed, &message); err != nil {
275
+ return noRestrictedTypeConfig{}, errors.New("restriction must be a boolean, string, object, or null")
276
+ }
277
+ return noRestrictedTypeConfig{enabled: true, message: message}, nil
278
+ }
279
+ if trimmed[0] != '{' {
280
+ return noRestrictedTypeConfig{}, errors.New("restriction must be a boolean, string, object, or null")
281
+ }
282
+
283
+ decoder := json.NewDecoder(bytes.NewReader(trimmed))
284
+ decoder.DisallowUnknownFields()
285
+ var object noRestrictedTypeObjectConfig
286
+ if err := decoder.Decode(&object); err != nil {
287
+ return noRestrictedTypeConfig{}, fmt.Errorf("invalid restriction object: %w", err)
288
+ }
289
+ // The official runtime schema keeps message optional even though its
290
+ // exported TypeScript object type requires one. Empty, fix-only, and
291
+ // suggest-only objects therefore ban with the standard diagnostic.
292
+ var restriction noRestrictedTypeConfig
293
+ if len(object.Message) != 0 {
294
+ if !isRestrictedTypeJSONString(object.Message) ||
295
+ json.Unmarshal(object.Message, &restriction.message) != nil {
296
+ return noRestrictedTypeConfig{}, errors.New("restriction object message must be a string")
297
+ }
298
+ }
299
+ if len(object.FixWith) != 0 {
300
+ if !isRestrictedTypeJSONString(object.FixWith) ||
301
+ json.Unmarshal(object.FixWith, &restriction.fixWith) != nil {
302
+ return noRestrictedTypeConfig{}, errors.New("restriction object fixWith must be a string")
303
+ }
304
+ }
305
+ if len(object.Suggest) != 0 {
306
+ if bytes.Equal(bytes.TrimSpace(object.Suggest), []byte("null")) ||
307
+ json.Unmarshal(object.Suggest, &restriction.suggestions) != nil {
308
+ return noRestrictedTypeConfig{}, errors.New("restriction object suggest must be a string array")
309
+ }
310
+ }
311
+ restriction.enabled = true
312
+ return restriction, nil
313
+ }
314
+
315
+ func isRestrictedTypeJSONString(raw json.RawMessage) bool {
316
+ trimmed := bytes.TrimSpace(raw)
317
+ return len(trimmed) >= 2 && trimmed[0] == '"'
318
+ }
319
+
320
+ func closeRestrictedTypesObject(decoder *json.Decoder, label string) error {
321
+ token, err := decoder.Token()
322
+ if err != nil {
323
+ return err
324
+ }
325
+ if token != json.Delim('}') {
326
+ return fmt.Errorf("%s object is not closed", label)
327
+ }
328
+ if err := decoder.Decode(&struct{}{}); !errors.Is(err, io.EOF) {
329
+ if err != nil {
330
+ return err
331
+ }
332
+ return fmt.Errorf("%s contains trailing JSON", label)
333
+ }
334
+ return nil
335
+ }
336
+
337
+ func normalizeRestrictedTypeName(name string) string {
338
+ return strings.Map(func(r rune) rune {
339
+ if unicode.IsSpace(r) || r == '\ufeff' {
340
+ return -1
341
+ }
342
+ return r
343
+ }, name)
58
344
  }
59
345
 
60
346
  func init() {
@@ -1,8 +1,8 @@
1
1
  // typescript/no-unnecessary-qualifier: a qualified reference like
2
2
  // `Foo.Bar` written inside `namespace Foo { ... }` (or `enum Foo { ...,
3
- // X = Foo.Y, ... }`) names the same scope the reference is already in.
4
- // The qualifier adds no information; dropping `Foo.` leaves the
5
- // identical binding lookup. typescript-eslint:
3
+ // X = Foo.Y, ... }`) is redundant only when the unqualified name `Bar`
4
+ // resolves, from the same location, to the exact same binding the
5
+ // qualifier reaches. typescript-eslint:
6
6
  // https://typescript-eslint.io/rules/no-unnecessary-qualifier/
7
7
  package linthost
8
8
 
@@ -13,17 +13,36 @@ import (
13
13
  // noUnnecessaryQualifier inspects every two-segment qualified reference
14
14
  // whose head is a plain identifier — `QualifiedName` shows up in type
15
15
  // position (`Foo.Bar` inside a type annotation) and
16
- // `PropertyAccessExpression` shows up in value position. The check is
17
- // AST-only: it walks the ancestor chain for an enclosing `namespace
18
- // <head> { ... }` or `enum <head> { ... }` declaration and reports when
19
- // one is found. The rule deliberately does not consult the Checker
20
- // because the upstream rule operates on lexical scope identity, which
21
- // the AST already encodes via declaration ancestry.
16
+ // `PropertyAccessExpression` shows up in value position.
17
+ //
18
+ // The check is type-aware, mirroring the upstream rule's
19
+ // `qualifierIsUnnecessary`: text identity alone is not enough because a
20
+ // qualifier can be load-bearing. `Foo.` is redundant only when
21
+ //
22
+ // 1. the head identifier resolves to a namespace/enum symbol that is
23
+ // declared by one of the declarations enclosing the access
24
+ // (`symbolIsNamespaceInScope`), and
25
+ // 2. the member name, looked up unqualified from the head's location,
26
+ // resolves to the same symbol the qualified access reaches
27
+ // (`getSymbolsInScope` + export-symbol equality).
28
+ //
29
+ // The second clause is what keeps a shadowed member (`const bar` hiding
30
+ // `Foo.bar`) or a shadowed namespace name (`const Foo = { bar }`) from
31
+ // being reported — dropping such a qualifier would silently change which
32
+ // binding the code reads.
22
33
  type noUnnecessaryQualifier struct{}
23
34
 
24
35
  func (noUnnecessaryQualifier) Name() string {
25
36
  return "typescript/no-unnecessary-qualifier"
26
37
  }
38
+
39
+ // NeedsTypeChecker marks the rule type-aware: it resolves the head and
40
+ // member symbols and enumerates the in-scope bindings through
41
+ // Context.Checker, so the engine must acquire a checker for it.
42
+ func (noUnnecessaryQualifier) NeedsTypeChecker() bool {
43
+ return true
44
+ }
45
+
27
46
  func (noUnnecessaryQualifier) Visits() []shimast.Kind {
28
47
  return []shimast.Kind{
29
48
  shimast.KindQualifiedName,
@@ -31,88 +50,146 @@ func (noUnnecessaryQualifier) Visits() []shimast.Kind {
31
50
  }
32
51
  }
33
52
  func (noUnnecessaryQualifier) Check(ctx *Context, node *shimast.Node) {
34
- head := noUnnecessaryQualifierHead(node)
35
- if head == "" {
53
+ head, name := noUnnecessaryQualifierParts(node)
54
+ if head == nil || name == nil {
36
55
  return
37
56
  }
38
- if !noUnnecessaryQualifierHasEnclosingScope(node, head) {
57
+ if !noUnnecessaryQualifierIsUnnecessary(ctx, node, head, name) {
39
58
  return
40
59
  }
41
- ctx.Report(node, "Qualifier `"+head+"` is the enclosing namespace or enum — drop the qualifier.")
60
+ ctx.Report(node, "Qualifier `"+identifierText(head)+"` is the enclosing namespace or enum — drop the qualifier.")
42
61
  }
43
62
 
44
- // noUnnecessaryQualifierHead returns the LHS identifier text when the
45
- // node is a two-segment qualified reference (`A.B`) whose head is a
46
- // plain identifier. Returns "" for nested chains (`A.B.C`), computed
47
- // accesses, or non-identifier heads — those shapes don't match the
48
- // "qualifier names the enclosing scope" pattern.
49
- func noUnnecessaryQualifierHead(node *shimast.Node) string {
63
+ // noUnnecessaryQualifierParts returns the head (qualifier) and member
64
+ // name nodes of a two-segment qualified reference (`A.B`) whose head is a
65
+ // plain identifier. Returns (nil, nil) for nested chains (`A.B.C`),
66
+ // computed accesses, or non-identifier heads — those shapes don't match
67
+ // the "qualifier names the enclosing scope" pattern.
68
+ func noUnnecessaryQualifierParts(node *shimast.Node) (head *shimast.Node, name *shimast.Node) {
50
69
  if node == nil {
51
- return ""
70
+ return nil, nil
52
71
  }
53
72
  switch node.Kind {
54
73
  case shimast.KindQualifiedName:
55
74
  qn := node.AsQualifiedName()
56
- if qn == nil || qn.Left == nil {
57
- return ""
75
+ if qn == nil || qn.Left == nil || qn.Right == nil {
76
+ return nil, nil
58
77
  }
59
78
  if qn.Left.Kind != shimast.KindIdentifier {
60
- return ""
79
+ return nil, nil
61
80
  }
62
- return identifierText(qn.Left)
81
+ return qn.Left, qn.Right
63
82
  case shimast.KindPropertyAccessExpression:
64
83
  pa := node.AsPropertyAccessExpression()
65
84
  if pa == nil || pa.Expression == nil || pa.Name() == nil {
66
- return ""
85
+ return nil, nil
67
86
  }
68
87
  // Optional chains (`Foo?.Bar`) aren't redundant in the same
69
88
  // sense — the `?.` changes runtime behavior, so leave them be.
70
89
  if pa.QuestionDotToken != nil {
71
- return ""
90
+ return nil, nil
72
91
  }
73
92
  if pa.Expression.Kind != shimast.KindIdentifier {
74
- return ""
93
+ return nil, nil
75
94
  }
76
- return identifierText(pa.Expression)
95
+ return pa.Expression, pa.Name()
96
+ }
97
+ return nil, nil
98
+ }
99
+
100
+ // noUnnecessaryQualifierIsUnnecessary is the Go port of upstream
101
+ // `qualifierIsUnnecessary`. It reports whether dropping the `head.`
102
+ // qualifier leaves the identical binding lookup.
103
+ func noUnnecessaryQualifierIsUnnecessary(ctx *Context, node *shimast.Node, head *shimast.Node, name *shimast.Node) bool {
104
+ if ctx == nil || ctx.Checker == nil {
105
+ return false
106
+ }
107
+ // The cheap AST walk short-circuits before any checker call: the vast
108
+ // majority of qualified references in a program sit outside every
109
+ // namespace/enum, so an empty enclosing set rules them out without
110
+ // resolving a symbol.
111
+ enclosing := noUnnecessaryQualifierEnclosingNamespaces(node)
112
+ if len(enclosing) == 0 {
113
+ return false
77
114
  }
78
- return ""
115
+ namespaceSymbol := ctx.Checker.GetSymbolAtLocation(head)
116
+ if namespaceSymbol == nil {
117
+ return false
118
+ }
119
+ if !noUnnecessaryQualifierSymbolIsNamespaceInScope(ctx, enclosing, namespaceSymbol, 0) {
120
+ return false
121
+ }
122
+ accessedSymbol := ctx.Checker.GetSymbolAtLocation(name)
123
+ if accessedSymbol == nil {
124
+ return false
125
+ }
126
+ fromScope := noUnnecessaryQualifierSymbolInScope(ctx, head, accessedSymbol.Flags, identifierText(name))
127
+ if fromScope == nil {
128
+ return false
129
+ }
130
+ // Upstream `symbolsAreEqual`: the unqualified lookup must reach the
131
+ // same symbol as the qualified access. Normalizing the in-scope
132
+ // symbol to its export symbol collapses the local/export duality that
133
+ // a namespace or enum member carries.
134
+ return accessedSymbol == ctx.Checker.GetExportSymbolOfSymbol(fromScope)
79
135
  }
80
136
 
81
- // noUnnecessaryQualifierHasEnclosingScope reports whether `node`
82
- // appears inside a `namespace <head> { ... }` or `enum <head> { ... }`
83
- // declaration body. The walk climbs `Parent` links until it finds a
84
- // matching block or hits the SourceFile root.
85
- func noUnnecessaryQualifierHasEnclosingScope(node *shimast.Node, head string) bool {
137
+ // noUnnecessaryQualifierEnclosingNamespaces collects the `namespace`
138
+ // (`ModuleDeclaration`) and `enum` (`EnumDeclaration`) declaration nodes
139
+ // that enclose `node`, mirroring the upstream `namespacesInScope` stack.
140
+ // The walk climbs `Parent` links until it hits the SourceFile root.
141
+ func noUnnecessaryQualifierEnclosingNamespaces(node *shimast.Node) []*shimast.Node {
142
+ var enclosing []*shimast.Node
86
143
  for cur := node.Parent; cur != nil; cur = cur.Parent {
87
144
  switch cur.Kind {
88
- case shimast.KindModuleDeclaration:
89
- decl := cur.AsModuleDeclaration()
90
- if decl == nil || decl.Name() == nil {
91
- continue
92
- }
93
- // `declare module "fs"` carries a string-literal name and
94
- // does not bind an identifier in the surrounding scope —
95
- // skip those entirely. The rule only fires when the head
96
- // matches the enclosing namespace's identifier name.
97
- if decl.Name().Kind != shimast.KindIdentifier {
98
- continue
99
- }
100
- if identifierText(decl.Name()) == head {
101
- return true
102
- }
103
- case shimast.KindEnumDeclaration:
104
- decl := cur.AsEnumDeclaration()
105
- if decl == nil || decl.Name() == nil {
106
- continue
107
- }
108
- if identifierText(decl.Name()) == head {
145
+ case shimast.KindModuleDeclaration, shimast.KindEnumDeclaration:
146
+ enclosing = append(enclosing, cur)
147
+ }
148
+ }
149
+ return enclosing
150
+ }
151
+
152
+ // noUnnecessaryQualifierSymbolIsNamespaceInScope is the Go port of
153
+ // upstream `symbolIsNamespaceInScope`: it reports whether `symbol` is one
154
+ // of the namespaces/enums currently in scope by checking its declarations
155
+ // against `enclosing`, following aliases when the direct declarations
156
+ // don't match. The depth guard bounds pathological alias cycles.
157
+ func noUnnecessaryQualifierSymbolIsNamespaceInScope(ctx *Context, enclosing []*shimast.Node, symbol *shimast.Symbol, depth int) bool {
158
+ if symbol == nil || depth > 16 {
159
+ return false
160
+ }
161
+ for _, decl := range symbol.Declarations {
162
+ for _, ns := range enclosing {
163
+ if decl == ns {
109
164
  return true
110
165
  }
111
166
  }
112
167
  }
168
+ if symbol.Flags&shimast.SymbolFlagsAlias != 0 {
169
+ alias := ctx.Checker.GetAliasedSymbol(symbol)
170
+ if alias != nil && alias != symbol {
171
+ return noUnnecessaryQualifierSymbolIsNamespaceInScope(ctx, enclosing, alias, depth+1)
172
+ }
173
+ }
113
174
  return false
114
175
  }
115
176
 
177
+ // noUnnecessaryQualifierSymbolInScope is the Go port of upstream
178
+ // `getSymbolInScope`: it returns the first symbol named `name` that the
179
+ // checker reports as visible (with the given meaning `flags`) from the
180
+ // `head` location, or nil when the name is not in scope.
181
+ func noUnnecessaryQualifierSymbolInScope(ctx *Context, head *shimast.Node, flags shimast.SymbolFlags, name string) *shimast.Symbol {
182
+ if name == "" {
183
+ return nil
184
+ }
185
+ for _, symbol := range ctx.Checker.GetSymbolsInScope(head, flags) {
186
+ if symbol != nil && symbol.Name == name {
187
+ return symbol
188
+ }
189
+ }
190
+ return nil
191
+ }
192
+
116
193
  func init() {
117
194
  Register(noUnnecessaryQualifier{})
118
195
  }