@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.
- package/README.md +125 -43
- package/go.mod +5 -4
- package/internal/rulecode/rulecode.go +88 -0
- package/lib/index.d.ts +10 -1
- package/lib/index.js +45 -17
- package/lib/index.js.map +1 -1
- package/lib/structures/ITtscLintConfig.d.ts +3 -0
- package/lib/structures/TtscLintRuleSetting.d.ts +3 -2
- package/lib/structures/format/ITtscLintFormat.d.ts +3 -2
- package/lib/structures/format/ITtscLintFormatSortImports.d.ts +20 -4
- package/lib/structures/rules/ITtscLintBoundariesRuleOptions.d.ts +80 -8
- package/lib/structures/rules/ITtscLintBoundariesRules.d.ts +6 -8
- package/lib/structures/rules/ITtscLintContributorRules.d.ts +18 -2
- package/lib/structures/rules/ITtscLintCoreRuleOptions.d.ts +347 -0
- package/lib/structures/rules/ITtscLintCoreRuleOptions.js +3 -0
- package/lib/structures/rules/ITtscLintCoreRuleOptions.js.map +1 -0
- package/lib/structures/rules/ITtscLintCoreRules.d.ts +101 -67
- package/lib/structures/rules/ITtscLintRegexpRules.d.ts +7 -2
- package/lib/structures/rules/ITtscLintRuleOptionsMap.d.ts +53 -7
- package/lib/structures/rules/ITtscLintRules.d.ts +6 -2
- package/lib/structures/rules/ITtscLintSolidRules.d.ts +0 -8
- package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.d.ts +206 -0
- package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.js +8 -0
- package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.js.map +1 -0
- package/lib/structures/rules/ITtscLintTypeScriptRules.d.ts +55 -42
- package/lib/structures/rules/ITtscLintUnicornRuleOptions.d.ts +276 -0
- package/lib/structures/rules/ITtscLintUnicornRuleOptions.js +3 -0
- package/lib/structures/rules/ITtscLintUnicornRuleOptions.js.map +1 -0
- package/lib/structures/rules/ITtscLintUnicornRules.d.ts +44 -36
- package/lib/structures/rules/index.d.ts +3 -0
- package/lib/structures/rules/index.js +3 -0
- package/lib/structures/rules/index.js.map +1 -1
- package/linthost/ast_helpers.go +521 -23
- package/linthost/ast_selector_matcher.go +1628 -0
- package/linthost/ast_selector_parser.go +628 -0
- package/linthost/comment_scan.go +154 -0
- package/linthost/compile.go +129 -79
- package/linthost/config.go +352 -88
- package/linthost/config_format.go +15 -1
- package/linthost/contrib_adapter.go +47 -8
- package/linthost/declaration_rules.go +12 -11
- package/linthost/directives.go +9 -67
- package/linthost/dispatch.go +37 -3
- package/linthost/engine.go +368 -53
- package/linthost/escape_string.go +77 -0
- package/linthost/fix.go +133 -23
- package/linthost/flags_gen.go +18 -17
- package/linthost/format.go +73 -167
- package/linthost/format_editor_settings.go +134 -26
- package/linthost/grapheme_tables_gen.go +1920 -0
- package/linthost/host.go +159 -61
- package/linthost/literal_escape_scan.go +117 -0
- package/linthost/lsp.go +545 -98
- package/linthost/numeric_literal_precision.go +228 -0
- package/linthost/polyfill_browserslist.go +1439 -0
- package/linthost/polyfill_browserslist_config.go +603 -0
- package/linthost/polyfill_corejs.go +750 -0
- package/linthost/polyfill_data.go +113 -0
- package/linthost/polyfill_data_gen.json +39868 -0
- package/linthost/print_nodes_array.go +7 -1
- package/linthost/print_nodes_object.go +7 -1
- package/linthost/project_engine.go +231 -0
- package/linthost/project_rules.go +78 -0
- package/linthost/regex_clean.go +191 -0
- package/linthost/regex_tree.go +1544 -0
- package/linthost/regex_tree_optimizer.go +1685 -0
- package/linthost/regexp_cache.go +44 -0
- package/linthost/rule_codes.go +105 -0
- package/linthost/rule_codes.json +745 -0
- package/linthost/rules_ban_ts_comment.go +312 -0
- package/linthost/rules_boundaries.go +128 -43
- package/linthost/rules_boundaries_dependencies.go +1013 -0
- package/linthost/rules_core_extra.go +62 -92
- package/linthost/rules_cypress.go +1 -1
- package/linthost/rules_default_case.go +87 -11
- package/linthost/rules_dupes.go +38 -13
- package/linthost/rules_empty.go +188 -20
- package/linthost/rules_escape.go +62 -48
- package/linthost/rules_format_arrow_parens.go +19 -5
- package/linthost/rules_format_bracket_spacing.go +1 -1
- package/linthost/rules_format_clause_join.go +1 -1
- package/linthost/rules_format_declaration_header.go +31 -19
- package/linthost/rules_format_indent.go +1 -1
- package/linthost/rules_format_jsdoc.go +1 -1
- package/linthost/rules_format_orphan_semi.go +1 -1
- package/linthost/rules_format_parameter_properties.go +17 -7
- package/linthost/rules_format_print_width.go +1 -1
- package/linthost/rules_format_quote_props.go +1 -1
- package/linthost/rules_format_quotes.go +1 -1
- package/linthost/rules_format_semi.go +106 -79
- package/linthost/rules_format_sort_imports.go +79 -45
- package/linthost/rules_format_statement_split.go +1 -1
- package/linthost/rules_format_ternary_nullish_parens.go +1 -1
- package/linthost/rules_format_trailing_comma.go +64 -1
- package/linthost/rules_format_whitespace.go +1 -1
- package/linthost/rules_functional.go +21 -22
- package/linthost/rules_gap.go +80 -3
- package/linthost/rules_grouped_accessor_pairs.go +84 -25
- package/linthost/rules_guard_for_in.go +64 -147
- package/linthost/rules_jsx_a11y.go +232 -22
- package/linthost/rules_logic.go +84 -38
- package/linthost/rules_nextjs.go +105 -16
- package/linthost/rules_no_class_assign.go +70 -0
- package/linthost/rules_no_duplicate_imports.go +266 -0
- package/linthost/rules_no_else_return.go +156 -38
- package/linthost/rules_no_extend_native.go +113 -17
- package/linthost/rules_no_fallthrough.go +1013 -0
- package/linthost/rules_no_func_assign.go +70 -0
- package/linthost/rules_no_import_assign.go +278 -86
- package/linthost/rules_no_inner_declarations.go +186 -0
- package/linthost/rules_no_loop_func.go +438 -53
- package/linthost/rules_no_mixed_operators.go +208 -52
- package/linthost/rules_no_param_reassign.go +376 -61
- package/linthost/rules_no_promise_executor_return.go +156 -0
- package/linthost/rules_no_restricted_imports.go +1013 -57
- package/linthost/rules_no_restricted_syntax.go +190 -26
- package/linthost/rules_no_useless_constructor.go +6 -36
- package/linthost/rules_problems.go +154 -311
- package/linthost/rules_promise.go +2578 -109
- package/linthost/rules_react_extras.go +71 -45
- package/linthost/rules_react_hooks.go +18 -2
- package/linthost/rules_react_perf.go +1 -0
- package/linthost/rules_react_refresh.go +1 -1
- package/linthost/rules_regexp.go +230 -14
- package/linthost/rules_security.go +45 -19
- package/linthost/rules_self.go +21 -1
- package/linthost/rules_solid.go +13 -7
- package/linthost/rules_storybook.go +1 -1
- package/linthost/rules_suggestions.go +490 -84
- package/linthost/rules_testing_library.go +12 -1
- package/linthost/rules_ts.go +227 -54
- package/linthost/rules_ts_async.go +0 -131
- package/linthost/rules_ts_no_base_to_string.go +3 -4
- package/linthost/rules_ts_no_misused_promises.go +817 -0
- package/linthost/rules_ts_no_restricted_types.go +320 -34
- package/linthost/rules_ts_no_unnecessary_qualifier.go +132 -55
- package/linthost/rules_ts_no_unsafe_assignment.go +608 -60
- package/linthost/rules_ts_require_array_sort_compare.go +5 -5
- package/linthost/rules_ts_switch_exhaustiveness_check.go +556 -86
- package/linthost/rules_unicorn_better_regex.go +224 -16
- package/linthost/rules_unicorn_consistent_destructuring.go +1002 -7
- package/linthost/rules_unicorn_consistent_existence_index_check.go +208 -29
- package/linthost/rules_unicorn_consistent_function_scoping.go +1041 -8
- package/linthost/rules_unicorn_consistent_template_literal_escape.go +178 -7
- package/linthost/rules_unicorn_escape_case.go +30 -19
- package/linthost/rules_unicorn_filename_case.go +763 -16
- package/linthost/rules_unicorn_import_style.go +783 -17
- package/linthost/rules_unicorn_isolated_functions.go +934 -14
- package/linthost/rules_unicorn_no_hex_escape.go +26 -11
- package/linthost/rules_unicorn_no_typeof_undefined.go +234 -25
- package/linthost/rules_unicorn_no_unnecessary_polyfills.go +376 -21
- package/linthost/rules_unicorn_no_unused_properties.go +756 -19
- package/linthost/rules_unicorn_number_literal_case.go +58 -39
- package/linthost/rules_unicorn_prefer_at.go +7 -3
- package/linthost/rules_unicorn_prefer_number_properties.go +381 -48
- package/linthost/rules_unicorn_prefer_optional_catch_binding.go +24 -24
- package/linthost/rules_unicorn_prefer_simple_condition_first.go +597 -19
- package/linthost/rules_unicorn_prefer_string_raw.go +147 -16
- package/linthost/rules_unicorn_prevent_abbreviations.go +1963 -95
- package/linthost/rules_unicorn_string_content.go +611 -15
- package/linthost/rules_unicorn_switch_case_break_position.go +172 -15
- package/linthost/rules_unicorn_template_indent.go +923 -18
- package/linthost/rules_unicorn_text_encoding_identifier_case.go +202 -42
- package/linthost/rules_unicorn_throw_new_error.go +218 -26
- package/linthost/rules_var.go +829 -142
- package/linthost/string_length.go +222 -0
- package/package.json +4 -3
- package/rule/project.go +206 -0
- package/rule/rule.go +58 -1
- package/src/index.ts +58 -17
- package/src/structures/ITtscLintConfig.ts +3 -0
- package/src/structures/TtscLintRuleSetting.ts +3 -2
- package/src/structures/format/ITtscLintFormat.ts +3 -2
- package/src/structures/format/ITtscLintFormatSortImports.ts +21 -4
- package/src/structures/rules/ITtscLintBoundariesRuleOptions.ts +122 -8
- package/src/structures/rules/ITtscLintBoundariesRules.ts +6 -8
- package/src/structures/rules/ITtscLintContributorRules.ts +18 -2
- package/src/structures/rules/ITtscLintCoreRuleOptions.ts +465 -0
- package/src/structures/rules/ITtscLintCoreRules.ts +122 -67
- package/src/structures/rules/ITtscLintRegexpRules.ts +7 -2
- package/src/structures/rules/ITtscLintRuleOptionsMap.ts +88 -7
- package/src/structures/rules/ITtscLintRules.ts +7 -2
- package/src/structures/rules/ITtscLintSolidRules.ts +0 -9
- package/src/structures/rules/ITtscLintTypeScriptRuleOptions.ts +248 -0
- package/src/structures/rules/ITtscLintTypeScriptRules.ts +64 -42
- package/src/structures/rules/ITtscLintUnicornRuleOptions.ts +350 -0
- package/src/structures/rules/ITtscLintUnicornRules.ts +60 -36
- package/src/structures/rules/index.ts +3 -0
|
@@ -1,60 +1,346 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
39
|
-
if
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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, ... }`)
|
|
4
|
-
//
|
|
5
|
-
//
|
|
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.
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
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 :=
|
|
35
|
-
if head ==
|
|
53
|
+
head, name := noUnnecessaryQualifierParts(node)
|
|
54
|
+
if head == nil || name == nil {
|
|
36
55
|
return
|
|
37
56
|
}
|
|
38
|
-
if !
|
|
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
|
-
//
|
|
45
|
-
//
|
|
46
|
-
// plain identifier. Returns
|
|
47
|
-
// accesses, or non-identifier heads — those shapes don't match
|
|
48
|
-
// "qualifier names the enclosing scope" pattern.
|
|
49
|
-
func
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
func
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
}
|