@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,33 +1,52 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// typescript-eslint strict-type-checked:
|
|
1
|
+
// typescript/switch-exhaustiveness-check: require switches over finite
|
|
2
|
+
// discriminants to cover every enumerable member. The implementation mirrors
|
|
3
|
+
// typescript-eslint's scalar defaults and option interactions while using the
|
|
4
|
+
// TypeScript-Go Checker as the type oracle.
|
|
5
|
+
//
|
|
6
|
+
// Reference:
|
|
8
7
|
// https://typescript-eslint.io/rules/switch-exhaustiveness-check/
|
|
9
8
|
package linthost
|
|
10
9
|
|
|
11
10
|
import (
|
|
11
|
+
"regexp"
|
|
12
|
+
"strings"
|
|
13
|
+
|
|
12
14
|
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
13
15
|
shimchecker "github.com/microsoft/typescript-go/shim/checker"
|
|
16
|
+
shimscanner "github.com/microsoft/typescript-go/shim/scanner"
|
|
14
17
|
)
|
|
15
18
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
19
|
+
type switchExhaustivenessCheck struct{ optionsRule }
|
|
20
|
+
|
|
21
|
+
type switchExhaustivenessCheckOptions struct {
|
|
22
|
+
AllowDefaultCaseForExhaustiveSwitch *bool `json:"allowDefaultCaseForExhaustiveSwitch"`
|
|
23
|
+
ConsiderDefaultExhaustiveForUnions *bool `json:"considerDefaultExhaustiveForUnions"`
|
|
24
|
+
DefaultCaseCommentPattern *string `json:"defaultCaseCommentPattern"`
|
|
25
|
+
RequireDefaultForNonUnion *bool `json:"requireDefaultForNonUnion"`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type resolvedSwitchExhaustivenessCheckOptions struct {
|
|
29
|
+
allowDefaultCaseForExhaustiveSwitch bool
|
|
30
|
+
considerDefaultExhaustiveForUnions bool
|
|
31
|
+
defaultCaseCommentPattern *regexp.Regexp
|
|
32
|
+
requireDefaultForNonUnion bool
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type switchExhaustivenessCheckDefaultCase struct {
|
|
36
|
+
node *shimast.Node
|
|
37
|
+
pos int
|
|
38
|
+
end int
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type switchExhaustivenessCheckMetadata struct {
|
|
42
|
+
containsNonLiteralType bool
|
|
43
|
+
defaultCase *switchExhaustivenessCheckDefaultCase
|
|
44
|
+
caseBlock *shimast.Node
|
|
45
|
+
lastClause *shimast.Node
|
|
46
|
+
missingMembers []*shimchecker.Type
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
var switchExhaustivenessCheckDefaultCommentPattern = regexp.MustCompile(`(?i)^no default$`)
|
|
31
50
|
|
|
32
51
|
func (switchExhaustivenessCheck) Name() string { return "typescript/switch-exhaustiveness-check" }
|
|
33
52
|
func (switchExhaustivenessCheck) NeedsTypeChecker() bool {
|
|
@@ -40,106 +59,557 @@ func (switchExhaustivenessCheck) Check(ctx *Context, node *shimast.Node) {
|
|
|
40
59
|
if ctx.Checker == nil {
|
|
41
60
|
return
|
|
42
61
|
}
|
|
43
|
-
|
|
44
|
-
|
|
62
|
+
|
|
63
|
+
var rawOptions switchExhaustivenessCheckOptions
|
|
64
|
+
if err := ctx.DecodeOptions(&rawOptions); err != nil {
|
|
45
65
|
return
|
|
46
66
|
}
|
|
47
|
-
|
|
48
|
-
|
|
67
|
+
options := resolveSwitchExhaustivenessCheckOptions(rawOptions)
|
|
68
|
+
metadata := switchExhaustivenessCheckCollectMetadata(
|
|
69
|
+
ctx,
|
|
70
|
+
node,
|
|
71
|
+
options.defaultCaseCommentPattern,
|
|
72
|
+
)
|
|
73
|
+
if metadata == nil {
|
|
49
74
|
return
|
|
50
75
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
76
|
+
|
|
77
|
+
sw := node.AsSwitchStatement()
|
|
78
|
+
if sw == nil || sw.Expression == nil {
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if !(options.considerDefaultExhaustiveForUnions && metadata.defaultCase != nil) &&
|
|
83
|
+
len(metadata.missingMembers) > 0 {
|
|
84
|
+
missing := make([]string, 0, len(metadata.missingMembers))
|
|
85
|
+
for _, member := range metadata.missingMembers {
|
|
86
|
+
missing = append(missing, switchExhaustivenessCheckTypeName(ctx.Checker, sw.Expression, member))
|
|
87
|
+
}
|
|
88
|
+
message := "Switch is not exhaustive. Cases not matched: " + strings.Join(missing, " | ")
|
|
89
|
+
if edit, ok := switchExhaustivenessCheckSuggestion(
|
|
90
|
+
ctx,
|
|
91
|
+
node,
|
|
92
|
+
metadata,
|
|
93
|
+
metadata.missingMembers,
|
|
94
|
+
false,
|
|
95
|
+
); ok {
|
|
96
|
+
ctx.ReportSuggestion(sw.Expression, message, "Add branches for missing cases.", edit)
|
|
97
|
+
} else {
|
|
98
|
+
ctx.Report(sw.Expression, message)
|
|
57
99
|
}
|
|
58
100
|
}
|
|
59
|
-
|
|
60
|
-
if
|
|
61
|
-
|
|
101
|
+
|
|
102
|
+
if !options.allowDefaultCaseForExhaustiveSwitch &&
|
|
103
|
+
len(metadata.missingMembers) == 0 &&
|
|
104
|
+
metadata.defaultCase != nil &&
|
|
105
|
+
!metadata.containsNonLiteralType {
|
|
106
|
+
switchExhaustivenessCheckReportDefaultCase(
|
|
107
|
+
ctx,
|
|
108
|
+
metadata.defaultCase,
|
|
109
|
+
"The switch statement is exhaustive, so the default case is unnecessary.",
|
|
110
|
+
)
|
|
62
111
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
112
|
+
|
|
113
|
+
if options.requireDefaultForNonUnion &&
|
|
114
|
+
metadata.containsNonLiteralType &&
|
|
115
|
+
metadata.defaultCase == nil {
|
|
116
|
+
const message = "Switch is not exhaustive. Cases not matched: default"
|
|
117
|
+
if edit, ok := switchExhaustivenessCheckSuggestion(
|
|
118
|
+
ctx,
|
|
119
|
+
node,
|
|
120
|
+
metadata,
|
|
121
|
+
nil,
|
|
122
|
+
true,
|
|
123
|
+
); ok {
|
|
124
|
+
ctx.ReportSuggestion(sw.Expression, message, "Add a default branch.", edit)
|
|
125
|
+
} else {
|
|
126
|
+
ctx.Report(sw.Expression, message)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
func resolveSwitchExhaustivenessCheckOptions(
|
|
132
|
+
raw switchExhaustivenessCheckOptions,
|
|
133
|
+
) resolvedSwitchExhaustivenessCheckOptions {
|
|
134
|
+
resolved := resolvedSwitchExhaustivenessCheckOptions{
|
|
135
|
+
allowDefaultCaseForExhaustiveSwitch: true,
|
|
136
|
+
defaultCaseCommentPattern: switchExhaustivenessCheckDefaultCommentPattern,
|
|
137
|
+
}
|
|
138
|
+
if raw.AllowDefaultCaseForExhaustiveSwitch != nil {
|
|
139
|
+
resolved.allowDefaultCaseForExhaustiveSwitch = *raw.AllowDefaultCaseForExhaustiveSwitch
|
|
140
|
+
}
|
|
141
|
+
if raw.ConsiderDefaultExhaustiveForUnions != nil {
|
|
142
|
+
resolved.considerDefaultExhaustiveForUnions = *raw.ConsiderDefaultExhaustiveForUnions
|
|
143
|
+
}
|
|
144
|
+
if raw.RequireDefaultForNonUnion != nil {
|
|
145
|
+
resolved.requireDefaultForNonUnion = *raw.RequireDefaultForNonUnion
|
|
146
|
+
}
|
|
147
|
+
if raw.DefaultCaseCommentPattern != nil {
|
|
148
|
+
pattern, err := regexp.Compile(*raw.DefaultCaseCommentPattern)
|
|
149
|
+
if err != nil {
|
|
150
|
+
resolved.defaultCaseCommentPattern = nil
|
|
151
|
+
} else {
|
|
152
|
+
resolved.defaultCaseCommentPattern = pattern
|
|
153
|
+
}
|
|
69
154
|
}
|
|
70
|
-
|
|
155
|
+
return resolved
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// switchExhaustivenessCheckCollectMetadata resolves the discriminant and case
|
|
159
|
+
// expressions through their base constraints, then computes missing literal
|
|
160
|
+
// branches before applying any policy option. Open primitive constituents are
|
|
161
|
+
// recorded independently, so `string | undefined` keeps `undefined` enumerable
|
|
162
|
+
// without pretending the `string` portion can be exhausted.
|
|
163
|
+
func switchExhaustivenessCheckCollectMetadata(
|
|
164
|
+
ctx *Context,
|
|
165
|
+
node *shimast.Node,
|
|
166
|
+
commentPattern *regexp.Regexp,
|
|
167
|
+
) *switchExhaustivenessCheckMetadata {
|
|
168
|
+
sw := node.AsSwitchStatement()
|
|
169
|
+
if sw == nil || sw.Expression == nil || sw.CaseBlock == nil {
|
|
170
|
+
return nil
|
|
171
|
+
}
|
|
172
|
+
block := sw.CaseBlock.AsCaseBlock()
|
|
173
|
+
if block == nil || block.Clauses == nil {
|
|
174
|
+
return nil
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
discriminantType := switchExhaustivenessCheckConstrainedType(
|
|
178
|
+
ctx.Checker,
|
|
179
|
+
ctx.Checker.GetTypeAtLocation(sw.Expression),
|
|
180
|
+
)
|
|
181
|
+
if discriminantType == nil {
|
|
182
|
+
return nil
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
caseTypes := make([]*shimchecker.Type, 0, len(block.Clauses.Nodes))
|
|
186
|
+
var defaultCase *switchExhaustivenessCheckDefaultCase
|
|
187
|
+
var lastClause *shimast.Node
|
|
71
188
|
for _, clause := range block.Clauses.Nodes {
|
|
72
|
-
if clause == nil
|
|
189
|
+
if clause == nil {
|
|
190
|
+
continue
|
|
191
|
+
}
|
|
192
|
+
lastClause = clause
|
|
193
|
+
if clause.Kind == shimast.KindDefaultClause {
|
|
194
|
+
if defaultCase == nil {
|
|
195
|
+
pos, _ := tokenRange(ctx.File, clause)
|
|
196
|
+
defaultCase = &switchExhaustivenessCheckDefaultCase{node: clause, pos: pos}
|
|
197
|
+
}
|
|
198
|
+
continue
|
|
199
|
+
}
|
|
200
|
+
if clause.Kind != shimast.KindCaseClause {
|
|
73
201
|
continue
|
|
74
202
|
}
|
|
75
203
|
caseClause := clause.AsCaseOrDefaultClause()
|
|
76
204
|
if caseClause == nil || caseClause.Expression == nil {
|
|
77
205
|
continue
|
|
78
206
|
}
|
|
79
|
-
caseType :=
|
|
80
|
-
|
|
207
|
+
caseType := switchExhaustivenessCheckConstrainedType(
|
|
208
|
+
ctx.Checker,
|
|
209
|
+
ctx.Checker.GetTypeAtLocation(caseClause.Expression),
|
|
210
|
+
)
|
|
211
|
+
if caseType != nil {
|
|
212
|
+
caseTypes = append(caseTypes, caseType)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if defaultCase == nil {
|
|
216
|
+
defaultCase = switchExhaustivenessCheckCommentDefaultCase(
|
|
217
|
+
ctx,
|
|
218
|
+
sw.CaseBlock,
|
|
219
|
+
lastClause,
|
|
220
|
+
commentPattern,
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
members, containsNonLiteralType := switchExhaustivenessCheckTypeParts(ctx.Checker, discriminantType)
|
|
225
|
+
missing := make([]*shimchecker.Type, 0, len(members))
|
|
226
|
+
for _, member := range members {
|
|
227
|
+
if !switchExhaustivenessCheckCaseTypesCover(caseTypes, member) {
|
|
228
|
+
missing = append(missing, member)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return &switchExhaustivenessCheckMetadata{
|
|
232
|
+
containsNonLiteralType: containsNonLiteralType,
|
|
233
|
+
defaultCase: defaultCase,
|
|
234
|
+
caseBlock: sw.CaseBlock,
|
|
235
|
+
lastClause: lastClause,
|
|
236
|
+
missingMembers: missing,
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
func switchExhaustivenessCheckConstrainedType(
|
|
241
|
+
checker *shimchecker.Checker,
|
|
242
|
+
t *shimchecker.Type,
|
|
243
|
+
) *shimchecker.Type {
|
|
244
|
+
if checker == nil || t == nil {
|
|
245
|
+
return t
|
|
246
|
+
}
|
|
247
|
+
if constraint := checker.GetBaseConstraintOfType(t); constraint != nil {
|
|
248
|
+
t = constraint
|
|
249
|
+
}
|
|
250
|
+
return shimchecker.Checker_getRegularTypeOfLiteralType(checker, t)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// switchExhaustivenessCheckTypeParts mirrors typescript-eslint's nested
|
|
254
|
+
// unionConstituents/intersectionConstituents walk. Literal-like intersection
|
|
255
|
+
// pieces remain enumerable even when another piece is open-ended. A union part
|
|
256
|
+
// is considered non-literal only when every one of its intersection pieces is
|
|
257
|
+
// non-literal.
|
|
258
|
+
func switchExhaustivenessCheckTypeParts(
|
|
259
|
+
checker *shimchecker.Checker,
|
|
260
|
+
t *shimchecker.Type,
|
|
261
|
+
) ([]*shimchecker.Type, bool) {
|
|
262
|
+
if t == nil {
|
|
263
|
+
return nil, false
|
|
264
|
+
}
|
|
265
|
+
unionParts := []*shimchecker.Type{t}
|
|
266
|
+
if t.Flags()&shimchecker.TypeFlagsUnion != 0 {
|
|
267
|
+
unionParts = t.Types()
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
members := make([]*shimchecker.Type, 0, len(unionParts))
|
|
271
|
+
seen := make(map[*shimchecker.Type]struct{}, len(unionParts))
|
|
272
|
+
containsNonLiteralType := false
|
|
273
|
+
for _, unionPart := range unionParts {
|
|
274
|
+
if unionPart == nil {
|
|
81
275
|
continue
|
|
82
276
|
}
|
|
83
|
-
|
|
277
|
+
intersectionParts := []*shimchecker.Type{unionPart}
|
|
278
|
+
if unionPart.Flags()&shimchecker.TypeFlagsIntersection != 0 {
|
|
279
|
+
intersectionParts = unionPart.Types()
|
|
280
|
+
}
|
|
281
|
+
partContainsLiteral := false
|
|
282
|
+
for _, intersectionPart := range intersectionParts {
|
|
283
|
+
intersectionPart = shimchecker.Checker_getRegularTypeOfLiteralType(checker, intersectionPart)
|
|
284
|
+
if !switchExhaustivenessCheckIsLiteralLike(intersectionPart) {
|
|
285
|
+
continue
|
|
286
|
+
}
|
|
287
|
+
partContainsLiteral = true
|
|
288
|
+
if switchExhaustivenessCheckContainsEquivalentMember(members, intersectionPart) {
|
|
289
|
+
continue
|
|
290
|
+
}
|
|
291
|
+
if _, ok := seen[intersectionPart]; ok {
|
|
292
|
+
continue
|
|
293
|
+
}
|
|
294
|
+
seen[intersectionPart] = struct{}{}
|
|
295
|
+
members = append(members, intersectionPart)
|
|
296
|
+
}
|
|
297
|
+
if !partContainsLiteral {
|
|
298
|
+
containsNonLiteralType = true
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return members, containsNonLiteralType
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
func switchExhaustivenessCheckContainsEquivalentMember(
|
|
305
|
+
members []*shimchecker.Type,
|
|
306
|
+
candidate *shimchecker.Type,
|
|
307
|
+
) bool {
|
|
308
|
+
if candidate == nil || candidate.Flags()&shimchecker.TypeFlagsUndefined == 0 {
|
|
309
|
+
return false
|
|
84
310
|
}
|
|
85
|
-
missing := 0
|
|
86
311
|
for _, member := range members {
|
|
87
|
-
if
|
|
88
|
-
|
|
312
|
+
if member != nil && member.Flags()&shimchecker.TypeFlagsUndefined != 0 {
|
|
313
|
+
return true
|
|
89
314
|
}
|
|
90
315
|
}
|
|
91
|
-
|
|
92
|
-
|
|
316
|
+
return false
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
func switchExhaustivenessCheckCaseTypesCover(
|
|
320
|
+
caseTypes []*shimchecker.Type,
|
|
321
|
+
member *shimchecker.Type,
|
|
322
|
+
) bool {
|
|
323
|
+
if member == nil {
|
|
324
|
+
return false
|
|
93
325
|
}
|
|
94
|
-
|
|
326
|
+
for _, caseType := range caseTypes {
|
|
327
|
+
if caseType == member {
|
|
328
|
+
return true
|
|
329
|
+
}
|
|
330
|
+
// TypeScript represents `undefined`, optional, and no-unchecked-indexed-
|
|
331
|
+
// access missing types with distinct pointers. They all carry the
|
|
332
|
+
// Undefined flag and denote the same runtime switch value.
|
|
333
|
+
if caseType != nil &&
|
|
334
|
+
caseType.Flags()&shimchecker.TypeFlagsUndefined != 0 &&
|
|
335
|
+
member.Flags()&shimchecker.TypeFlagsUndefined != 0 {
|
|
336
|
+
return true
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return false
|
|
95
340
|
}
|
|
96
341
|
|
|
97
|
-
|
|
98
|
-
// enum-literal constituents of t when t is a discriminable union, and
|
|
99
|
-
// nil when t is open-ended (plain `string`, `number`, `any`, `unknown`,
|
|
100
|
-
// etc.). A union whose constituents are all literals or enum literals
|
|
101
|
-
// is treated as exhaustively coverable; any non-literal constituent
|
|
102
|
-
// (e.g. `string` mixed into `"a" | "b" | string`) widens the union and
|
|
103
|
-
// disqualifies the whole discriminant.
|
|
104
|
-
func switchExhaustivenessCheckCollectMembers(t *shimchecker.Type) []*shimchecker.Type {
|
|
342
|
+
func switchExhaustivenessCheckIsLiteralLike(t *shimchecker.Type) bool {
|
|
105
343
|
if t == nil {
|
|
344
|
+
return false
|
|
345
|
+
}
|
|
346
|
+
return t.Flags()&(shimchecker.TypeFlagsLiteral|
|
|
347
|
+
shimchecker.TypeFlagsUndefined|
|
|
348
|
+
shimchecker.TypeFlagsNull|
|
|
349
|
+
shimchecker.TypeFlagsUniqueESSymbol) != 0
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
func switchExhaustivenessCheckTypeName(
|
|
353
|
+
checker *shimchecker.Checker,
|
|
354
|
+
location *shimast.Node,
|
|
355
|
+
t *shimchecker.Type,
|
|
356
|
+
) string {
|
|
357
|
+
if t == nil {
|
|
358
|
+
return "unknown"
|
|
359
|
+
}
|
|
360
|
+
if t.Flags()&shimchecker.TypeFlagsESSymbolLike != 0 {
|
|
361
|
+
if symbol := t.Symbol(); symbol != nil {
|
|
362
|
+
if name := shimchecker.Checker_symbolToValueString(checker, symbol, location); name != "" {
|
|
363
|
+
return "typeof " + name
|
|
364
|
+
}
|
|
365
|
+
if symbol.Name != "" {
|
|
366
|
+
return "typeof " + symbol.Name
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if name := shimchecker.Checker_typeToStringFullyQualified(checker, t, location); name != "" {
|
|
371
|
+
return name
|
|
372
|
+
}
|
|
373
|
+
return checker.TypeToString(t)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
func switchExhaustivenessCheckSuggestion(
|
|
377
|
+
ctx *Context,
|
|
378
|
+
switchNode *shimast.Node,
|
|
379
|
+
metadata *switchExhaustivenessCheckMetadata,
|
|
380
|
+
members []*shimchecker.Type,
|
|
381
|
+
includeDefault bool,
|
|
382
|
+
) (TextEdit, bool) {
|
|
383
|
+
if ctx == nil || ctx.File == nil || ctx.Checker == nil ||
|
|
384
|
+
switchNode == nil || metadata == nil {
|
|
385
|
+
return TextEdit{}, false
|
|
386
|
+
}
|
|
387
|
+
sw := switchNode.AsSwitchStatement()
|
|
388
|
+
if sw == nil || sw.Expression == nil {
|
|
389
|
+
return TextEdit{}, false
|
|
390
|
+
}
|
|
391
|
+
branches := make([]string, 0, len(members)+1)
|
|
392
|
+
for _, member := range members {
|
|
393
|
+
expression, ok := switchExhaustivenessCheckCaseExpression(
|
|
394
|
+
ctx.Checker,
|
|
395
|
+
sw.Expression,
|
|
396
|
+
member,
|
|
397
|
+
)
|
|
398
|
+
if !ok {
|
|
399
|
+
return TextEdit{}, false
|
|
400
|
+
}
|
|
401
|
+
branches = append(
|
|
402
|
+
branches,
|
|
403
|
+
`case `+expression+`: { throw new Error("Not implemented yet"); }`,
|
|
404
|
+
)
|
|
405
|
+
}
|
|
406
|
+
if includeDefault {
|
|
407
|
+
branches = append(branches, `default: { throw new Error("Not implemented yet"); }`)
|
|
408
|
+
}
|
|
409
|
+
if len(branches) == 0 {
|
|
410
|
+
return TextEdit{}, false
|
|
411
|
+
}
|
|
412
|
+
return switchExhaustivenessCheckInsertionEdit(ctx.File, switchNode, metadata, branches)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
func switchExhaustivenessCheckCaseExpression(
|
|
416
|
+
checker *shimchecker.Checker,
|
|
417
|
+
location *shimast.Node,
|
|
418
|
+
member *shimchecker.Type,
|
|
419
|
+
) (string, bool) {
|
|
420
|
+
if checker == nil || location == nil || member == nil {
|
|
421
|
+
return "", false
|
|
422
|
+
}
|
|
423
|
+
if symbol := member.Symbol(); symbol != nil {
|
|
424
|
+
if !shimchecker.Checker_isSymbolAccessibleAsValue(checker, symbol, location) {
|
|
425
|
+
return "", false
|
|
426
|
+
}
|
|
427
|
+
expression := strings.TrimSpace(
|
|
428
|
+
shimchecker.Checker_symbolToValueString(checker, symbol, location),
|
|
429
|
+
)
|
|
430
|
+
return expression, expression != ""
|
|
431
|
+
}
|
|
432
|
+
expression := strings.TrimSpace(
|
|
433
|
+
shimchecker.Checker_typeToStringFullyQualified(checker, member, location),
|
|
434
|
+
)
|
|
435
|
+
return expression, expression != ""
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
func switchExhaustivenessCheckInsertionEdit(
|
|
439
|
+
file *shimast.SourceFile,
|
|
440
|
+
switchNode *shimast.Node,
|
|
441
|
+
metadata *switchExhaustivenessCheckMetadata,
|
|
442
|
+
branches []string,
|
|
443
|
+
) (TextEdit, bool) {
|
|
444
|
+
if file == nil || switchNode == nil || metadata == nil || len(branches) == 0 {
|
|
445
|
+
return TextEdit{}, false
|
|
446
|
+
}
|
|
447
|
+
source := file.Text()
|
|
448
|
+
newline := "\n"
|
|
449
|
+
if strings.Contains(source, "\r\n") {
|
|
450
|
+
newline = "\r\n"
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if metadata.defaultCase != nil &&
|
|
454
|
+
metadata.defaultCase.pos >= 0 && metadata.defaultCase.pos <= len(source) {
|
|
455
|
+
pos := metadata.defaultCase.pos
|
|
456
|
+
indent := switchExhaustivenessCheckLineIndent(source, pos)
|
|
457
|
+
text := strings.Join(branches, newline+indent) + newline + indent
|
|
458
|
+
if !switchExhaustivenessCheckLineHasOnlyIndent(source, pos) {
|
|
459
|
+
indent = switchExhaustivenessCheckCaseIndent(source, file, switchNode, metadata)
|
|
460
|
+
text = newline + indent + strings.Join(branches, newline+indent) + newline + indent
|
|
461
|
+
}
|
|
462
|
+
return TextEdit{Pos: pos, End: pos, Text: text}, true
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if metadata.lastClause != nil {
|
|
466
|
+
pos := metadata.lastClause.End()
|
|
467
|
+
if pos < 0 || pos > len(source) {
|
|
468
|
+
return TextEdit{}, false
|
|
469
|
+
}
|
|
470
|
+
clausePos, _ := tokenRange(file, metadata.lastClause)
|
|
471
|
+
indent := switchExhaustivenessCheckLineIndent(source, clausePos)
|
|
472
|
+
text := newline + indent + strings.Join(branches, newline+indent)
|
|
473
|
+
return TextEdit{Pos: pos, End: pos, Text: text}, true
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if metadata.caseBlock == nil ||
|
|
477
|
+
shimscanner.ScanTokenAtPosition(file, metadata.caseBlock.Pos()) != shimast.KindOpenBraceToken {
|
|
478
|
+
return TextEdit{}, false
|
|
479
|
+
}
|
|
480
|
+
openingBrace := shimscanner.GetRangeOfTokenAtPosition(file, metadata.caseBlock.Pos())
|
|
481
|
+
if shimscanner.ScanTokenAtPosition(file, openingBrace.End()) != shimast.KindCloseBraceToken {
|
|
482
|
+
return TextEdit{}, false
|
|
483
|
+
}
|
|
484
|
+
closingBrace := shimscanner.GetRangeOfTokenAtPosition(file, openingBrace.End())
|
|
485
|
+
if openingBrace.End() < 0 || closingBrace.Pos() < openingBrace.End() ||
|
|
486
|
+
closingBrace.Pos() > len(source) {
|
|
487
|
+
return TextEdit{}, false
|
|
488
|
+
}
|
|
489
|
+
switchPos, _ := tokenRange(file, switchNode)
|
|
490
|
+
switchIndent := switchExhaustivenessCheckLineIndent(source, switchPos)
|
|
491
|
+
caseIndent := switchIndent + " "
|
|
492
|
+
branchText := strings.Join(branches, newline+caseIndent)
|
|
493
|
+
interior := source[openingBrace.End():closingBrace.Pos()]
|
|
494
|
+
if strings.TrimSpace(interior) == "" {
|
|
495
|
+
return TextEdit{
|
|
496
|
+
Pos: openingBrace.End(),
|
|
497
|
+
End: closingBrace.Pos(),
|
|
498
|
+
Text: newline + caseIndent + branchText + newline + switchIndent,
|
|
499
|
+
}, true
|
|
500
|
+
}
|
|
501
|
+
return TextEdit{
|
|
502
|
+
Pos: openingBrace.End(),
|
|
503
|
+
End: openingBrace.End(),
|
|
504
|
+
Text: newline + caseIndent + branchText + newline + caseIndent,
|
|
505
|
+
}, true
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
func switchExhaustivenessCheckLineIndent(source string, pos int) string {
|
|
509
|
+
if pos < 0 || pos > len(source) {
|
|
510
|
+
return ""
|
|
511
|
+
}
|
|
512
|
+
lineStart := strings.LastIndex(source[:pos], "\n") + 1
|
|
513
|
+
indent := source[lineStart:pos]
|
|
514
|
+
for _, ch := range indent {
|
|
515
|
+
if ch != ' ' && ch != '\t' && ch != '\r' {
|
|
516
|
+
return ""
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return indent
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
func switchExhaustivenessCheckLineHasOnlyIndent(source string, pos int) bool {
|
|
523
|
+
if pos < 0 || pos > len(source) {
|
|
524
|
+
return false
|
|
525
|
+
}
|
|
526
|
+
lineStart := strings.LastIndex(source[:pos], "\n") + 1
|
|
527
|
+
return strings.TrimSpace(source[lineStart:pos]) == ""
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
func switchExhaustivenessCheckCaseIndent(
|
|
531
|
+
source string,
|
|
532
|
+
file *shimast.SourceFile,
|
|
533
|
+
switchNode *shimast.Node,
|
|
534
|
+
metadata *switchExhaustivenessCheckMetadata,
|
|
535
|
+
) string {
|
|
536
|
+
if metadata != nil && metadata.lastClause != nil {
|
|
537
|
+
clausePos, _ := tokenRange(file, metadata.lastClause)
|
|
538
|
+
if switchExhaustivenessCheckLineHasOnlyIndent(source, clausePos) {
|
|
539
|
+
return switchExhaustivenessCheckLineIndent(source, clausePos)
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
switchPos, _ := tokenRange(file, switchNode)
|
|
543
|
+
return switchExhaustivenessCheckLineIndent(source, switchPos) + " "
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// switchExhaustivenessCheckCommentDefaultCase recognizes only the last comment
|
|
547
|
+
// after the last real clause and before the case block's closing brace. This is
|
|
548
|
+
// the same scope as SourceCode#getCommentsAfter(lastCase); a matching comment
|
|
549
|
+
// elsewhere in the switch cannot suppress a diagnostic.
|
|
550
|
+
func switchExhaustivenessCheckCommentDefaultCase(
|
|
551
|
+
ctx *Context,
|
|
552
|
+
caseBlock *shimast.Node,
|
|
553
|
+
lastClause *shimast.Node,
|
|
554
|
+
pattern *regexp.Regexp,
|
|
555
|
+
) *switchExhaustivenessCheckDefaultCase {
|
|
556
|
+
if ctx.File == nil || caseBlock == nil || lastClause == nil || pattern == nil {
|
|
106
557
|
return nil
|
|
107
558
|
}
|
|
108
|
-
|
|
109
|
-
|
|
559
|
+
text := ctx.File.Text()
|
|
560
|
+
from := lastClause.End()
|
|
561
|
+
if shimscanner.ScanTokenAtPosition(ctx.File, lastClause.End()) != shimast.KindCloseBraceToken {
|
|
110
562
|
return nil
|
|
111
563
|
}
|
|
112
|
-
|
|
113
|
-
|
|
564
|
+
closeBrace := shimscanner.GetRangeOfTokenAtPosition(ctx.File, lastClause.End())
|
|
565
|
+
to := closeBrace.Pos()
|
|
566
|
+
if from < 0 || to < from || to > len(text) {
|
|
114
567
|
return nil
|
|
115
568
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
569
|
+
candidateKind := shimast.KindUnknown
|
|
570
|
+
candidatePos := -1
|
|
571
|
+
candidateEnd := -1
|
|
572
|
+
// `from:to` is itself a parser-classified trivia gap: it begins after the
|
|
573
|
+
// last clause and ends before the case block's closing-brace token. Scan the
|
|
574
|
+
// bounded gap directly so a file with many switches does not re-enumerate
|
|
575
|
+
// the whole AST for every switch node.
|
|
576
|
+
scanCommentGap(shimscanner.NewScanner(), text, from, to, func(kind shimast.Kind, pos, end int) {
|
|
577
|
+
candidateKind = kind
|
|
578
|
+
candidatePos = pos
|
|
579
|
+
candidateEnd = end
|
|
580
|
+
})
|
|
581
|
+
if candidatePos < 0 || candidateEnd <= candidatePos {
|
|
582
|
+
return nil
|
|
583
|
+
}
|
|
584
|
+
value := text[candidatePos:candidateEnd]
|
|
585
|
+
switch candidateKind {
|
|
586
|
+
case shimast.KindSingleLineCommentTrivia:
|
|
587
|
+
value = strings.TrimPrefix(value, "//")
|
|
588
|
+
case shimast.KindMultiLineCommentTrivia:
|
|
589
|
+
value = strings.TrimSuffix(strings.TrimPrefix(value, "/*"), "*/")
|
|
590
|
+
}
|
|
591
|
+
if !pattern.MatchString(strings.TrimSpace(value)) {
|
|
592
|
+
return nil
|
|
593
|
+
}
|
|
594
|
+
return &switchExhaustivenessCheckDefaultCase{
|
|
595
|
+
pos: candidatePos,
|
|
596
|
+
end: candidateEnd,
|
|
125
597
|
}
|
|
126
|
-
return members
|
|
127
598
|
}
|
|
128
599
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if flags&shimchecker.TypeFlagsLiteral != 0 {
|
|
137
|
-
return true
|
|
600
|
+
func switchExhaustivenessCheckReportDefaultCase(
|
|
601
|
+
ctx *Context,
|
|
602
|
+
defaultCase *switchExhaustivenessCheckDefaultCase,
|
|
603
|
+
message string,
|
|
604
|
+
) {
|
|
605
|
+
if defaultCase == nil {
|
|
606
|
+
return
|
|
138
607
|
}
|
|
139
|
-
if
|
|
140
|
-
|
|
608
|
+
if defaultCase.node != nil {
|
|
609
|
+
ctx.Report(defaultCase.node, message)
|
|
610
|
+
return
|
|
141
611
|
}
|
|
142
|
-
|
|
612
|
+
ctx.ReportRange(defaultCase.pos, defaultCase.end, message)
|
|
143
613
|
}
|
|
144
614
|
|
|
145
615
|
func init() {
|