@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
|
@@ -0,0 +1,1013 @@
|
|
|
1
|
+
// noFallthrough: `switch` cases whose end is reachable and that spill into
|
|
2
|
+
// the next `case` / `default` label without an intentional-fallthrough
|
|
3
|
+
// comment.
|
|
4
|
+
//
|
|
5
|
+
// The rule mirrors ESLint's `no-fallthrough` semantics:
|
|
6
|
+
//
|
|
7
|
+
// - Reachability is decided by a structured statement completion
|
|
8
|
+
// analysis ("can this statement list complete normally?") that
|
|
9
|
+
// composes blocks, `if/else`, loops, `switch`, labeled statements,
|
|
10
|
+
// and `try/catch/finally`. Nested function bodies, class field
|
|
11
|
+
// initializers, and static blocks keep separate code paths, so their
|
|
12
|
+
// completions do not terminate the enclosing case; immediately evaluated
|
|
13
|
+
// class heritage and computed names remain visible. The analysis is
|
|
14
|
+
// deliberately implemented here
|
|
15
|
+
// instead of reusing the checker's flow nodes: binder flow graphs
|
|
16
|
+
// require a bound Program plus checker-side reachability walks,
|
|
17
|
+
// while this rule must stay AST-only so the engine can keep running
|
|
18
|
+
// it in the parallel no-checker lane.
|
|
19
|
+
// - An intentional fallthrough is marked by the last comment before
|
|
20
|
+
// the next case label (or, when the case body is exactly one block
|
|
21
|
+
// statement, the last comment before that block's closing brace)
|
|
22
|
+
// matching the fallthrough comment pattern. The default pattern is
|
|
23
|
+
// ESLint's `/falls?\s?through/i`. Directive comments
|
|
24
|
+
// (`eslint-disable-next-line …`, `lint-disable …`, `globals …`) are
|
|
25
|
+
// never markers even when their text happens to match.
|
|
26
|
+
// - `commentPattern`, `allowEmptyCase`, and
|
|
27
|
+
// `reportUnusedFallthroughComment` options match the upstream
|
|
28
|
+
// schema. A custom `commentPattern` replaces the default marker
|
|
29
|
+
// pattern entirely, exactly like ESLint.
|
|
30
|
+
//
|
|
31
|
+
// https://eslint.org/docs/latest/rules/no-fallthrough
|
|
32
|
+
package linthost
|
|
33
|
+
|
|
34
|
+
import (
|
|
35
|
+
"regexp"
|
|
36
|
+
"strconv"
|
|
37
|
+
"strings"
|
|
38
|
+
|
|
39
|
+
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
40
|
+
shimscanner "github.com/microsoft/typescript-go/shim/scanner"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
// noFallthroughDefaultCommentPattern is ESLint's DEFAULT_FALLTHROUGH_COMMENT
|
|
44
|
+
// (`/falls?\s?through/iu`) translated to RE2. It accepts `fall through`,
|
|
45
|
+
// `falls through`, `fallthrough`, `fallsthrough`, any letter case.
|
|
46
|
+
var noFallthroughDefaultCommentPattern = regexp.MustCompile(`(?i)falls?\s?through`)
|
|
47
|
+
|
|
48
|
+
// noFallthroughDirectivePattern ports ESLint's shared `directivesPattern`
|
|
49
|
+
// and extends it with the `lint-*` directive family this host also
|
|
50
|
+
// recognizes (see directives.go). A directive comment is configuration,
|
|
51
|
+
// not documentation, so it never counts as an intentional-fallthrough
|
|
52
|
+
// marker even when its text matches the comment pattern (e.g.
|
|
53
|
+
// `// eslint-enable no-fallthrough`).
|
|
54
|
+
var noFallthroughDirectivePattern = regexp.MustCompile(
|
|
55
|
+
`^(?:eslint(?:-env|-enable|-disable(?:(?:-next)?-line)?)?|lint-(?:enable|disable(?:(?:-next)?-line)?)|exported|globals?)(?:\s|$)`,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
// noFallthroughOptions mirrors the upstream rule's options schema.
|
|
59
|
+
type noFallthroughOptions struct {
|
|
60
|
+
// CommentPattern replaces the default fallthrough marker pattern when
|
|
61
|
+
// non-empty. Compiled as an RE2 expression against the comment's inner
|
|
62
|
+
// text; an invalid pattern falls back to the default marker pattern so
|
|
63
|
+
// a config typo cannot silently disable marker recognition.
|
|
64
|
+
CommentPattern string `json:"commentPattern"`
|
|
65
|
+
// AllowEmptyCase suppresses the blank-line heuristic for empty cases:
|
|
66
|
+
// by default an empty case separated from the next label by at least
|
|
67
|
+
// one blank line is treated as an accidental fallthrough.
|
|
68
|
+
AllowEmptyCase bool `json:"allowEmptyCase"`
|
|
69
|
+
// ReportUnusedFallthroughComment reports fallthrough marker comments on
|
|
70
|
+
// cases that cannot actually fall through (e.g. after a `break`).
|
|
71
|
+
ReportUnusedFallthroughComment bool `json:"reportUnusedFallthroughComment"`
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
type noFallthrough struct{ optionsRule }
|
|
75
|
+
|
|
76
|
+
func (noFallthrough) Name() string { return "no-fallthrough" }
|
|
77
|
+
func (noFallthrough) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindSwitchStatement} }
|
|
78
|
+
func (noFallthrough) Check(ctx *Context, node *shimast.Node) {
|
|
79
|
+
sw := node.AsSwitchStatement()
|
|
80
|
+
if sw == nil || sw.CaseBlock == nil || ctx.File == nil {
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
block := sw.CaseBlock.AsCaseBlock()
|
|
84
|
+
if block == nil || block.Clauses == nil {
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
var opts noFallthroughOptions
|
|
88
|
+
ctx.DecodeOptions(&opts)
|
|
89
|
+
pattern := noFallthroughDefaultCommentPattern
|
|
90
|
+
if opts.CommentPattern != "" {
|
|
91
|
+
if custom, err := compileUserPattern(opts.CommentPattern); err == nil {
|
|
92
|
+
pattern = custom
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
clauses := block.Clauses.Nodes
|
|
96
|
+
for i := 0; i+1 < len(clauses); i++ {
|
|
97
|
+
clause := clauses[i].AsCaseOrDefaultClause()
|
|
98
|
+
next := clauses[i+1]
|
|
99
|
+
if clause == nil || next == nil {
|
|
100
|
+
continue
|
|
101
|
+
}
|
|
102
|
+
stmts := clauseStatements(clause)
|
|
103
|
+
// ESLint calls this `isSwitchExitReachable`: the code path segment at
|
|
104
|
+
// the end of the case's consequent is reachable, i.e. the statement
|
|
105
|
+
// list can complete normally.
|
|
106
|
+
endReachable := statementListCompletion(stmts).normal
|
|
107
|
+
// The last case never falls through (there is no next label), which
|
|
108
|
+
// the `i+1 < len(clauses)` loop bound already guarantees.
|
|
109
|
+
fallsThrough := endReachable &&
|
|
110
|
+
(len(stmts) > 0 ||
|
|
111
|
+
(!opts.AllowEmptyCase && noFallthroughHasBlankLinesBetween(ctx.File, clauses[i], next)))
|
|
112
|
+
commentPos, commentEnd, hasMarker := noFallthroughMarkerComment(ctx.File, clause, next, pattern)
|
|
113
|
+
if fallsThrough && !hasMarker {
|
|
114
|
+
label := "case"
|
|
115
|
+
if next.Kind == shimast.KindDefaultClause {
|
|
116
|
+
label = "default"
|
|
117
|
+
}
|
|
118
|
+
ctx.Report(next, "Expected a 'break' statement before '"+label+"'.")
|
|
119
|
+
} else if opts.ReportUnusedFallthroughComment && !endReachable && hasMarker {
|
|
120
|
+
ctx.ReportRange(commentPos, commentEnd, "Found a comment that would permit fallthrough, but case cannot fall through.")
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// clauseStatements returns a clause's statement list, tolerating nil
|
|
126
|
+
// intermediates on malformed parses.
|
|
127
|
+
func clauseStatements(clause *shimast.CaseOrDefaultClause) []*shimast.Node {
|
|
128
|
+
if clause == nil || clause.Statements == nil {
|
|
129
|
+
return nil
|
|
130
|
+
}
|
|
131
|
+
return clause.Statements.Nodes
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// noFallthroughHasBlankLinesBetween reports whether at least one blank
|
|
135
|
+
// line separates the end of `clause` from the first token of `next`
|
|
136
|
+
// (comments between them are skipped, exactly like ESLint's
|
|
137
|
+
// `getTokenAfter`). ESLint treats a blank-line gap after an empty case as
|
|
138
|
+
// an accidental fallthrough unless `allowEmptyCase` is set.
|
|
139
|
+
func noFallthroughHasBlankLinesBetween(file *shimast.SourceFile, clause, next *shimast.Node) bool {
|
|
140
|
+
text := file.Text()
|
|
141
|
+
nextToken := shimscanner.SkipTrivia(text, next.Pos())
|
|
142
|
+
endLine := shimscanner.GetECMALineOfPosition(file, clause.End())
|
|
143
|
+
nextLine := shimscanner.GetECMALineOfPosition(file, nextToken)
|
|
144
|
+
return nextLine > endLine+1
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// noFallthroughMarkerComment locates ESLint's intentional-fallthrough
|
|
148
|
+
// marker for the transition `clause` → `next` and returns its source
|
|
149
|
+
// range. Two positions are eligible, checked in upstream order:
|
|
150
|
+
//
|
|
151
|
+
// 1. When the case body is exactly one block statement, the last
|
|
152
|
+
// comment before the block's closing brace.
|
|
153
|
+
// 2. The last comment between the end of `clause` and the `case` /
|
|
154
|
+
// `default` keyword of `next`.
|
|
155
|
+
//
|
|
156
|
+
// Only the LAST comment of each region counts (`getCommentsBefore(...).pop()`
|
|
157
|
+
// upstream), so an unrelated comment after the marker invalidates it.
|
|
158
|
+
func noFallthroughMarkerComment(
|
|
159
|
+
file *shimast.SourceFile,
|
|
160
|
+
clause *shimast.CaseOrDefaultClause,
|
|
161
|
+
next *shimast.Node,
|
|
162
|
+
pattern *regexp.Regexp,
|
|
163
|
+
) (pos, end int, ok bool) {
|
|
164
|
+
text := file.Text()
|
|
165
|
+
stmts := clauseStatements(clause)
|
|
166
|
+
if len(stmts) == 1 && stmts[0] != nil && stmts[0].Kind == shimast.KindBlock {
|
|
167
|
+
if block := stmts[0].AsBlock(); block != nil {
|
|
168
|
+
from := blockInteriorStart(text, block)
|
|
169
|
+
closeBrace := block.End() - 1 // exclusive End covers the `}` token
|
|
170
|
+
if from >= 0 && closeBrace > from {
|
|
171
|
+
if p, e, found := lastCommentInTrivia(text, from, closeBrace); found &&
|
|
172
|
+
isNoFallthroughMarker(text[p:e], pattern) {
|
|
173
|
+
return p, e, true
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
nextToken := shimscanner.SkipTrivia(text, next.Pos())
|
|
179
|
+
if p, e, found := lastCommentInTrivia(text, clause.End(), nextToken); found &&
|
|
180
|
+
isNoFallthroughMarker(text[p:e], pattern) {
|
|
181
|
+
return p, e, true
|
|
182
|
+
}
|
|
183
|
+
return 0, 0, false
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// blockInteriorStart returns the offset just past a block's last interior
|
|
187
|
+
// token: after the final statement when the block has one, otherwise
|
|
188
|
+
// after the opening `{`. The region from there to the closing brace is
|
|
189
|
+
// trivia-only. Returns -1 when the block shape is malformed.
|
|
190
|
+
func blockInteriorStart(text string, block *shimast.Block) int {
|
|
191
|
+
if block.Statements != nil {
|
|
192
|
+
if nodes := block.Statements.Nodes; len(nodes) > 0 {
|
|
193
|
+
last := nodes[len(nodes)-1]
|
|
194
|
+
if last == nil {
|
|
195
|
+
return -1
|
|
196
|
+
}
|
|
197
|
+
return last.End()
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
openBrace := shimscanner.SkipTrivia(text, block.Pos())
|
|
201
|
+
if openBrace < 0 || openBrace >= len(text) || text[openBrace] != '{' {
|
|
202
|
+
return -1
|
|
203
|
+
}
|
|
204
|
+
return openBrace + 1
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// isNoFallthroughMarker reports whether one raw comment token (delimiters
|
|
208
|
+
// included) is an intentional-fallthrough marker: its inner text matches
|
|
209
|
+
// the configured pattern and it is not a directive comment. Mirrors
|
|
210
|
+
// upstream `isFallThroughComment`, which tests the un-trimmed comment
|
|
211
|
+
// value against the pattern but the trimmed value against the directive
|
|
212
|
+
// pattern.
|
|
213
|
+
func isNoFallthroughMarker(raw string, pattern *regexp.Regexp) bool {
|
|
214
|
+
value := commentInnerText(raw)
|
|
215
|
+
return pattern.MatchString(value) &&
|
|
216
|
+
!noFallthroughDirectivePattern.MatchString(strings.TrimSpace(value))
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// commentInnerText strips the comment delimiters from a raw comment token
|
|
220
|
+
// and returns the interior verbatim — no trimming, no JSDoc `*` handling —
|
|
221
|
+
// matching ESLint's `comment.value`. (stripCommentDelimiters is not reused
|
|
222
|
+
// here because it trims and strips JSDoc stars, which would let anchored
|
|
223
|
+
// custom patterns match text ESLint would reject.)
|
|
224
|
+
func commentInnerText(raw string) string {
|
|
225
|
+
switch {
|
|
226
|
+
case strings.HasPrefix(raw, "//"):
|
|
227
|
+
return raw[2:]
|
|
228
|
+
case strings.HasPrefix(raw, "/*"):
|
|
229
|
+
inner := raw[2:]
|
|
230
|
+
inner = strings.TrimSuffix(inner, "*/")
|
|
231
|
+
return inner
|
|
232
|
+
default:
|
|
233
|
+
return raw
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// lastCommentInTrivia scans the trivia-only region text[from:to) and
|
|
238
|
+
// returns the source range of the last comment token in it. Both regions
|
|
239
|
+
// this rule inspects — between a clause end and the next case keyword,
|
|
240
|
+
// and between a block's last token and its closing brace — contain only
|
|
241
|
+
// whitespace and comments by construction, so a local scan is exact. The
|
|
242
|
+
// scan bails out on any unexpected non-trivia byte instead of guessing.
|
|
243
|
+
func lastCommentInTrivia(text string, from, to int) (pos, end int, ok bool) {
|
|
244
|
+
if from < 0 || to > len(text) || from >= to {
|
|
245
|
+
return 0, 0, false
|
|
246
|
+
}
|
|
247
|
+
i := from
|
|
248
|
+
for i < to {
|
|
249
|
+
c := text[i]
|
|
250
|
+
switch {
|
|
251
|
+
case c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == '\v' || c == '\f':
|
|
252
|
+
i++
|
|
253
|
+
case c == '/' && i+1 < to && text[i+1] == '/':
|
|
254
|
+
start := i
|
|
255
|
+
i += 2
|
|
256
|
+
for i < to && text[i] != '\n' && text[i] != '\r' {
|
|
257
|
+
i++
|
|
258
|
+
}
|
|
259
|
+
pos, end, ok = start, i, true
|
|
260
|
+
case c == '/' && i+1 < to && text[i+1] == '*':
|
|
261
|
+
start := i
|
|
262
|
+
i += 2
|
|
263
|
+
for i < to {
|
|
264
|
+
if text[i] == '*' && i+1 < to && text[i+1] == '/' {
|
|
265
|
+
i += 2
|
|
266
|
+
break
|
|
267
|
+
}
|
|
268
|
+
i++
|
|
269
|
+
}
|
|
270
|
+
pos, end, ok = start, i, true
|
|
271
|
+
case c >= 0x80:
|
|
272
|
+
// Exotic Unicode whitespace (NBSP, U+2028, …) is legal trivia;
|
|
273
|
+
// skip the whole rune. Any other non-ASCII byte here would mean
|
|
274
|
+
// the region was not trivia-only, and the rune skip is still the
|
|
275
|
+
// safest way to keep scanning without splitting a code point.
|
|
276
|
+
i++
|
|
277
|
+
for i < to && text[i]&0xC0 == 0x80 {
|
|
278
|
+
i++
|
|
279
|
+
}
|
|
280
|
+
default:
|
|
281
|
+
// Not a trivia byte: the region assumption was violated
|
|
282
|
+
// (malformed parse). Stop rather than misattribute comments.
|
|
283
|
+
return pos, end, ok
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return pos, end, ok
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// caseCompletion records every way a statement can finish: normally, by
|
|
290
|
+
// return or throw, or through a labeled/unlabeled break or continue. The
|
|
291
|
+
// empty-string key stands for the unlabeled form. Enclosing constructs
|
|
292
|
+
// consume the completions they own and propagate the rest.
|
|
293
|
+
type caseCompletion struct {
|
|
294
|
+
normal bool
|
|
295
|
+
returns bool
|
|
296
|
+
throws bool
|
|
297
|
+
breaks map[string]struct{}
|
|
298
|
+
continues map[string]struct{}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// expressionCompletion records abrupt paths created while evaluating an
|
|
302
|
+
// expression. In addition to ordinary exception edges, `yield` can resume via
|
|
303
|
+
// either generator.throw() or generator.return(), so both paths must survive
|
|
304
|
+
// into enclosing try/finally composition.
|
|
305
|
+
type expressionCompletion struct {
|
|
306
|
+
returns bool
|
|
307
|
+
throws bool
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
func (c *expressionCompletion) merge(other expressionCompletion) {
|
|
311
|
+
c.returns = c.returns || other.returns
|
|
312
|
+
c.throws = c.throws || other.throws
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
func (c *caseCompletion) addBreak(label string) {
|
|
316
|
+
if c.breaks == nil {
|
|
317
|
+
c.breaks = map[string]struct{}{}
|
|
318
|
+
}
|
|
319
|
+
c.breaks[label] = struct{}{}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
func (c *caseCompletion) addContinue(label string) {
|
|
323
|
+
if c.continues == nil {
|
|
324
|
+
c.continues = map[string]struct{}{}
|
|
325
|
+
}
|
|
326
|
+
c.continues[label] = struct{}{}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
func (c *caseCompletion) hasBreak(label string) bool {
|
|
330
|
+
_, ok := c.breaks[label]
|
|
331
|
+
return ok
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
func (c *caseCompletion) hasContinue(label string) bool {
|
|
335
|
+
_, ok := c.continues[label]
|
|
336
|
+
return ok
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
func (c *caseCompletion) removeBreak(label string) { delete(c.breaks, label) }
|
|
340
|
+
func (c *caseCompletion) removeContinue(label string) { delete(c.continues, label) }
|
|
341
|
+
|
|
342
|
+
// mergeAbrupt unions the other completion's abrupt paths into this one.
|
|
343
|
+
// normal is deliberately untouched; each composition rule computes it.
|
|
344
|
+
func (c *caseCompletion) mergeAbrupt(other caseCompletion) {
|
|
345
|
+
c.returns = c.returns || other.returns
|
|
346
|
+
c.throws = c.throws || other.throws
|
|
347
|
+
for label := range other.breaks {
|
|
348
|
+
c.addBreak(label)
|
|
349
|
+
}
|
|
350
|
+
for label := range other.continues {
|
|
351
|
+
c.addContinue(label)
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// hasCompletion reports whether at least one control-flow path leaves the
|
|
356
|
+
// statement. An infinite loop with no reachable escape has no completion,
|
|
357
|
+
// which also means a following finally block is unreachable.
|
|
358
|
+
func (c *caseCompletion) hasCompletion() bool {
|
|
359
|
+
return c.normal || c.returns || c.throws || len(c.breaks) > 0 || len(c.continues) > 0
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// statementListCompletion runs the completion analysis over a statement
|
|
363
|
+
// list executed in order. Once a statement cannot complete normally the
|
|
364
|
+
// remainder of the list is unreachable: it neither restores normal
|
|
365
|
+
// completion nor contributes escapes (an unreachable `break` can never
|
|
366
|
+
// execute, matching ESLint's unreachable-segment tracking).
|
|
367
|
+
func statementListCompletion(stmts []*shimast.Node) caseCompletion {
|
|
368
|
+
out := caseCompletion{normal: true}
|
|
369
|
+
for _, stmt := range stmts {
|
|
370
|
+
if stmt == nil {
|
|
371
|
+
continue
|
|
372
|
+
}
|
|
373
|
+
if !out.normal {
|
|
374
|
+
break
|
|
375
|
+
}
|
|
376
|
+
r := statementCompletion(stmt, nil)
|
|
377
|
+
out.mergeAbrupt(r)
|
|
378
|
+
out.normal = r.normal
|
|
379
|
+
}
|
|
380
|
+
return out
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// statementCompletion computes how a single statement can complete.
|
|
384
|
+
// `labels` carries the label names bound to this statement by directly
|
|
385
|
+
// wrapping labeled statements, so loops can absorb `continue L` / `break L`
|
|
386
|
+
// aimed at themselves. Evaluated expressions are inspected for throwable
|
|
387
|
+
// nodes and abrupt generator resumptions; nested function, class-field, and
|
|
388
|
+
// static-block code paths remain isolated from the enclosing case.
|
|
389
|
+
func statementCompletion(stmt *shimast.Node, labels []string) caseCompletion {
|
|
390
|
+
if stmt == nil {
|
|
391
|
+
return caseCompletion{normal: true}
|
|
392
|
+
}
|
|
393
|
+
switch stmt.Kind {
|
|
394
|
+
case shimast.KindReturnStatement:
|
|
395
|
+
ret := stmt.AsReturnStatement()
|
|
396
|
+
expression := expressionCompletion{}
|
|
397
|
+
if ret != nil {
|
|
398
|
+
expression = executableNodeCompletion(ret.Expression)
|
|
399
|
+
}
|
|
400
|
+
return caseCompletion{
|
|
401
|
+
returns: true,
|
|
402
|
+
throws: expression.throws,
|
|
403
|
+
}
|
|
404
|
+
case shimast.KindThrowStatement:
|
|
405
|
+
// Evaluating the operand may throw first, and the statement itself
|
|
406
|
+
// always produces a throw completion. Both reach the same catch edge.
|
|
407
|
+
throw := stmt.AsThrowStatement()
|
|
408
|
+
expression := expressionCompletion{}
|
|
409
|
+
if throw != nil {
|
|
410
|
+
expression = executableNodeCompletion(throw.Expression)
|
|
411
|
+
}
|
|
412
|
+
return caseCompletion{returns: expression.returns, throws: true}
|
|
413
|
+
case shimast.KindBreakStatement:
|
|
414
|
+
out := caseCompletion{}
|
|
415
|
+
out.addBreak(identifierText(stmt.AsBreakStatement().Label))
|
|
416
|
+
return out
|
|
417
|
+
case shimast.KindContinueStatement:
|
|
418
|
+
out := caseCompletion{}
|
|
419
|
+
out.addContinue(identifierText(stmt.AsContinueStatement().Label))
|
|
420
|
+
return out
|
|
421
|
+
case shimast.KindBlock:
|
|
422
|
+
block := stmt.AsBlock()
|
|
423
|
+
if block == nil || block.Statements == nil {
|
|
424
|
+
return caseCompletion{normal: true}
|
|
425
|
+
}
|
|
426
|
+
return statementListCompletion(block.Statements.Nodes)
|
|
427
|
+
case shimast.KindIfStatement:
|
|
428
|
+
return ifCompletion(stmt.AsIfStatement())
|
|
429
|
+
case shimast.KindLabeledStatement:
|
|
430
|
+
return labeledCompletion(stmt.AsLabeledStatement(), labels)
|
|
431
|
+
case shimast.KindWhileStatement:
|
|
432
|
+
s := stmt.AsWhileStatement()
|
|
433
|
+
constTrue, constFalse := literalTruthiness(s.Expression)
|
|
434
|
+
return loopCompletion(
|
|
435
|
+
s.Statement,
|
|
436
|
+
labels,
|
|
437
|
+
constTrue,
|
|
438
|
+
constFalse,
|
|
439
|
+
false,
|
|
440
|
+
executableNodeCompletion(s.Expression),
|
|
441
|
+
expressionCompletion{},
|
|
442
|
+
)
|
|
443
|
+
case shimast.KindDoStatement:
|
|
444
|
+
s := stmt.AsDoStatement()
|
|
445
|
+
constTrue, constFalse := literalTruthiness(s.Expression)
|
|
446
|
+
return loopCompletion(
|
|
447
|
+
s.Statement,
|
|
448
|
+
labels,
|
|
449
|
+
constTrue,
|
|
450
|
+
constFalse,
|
|
451
|
+
true,
|
|
452
|
+
executableNodeCompletion(s.Expression),
|
|
453
|
+
expressionCompletion{},
|
|
454
|
+
)
|
|
455
|
+
case shimast.KindForStatement:
|
|
456
|
+
s := stmt.AsForStatement()
|
|
457
|
+
constTrue, constFalse := literalTruthiness(s.Condition)
|
|
458
|
+
if s.Condition == nil {
|
|
459
|
+
// `for (;;)` loops forever unless something breaks out.
|
|
460
|
+
constTrue, constFalse = true, false
|
|
461
|
+
}
|
|
462
|
+
out := loopCompletion(
|
|
463
|
+
s.Statement,
|
|
464
|
+
labels,
|
|
465
|
+
constTrue,
|
|
466
|
+
constFalse,
|
|
467
|
+
false,
|
|
468
|
+
executableNodeCompletion(s.Condition),
|
|
469
|
+
executableNodeCompletion(s.Incrementor),
|
|
470
|
+
)
|
|
471
|
+
mergeExpressionCompletion(&out, executableNodeCompletion(s.Initializer))
|
|
472
|
+
return out
|
|
473
|
+
case shimast.KindForInStatement, shimast.KindForOfStatement:
|
|
474
|
+
// The iterated collection may be empty, so the loop always offers
|
|
475
|
+
// normal completion — identical to a non-constant loop test.
|
|
476
|
+
s := stmt.AsForInOrOfStatement()
|
|
477
|
+
out := loopCompletion(
|
|
478
|
+
s.Statement,
|
|
479
|
+
labels,
|
|
480
|
+
false,
|
|
481
|
+
false,
|
|
482
|
+
false,
|
|
483
|
+
expressionCompletion{},
|
|
484
|
+
expressionCompletion{},
|
|
485
|
+
)
|
|
486
|
+
mergeExpressionCompletion(&out, executableNodeCompletion(s.Initializer))
|
|
487
|
+
mergeExpressionCompletion(&out, executableNodeCompletion(s.Expression))
|
|
488
|
+
return out
|
|
489
|
+
case shimast.KindSwitchStatement:
|
|
490
|
+
return switchCompletion(stmt.AsSwitchStatement(), labels)
|
|
491
|
+
case shimast.KindTryStatement:
|
|
492
|
+
return tryCompletion(stmt.AsTryStatement())
|
|
493
|
+
case shimast.KindWithStatement:
|
|
494
|
+
s := stmt.AsWithStatement()
|
|
495
|
+
out := statementCompletion(s.Statement, nil)
|
|
496
|
+
mergeExpressionCompletion(&out, executableNodeCompletion(s.Expression))
|
|
497
|
+
return out
|
|
498
|
+
default:
|
|
499
|
+
// Leaf statements complete normally, but their evaluated expressions
|
|
500
|
+
// can still enter an enclosing catch. The walker excludes type syntax
|
|
501
|
+
// and nested function/class execution contexts.
|
|
502
|
+
expression := executableNodeCompletion(stmt)
|
|
503
|
+
return caseCompletion{normal: true, returns: expression.returns, throws: expression.throws}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// ifCompletion: an `if` without `else` can always complete normally (the
|
|
508
|
+
// condition may be false); with an `else`, normal completion requires at
|
|
509
|
+
// least one branch to complete normally. Conditions are not constant-
|
|
510
|
+
// folded, matching ESLint's code path analysis which folds only loop
|
|
511
|
+
// tests.
|
|
512
|
+
func ifCompletion(s *shimast.IfStatement) caseCompletion {
|
|
513
|
+
if s == nil {
|
|
514
|
+
return caseCompletion{normal: true}
|
|
515
|
+
}
|
|
516
|
+
then := statementCompletion(s.ThenStatement, nil)
|
|
517
|
+
expression := executableNodeCompletion(s.Expression)
|
|
518
|
+
if s.ElseStatement == nil {
|
|
519
|
+
then.normal = true
|
|
520
|
+
mergeExpressionCompletion(&then, expression)
|
|
521
|
+
return then
|
|
522
|
+
}
|
|
523
|
+
els := statementCompletion(s.ElseStatement, nil)
|
|
524
|
+
out := caseCompletion{
|
|
525
|
+
normal: then.normal || els.normal,
|
|
526
|
+
returns: expression.returns,
|
|
527
|
+
throws: expression.throws,
|
|
528
|
+
}
|
|
529
|
+
out.mergeAbrupt(then)
|
|
530
|
+
out.mergeAbrupt(els)
|
|
531
|
+
return out
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// labeledCompletion: `L: stmt` completes normally when the body does, or
|
|
535
|
+
// when a `break L` escapes the body (control resumes right after the
|
|
536
|
+
// labeled statement). The label is added to the body's label set so a
|
|
537
|
+
// directly-labeled loop can absorb `continue L` itself.
|
|
538
|
+
func labeledCompletion(s *shimast.LabeledStatement, labels []string) caseCompletion {
|
|
539
|
+
if s == nil {
|
|
540
|
+
return caseCompletion{normal: true}
|
|
541
|
+
}
|
|
542
|
+
name := identifierText(s.Label)
|
|
543
|
+
if name == "" {
|
|
544
|
+
return statementCompletion(s.Statement, labels)
|
|
545
|
+
}
|
|
546
|
+
inner := statementCompletion(s.Statement, append(labels, name))
|
|
547
|
+
if inner.hasBreak(name) {
|
|
548
|
+
inner.normal = true
|
|
549
|
+
}
|
|
550
|
+
inner.removeBreak(name)
|
|
551
|
+
inner.removeContinue(name)
|
|
552
|
+
return inner
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// loopCompletion is the shared engine for `while`, `do/while`, `for`,
|
|
556
|
+
// `for-in`, and `for-of` bodies. Loop tests are constant-folded only for
|
|
557
|
+
// simple literals (`true`, `1`, `"x"`, `null`, …), matching ESLint's
|
|
558
|
+
// `getBooleanValueIfSimpleConstant`. The loop absorbs unlabeled breaks
|
|
559
|
+
// and continues plus the labeled forms naming the loop itself (via
|
|
560
|
+
// `labels`); everything else escapes to the enclosing construct.
|
|
561
|
+
func loopCompletion(
|
|
562
|
+
body *shimast.Node,
|
|
563
|
+
labels []string,
|
|
564
|
+
constTrue, constFalse, isDoWhile bool,
|
|
565
|
+
test, increment expressionCompletion,
|
|
566
|
+
) caseCompletion {
|
|
567
|
+
if constFalse && !isDoWhile {
|
|
568
|
+
// The body never runs, so nothing inside it (including breaks) can
|
|
569
|
+
// execute. The test is still evaluated once before normal completion.
|
|
570
|
+
return caseCompletion{normal: true, returns: test.returns, throws: test.throws}
|
|
571
|
+
}
|
|
572
|
+
r := statementCompletion(body, nil)
|
|
573
|
+
exitByBreak := r.hasBreak("")
|
|
574
|
+
iterationEnds := r.normal || r.hasContinue("")
|
|
575
|
+
for _, l := range labels {
|
|
576
|
+
exitByBreak = exitByBreak || r.hasBreak(l)
|
|
577
|
+
iterationEnds = iterationEnds || r.hasContinue(l)
|
|
578
|
+
}
|
|
579
|
+
if !isDoWhile || iterationEnds {
|
|
580
|
+
// while/for tests run before the first iteration. A do/while test is
|
|
581
|
+
// reachable only when the body reaches the iteration boundary.
|
|
582
|
+
mergeExpressionCompletion(&r, test)
|
|
583
|
+
}
|
|
584
|
+
if iterationEnds {
|
|
585
|
+
mergeExpressionCompletion(&r, increment)
|
|
586
|
+
}
|
|
587
|
+
var normal bool
|
|
588
|
+
switch {
|
|
589
|
+
case constTrue:
|
|
590
|
+
// Infinite loop: only a break reaches the code after it.
|
|
591
|
+
normal = exitByBreak
|
|
592
|
+
case isDoWhile:
|
|
593
|
+
// The body runs at least once; afterwards the test can fail.
|
|
594
|
+
normal = exitByBreak || iterationEnds
|
|
595
|
+
default:
|
|
596
|
+
// The test may fail before the first iteration.
|
|
597
|
+
normal = true
|
|
598
|
+
}
|
|
599
|
+
r.removeBreak("")
|
|
600
|
+
r.removeContinue("")
|
|
601
|
+
for _, l := range labels {
|
|
602
|
+
r.removeBreak(l)
|
|
603
|
+
r.removeContinue(l)
|
|
604
|
+
}
|
|
605
|
+
r.normal = normal
|
|
606
|
+
return r
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// switchCompletion: a nested `switch` completes normally when it has no
|
|
610
|
+
// `default` (the discriminant may match nothing), when its final clause
|
|
611
|
+
// completes normally (falling off the end), or when any reachable `break`
|
|
612
|
+
// targets it. Labeled breaks naming the switch (via `labels`) are
|
|
613
|
+
// absorbed the same way; `continue` never targets a switch and passes
|
|
614
|
+
// through.
|
|
615
|
+
func switchCompletion(s *shimast.SwitchStatement, labels []string) caseCompletion {
|
|
616
|
+
if s == nil {
|
|
617
|
+
return caseCompletion{normal: true}
|
|
618
|
+
}
|
|
619
|
+
discriminant := executableNodeCompletion(s.Expression)
|
|
620
|
+
out := caseCompletion{normal: true, returns: discriminant.returns, throws: discriminant.throws}
|
|
621
|
+
if s.CaseBlock == nil {
|
|
622
|
+
return out
|
|
623
|
+
}
|
|
624
|
+
block := s.CaseBlock.AsCaseBlock()
|
|
625
|
+
if block == nil || block.Clauses == nil || len(block.Clauses.Nodes) == 0 {
|
|
626
|
+
return out
|
|
627
|
+
}
|
|
628
|
+
clauses := block.Clauses.Nodes
|
|
629
|
+
hasDefault := false
|
|
630
|
+
exitByBreak := false
|
|
631
|
+
lastNormal := false
|
|
632
|
+
out.normal = false
|
|
633
|
+
for i, clauseNode := range clauses {
|
|
634
|
+
if clauseNode == nil {
|
|
635
|
+
continue
|
|
636
|
+
}
|
|
637
|
+
if clauseNode.Kind == shimast.KindDefaultClause {
|
|
638
|
+
hasDefault = true
|
|
639
|
+
}
|
|
640
|
+
clause := clauseNode.AsCaseOrDefaultClause()
|
|
641
|
+
if clause != nil {
|
|
642
|
+
mergeExpressionCompletion(&out, executableNodeCompletion(clause.Expression))
|
|
643
|
+
}
|
|
644
|
+
r := statementListCompletion(clauseStatements(clause))
|
|
645
|
+
if r.hasBreak("") {
|
|
646
|
+
exitByBreak = true
|
|
647
|
+
}
|
|
648
|
+
for _, l := range labels {
|
|
649
|
+
if r.hasBreak(l) {
|
|
650
|
+
exitByBreak = true
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
if i == len(clauses)-1 {
|
|
654
|
+
lastNormal = r.normal
|
|
655
|
+
}
|
|
656
|
+
out.mergeAbrupt(r)
|
|
657
|
+
}
|
|
658
|
+
out.removeBreak("")
|
|
659
|
+
for _, l := range labels {
|
|
660
|
+
out.removeBreak(l)
|
|
661
|
+
}
|
|
662
|
+
out.normal = !hasDefault || lastNormal || exitByBreak
|
|
663
|
+
return out
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// tryCompletion composes normal, return, throw, break, and continue paths.
|
|
667
|
+
// A catch contributes only when the protected block has a reachable throw
|
|
668
|
+
// edge. A reachable finally runs for every completion from try/catch; its
|
|
669
|
+
// abrupt completions override the incoming completion, while an ordinary
|
|
670
|
+
// finally path preserves it.
|
|
671
|
+
func tryCompletion(s *shimast.TryStatement) caseCompletion {
|
|
672
|
+
if s == nil {
|
|
673
|
+
return caseCompletion{normal: true}
|
|
674
|
+
}
|
|
675
|
+
tryC := blockNodeCompletion(s.TryBlock)
|
|
676
|
+
var catchC caseCompletion
|
|
677
|
+
hasCatch := false
|
|
678
|
+
if s.CatchClause != nil {
|
|
679
|
+
if clause := s.CatchClause.AsCatchClause(); clause != nil {
|
|
680
|
+
hasCatch = true
|
|
681
|
+
catchC = blockNodeCompletion(clause.Block)
|
|
682
|
+
mergeExpressionCompletion(&catchC, executableNodeCompletion(clause.VariableDeclaration))
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
main := tryC
|
|
686
|
+
if hasCatch && tryC.throws {
|
|
687
|
+
// The catch consumes every throw from the protected block and replaces
|
|
688
|
+
// those paths with the catch block's possible completions.
|
|
689
|
+
main.throws = false
|
|
690
|
+
main.normal = tryC.normal || catchC.normal
|
|
691
|
+
main.mergeAbrupt(catchC)
|
|
692
|
+
}
|
|
693
|
+
if s.FinallyBlock == nil {
|
|
694
|
+
return main
|
|
695
|
+
}
|
|
696
|
+
if !main.hasCompletion() {
|
|
697
|
+
// No path leaves the protected region (for example, a closed infinite
|
|
698
|
+
// loop), so execution never enters the finally block.
|
|
699
|
+
return caseCompletion{}
|
|
700
|
+
}
|
|
701
|
+
finallyC := blockNodeCompletion(s.FinallyBlock)
|
|
702
|
+
out := caseCompletion{}
|
|
703
|
+
if finallyC.normal {
|
|
704
|
+
out.mergeAbrupt(main)
|
|
705
|
+
}
|
|
706
|
+
out.mergeAbrupt(finallyC)
|
|
707
|
+
out.normal = main.normal && finallyC.normal
|
|
708
|
+
return out
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// blockNodeCompletion analyzes a *BlockNode (try/catch/finally bodies).
|
|
712
|
+
func blockNodeCompletion(node *shimast.Node) caseCompletion {
|
|
713
|
+
if node == nil {
|
|
714
|
+
return caseCompletion{normal: true}
|
|
715
|
+
}
|
|
716
|
+
block := node.AsBlock()
|
|
717
|
+
if block == nil || block.Statements == nil {
|
|
718
|
+
return caseCompletion{normal: true}
|
|
719
|
+
}
|
|
720
|
+
return statementListCompletion(block.Statements.Nodes)
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// executableNodeCompletion mirrors ESLint's expression-level code-path
|
|
724
|
+
// effects. A reachable value-reference identifier, member access, call,
|
|
725
|
+
// import call, construction, or abrupt yield resumption can enter the nearest
|
|
726
|
+
// catch. Type syntax and separately analyzed function/class execution contexts
|
|
727
|
+
// never leak abrupt paths into the enclosing statement.
|
|
728
|
+
func executableNodeCompletion(node *shimast.Node) expressionCompletion {
|
|
729
|
+
if node == nil {
|
|
730
|
+
return expressionCompletion{}
|
|
731
|
+
}
|
|
732
|
+
if noFallthroughNodeIsJSXTagName(node) {
|
|
733
|
+
return expressionCompletion{}
|
|
734
|
+
}
|
|
735
|
+
if node.Kind == shimast.KindExpressionWithTypeArguments {
|
|
736
|
+
if node.Parent != nil &&
|
|
737
|
+
((node.Parent.Kind >= shimast.KindFirstJSDocNode && node.Parent.Kind <= shimast.KindLastJSDocNode) ||
|
|
738
|
+
(node.Parent.Kind >= shimast.KindFirstJSDocTagNode && node.Parent.Kind <= shimast.KindLastJSDocTagNode)) {
|
|
739
|
+
return expressionCompletion{}
|
|
740
|
+
}
|
|
741
|
+
if node.Parent == nil || node.Parent.Kind != shimast.KindHeritageClause {
|
|
742
|
+
expression := node.AsExpressionWithTypeArguments()
|
|
743
|
+
if expression == nil {
|
|
744
|
+
return expressionCompletion{}
|
|
745
|
+
}
|
|
746
|
+
return executableNodeCompletion(expression.Expression)
|
|
747
|
+
}
|
|
748
|
+
return runtimeHeritageExpressionCompletion(node)
|
|
749
|
+
}
|
|
750
|
+
if (node.Kind >= shimast.KindFirstJSDocNode && node.Kind <= shimast.KindLastJSDocNode) ||
|
|
751
|
+
(node.Kind >= shimast.KindFirstJSDocTagNode && node.Kind <= shimast.KindLastJSDocTagNode) {
|
|
752
|
+
return expressionCompletion{}
|
|
753
|
+
}
|
|
754
|
+
if node.Kind >= shimast.KindFirstTypeNode && node.Kind <= shimast.KindLastTypeNode {
|
|
755
|
+
return expressionCompletion{}
|
|
756
|
+
}
|
|
757
|
+
switch node.Kind {
|
|
758
|
+
case shimast.KindFunctionDeclaration,
|
|
759
|
+
shimast.KindFunctionExpression,
|
|
760
|
+
shimast.KindArrowFunction,
|
|
761
|
+
shimast.KindClassStaticBlockDeclaration,
|
|
762
|
+
shimast.KindInterfaceDeclaration,
|
|
763
|
+
shimast.KindTypeAliasDeclaration,
|
|
764
|
+
shimast.KindJsxNamespacedName:
|
|
765
|
+
return expressionCompletion{}
|
|
766
|
+
case shimast.KindMethodDeclaration,
|
|
767
|
+
shimast.KindConstructor,
|
|
768
|
+
shimast.KindGetAccessor,
|
|
769
|
+
shimast.KindSetAccessor,
|
|
770
|
+
shimast.KindPropertyDeclaration:
|
|
771
|
+
return classElementHeaderCompletion(node)
|
|
772
|
+
case shimast.KindIdentifier:
|
|
773
|
+
return expressionCompletion{throws: noFallthroughIdentifierIsReference(node)}
|
|
774
|
+
case shimast.KindYieldExpression:
|
|
775
|
+
expression := expressionCompletion{returns: true, throws: true}
|
|
776
|
+
node.ForEachChild(func(child *shimast.Node) bool {
|
|
777
|
+
expression.merge(executableNodeCompletion(child))
|
|
778
|
+
return expression.returns && expression.throws
|
|
779
|
+
})
|
|
780
|
+
return expression
|
|
781
|
+
case shimast.KindPropertyAccessExpression,
|
|
782
|
+
shimast.KindElementAccessExpression,
|
|
783
|
+
shimast.KindCallExpression,
|
|
784
|
+
shimast.KindNewExpression:
|
|
785
|
+
expression := expressionCompletion{throws: true}
|
|
786
|
+
node.ForEachChild(func(child *shimast.Node) bool {
|
|
787
|
+
expression.merge(executableNodeCompletion(child))
|
|
788
|
+
return expression.returns && expression.throws
|
|
789
|
+
})
|
|
790
|
+
return expression
|
|
791
|
+
}
|
|
792
|
+
expression := expressionCompletion{}
|
|
793
|
+
node.ForEachChild(func(child *shimast.Node) bool {
|
|
794
|
+
expression.merge(executableNodeCompletion(child))
|
|
795
|
+
return expression.returns && expression.throws
|
|
796
|
+
})
|
|
797
|
+
return expression
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
func mergeExpressionCompletion(completion *caseCompletion, expression expressionCompletion) {
|
|
801
|
+
completion.returns = completion.returns || expression.returns
|
|
802
|
+
completion.throws = completion.throws || expression.throws
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// runtimeHeritageExpressionCompletion retains the value expression in
|
|
806
|
+
// a class extends clause while excluding interface heritage and implements
|
|
807
|
+
// clauses, which are type-only even though they share the same AST node kind.
|
|
808
|
+
func runtimeHeritageExpressionCompletion(node *shimast.Node) expressionCompletion {
|
|
809
|
+
if node == nil || node.Parent == nil || node.Parent.Kind != shimast.KindHeritageClause {
|
|
810
|
+
return expressionCompletion{}
|
|
811
|
+
}
|
|
812
|
+
clauseNode := node.Parent
|
|
813
|
+
clause := clauseNode.AsHeritageClause()
|
|
814
|
+
if clause == nil || clause.Token != shimast.KindExtendsKeyword || clauseNode.Parent == nil {
|
|
815
|
+
return expressionCompletion{}
|
|
816
|
+
}
|
|
817
|
+
if clauseNode.Parent.Kind != shimast.KindClassDeclaration &&
|
|
818
|
+
clauseNode.Parent.Kind != shimast.KindClassExpression {
|
|
819
|
+
return expressionCompletion{}
|
|
820
|
+
}
|
|
821
|
+
expression := node.AsExpressionWithTypeArguments()
|
|
822
|
+
if expression == nil {
|
|
823
|
+
return expressionCompletion{}
|
|
824
|
+
}
|
|
825
|
+
return executableNodeCompletion(expression.Expression)
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// classElementHeaderCompletion keeps class member bodies and field
|
|
829
|
+
// initializers in their own code paths while retaining immediately evaluated
|
|
830
|
+
// decorators and computed property names in the enclosing class evaluation.
|
|
831
|
+
func classElementHeaderCompletion(node *shimast.Node) expressionCompletion {
|
|
832
|
+
if node == nil {
|
|
833
|
+
return expressionCompletion{}
|
|
834
|
+
}
|
|
835
|
+
completion := expressionCompletion{}
|
|
836
|
+
if modifiers := node.Modifiers(); modifiers != nil {
|
|
837
|
+
for _, modifier := range modifiers.Nodes {
|
|
838
|
+
completion.merge(executableNodeCompletion(modifier))
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
completion.merge(executableNodeCompletion(node.Name()))
|
|
842
|
+
return completion
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// noFallthroughIdentifierIsReference excludes declaration names, property
|
|
846
|
+
// names, and statement labels. Every remaining identifier is a value read,
|
|
847
|
+
// matching the first-throwable-node rule used by ESLint inside try blocks.
|
|
848
|
+
func noFallthroughIdentifierIsReference(node *shimast.Node) bool {
|
|
849
|
+
if node == nil || node.Parent == nil {
|
|
850
|
+
return true
|
|
851
|
+
}
|
|
852
|
+
parent := node.Parent
|
|
853
|
+
switch parent.Kind {
|
|
854
|
+
case shimast.KindJsxAttribute,
|
|
855
|
+
shimast.KindImportClause,
|
|
856
|
+
shimast.KindImportSpecifier,
|
|
857
|
+
shimast.KindNamespaceImport,
|
|
858
|
+
shimast.KindCatchClause:
|
|
859
|
+
return false
|
|
860
|
+
case shimast.KindArrayLiteralExpression:
|
|
861
|
+
return !isDestructuringAssignmentTarget(parent)
|
|
862
|
+
case shimast.KindSpreadElement, shimast.KindSpreadAssignment:
|
|
863
|
+
return !isDestructuringAssignmentTarget(parent)
|
|
864
|
+
case shimast.KindPropertyAccessExpression:
|
|
865
|
+
access := parent.AsPropertyAccessExpression()
|
|
866
|
+
return access == nil || access.Name() != node
|
|
867
|
+
case shimast.KindPropertyAssignment:
|
|
868
|
+
assignment := parent.AsPropertyAssignment()
|
|
869
|
+
return assignment == nil || assignment.Name() != node
|
|
870
|
+
case shimast.KindBindingElement:
|
|
871
|
+
element := parent.AsBindingElement()
|
|
872
|
+
if element == nil || (element.Name() != node && element.PropertyName != node) {
|
|
873
|
+
return true
|
|
874
|
+
}
|
|
875
|
+
if element.PropertyName == node || element.DotDotDotToken != nil {
|
|
876
|
+
return false
|
|
877
|
+
}
|
|
878
|
+
if element.Initializer != nil {
|
|
879
|
+
return true
|
|
880
|
+
}
|
|
881
|
+
return parent.Parent != nil && parent.Parent.Kind == shimast.KindObjectBindingPattern
|
|
882
|
+
case shimast.KindVariableDeclaration:
|
|
883
|
+
declaration := parent.AsVariableDeclaration()
|
|
884
|
+
return declaration == nil || declaration.Name() != node
|
|
885
|
+
case shimast.KindParameter:
|
|
886
|
+
parameter := parent.AsParameterDeclaration()
|
|
887
|
+
return parameter == nil || parameter.Name() != node
|
|
888
|
+
case shimast.KindFunctionDeclaration,
|
|
889
|
+
shimast.KindFunctionExpression,
|
|
890
|
+
shimast.KindClassDeclaration,
|
|
891
|
+
shimast.KindClassExpression,
|
|
892
|
+
shimast.KindMethodDeclaration,
|
|
893
|
+
shimast.KindPropertyDeclaration,
|
|
894
|
+
shimast.KindGetAccessor,
|
|
895
|
+
shimast.KindSetAccessor,
|
|
896
|
+
shimast.KindEnumDeclaration,
|
|
897
|
+
shimast.KindEnumMember,
|
|
898
|
+
shimast.KindModuleDeclaration:
|
|
899
|
+
return parent.Name() != node
|
|
900
|
+
case shimast.KindLabeledStatement:
|
|
901
|
+
statement := parent.AsLabeledStatement()
|
|
902
|
+
return statement == nil || statement.Label != node
|
|
903
|
+
case shimast.KindBreakStatement:
|
|
904
|
+
statement := parent.AsBreakStatement()
|
|
905
|
+
return statement == nil || statement.Label != node
|
|
906
|
+
case shimast.KindContinueStatement:
|
|
907
|
+
statement := parent.AsContinueStatement()
|
|
908
|
+
return statement == nil || statement.Label != node
|
|
909
|
+
}
|
|
910
|
+
return true
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// noFallthroughNodeIsJSXTagName excludes JSX tag names from Identifier and
|
|
914
|
+
// MemberExpression throw heuristics. ESTree represents them as JSXIdentifier /
|
|
915
|
+
// JSXMemberExpression rather than runtime Identifier / MemberExpression nodes;
|
|
916
|
+
// evaluated attribute and child expressions remain traversable.
|
|
917
|
+
func noFallthroughNodeIsJSXTagName(node *shimast.Node) bool {
|
|
918
|
+
current := node
|
|
919
|
+
for current != nil && current.Parent != nil && current.Parent.Kind == shimast.KindPropertyAccessExpression {
|
|
920
|
+
current = current.Parent
|
|
921
|
+
}
|
|
922
|
+
if current == nil || current.Parent == nil {
|
|
923
|
+
return false
|
|
924
|
+
}
|
|
925
|
+
switch current.Parent.Kind {
|
|
926
|
+
case shimast.KindJsxOpeningElement,
|
|
927
|
+
shimast.KindJsxSelfClosingElement,
|
|
928
|
+
shimast.KindJsxClosingElement:
|
|
929
|
+
return current.Parent.TagName() == current
|
|
930
|
+
default:
|
|
931
|
+
return false
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// literalTruthiness folds a loop test into a constant when it is a simple
|
|
936
|
+
// literal, mirroring ESLint's `getBooleanValueIfSimpleConstant` (ESTree
|
|
937
|
+
// `Literal` nodes only — template literals and negations are NOT folded).
|
|
938
|
+
// Returns (false, false) for non-literal or unparseable expressions.
|
|
939
|
+
func literalTruthiness(expr *shimast.Node) (constTrue, constFalse bool) {
|
|
940
|
+
expr = stripParens(expr)
|
|
941
|
+
if expr == nil {
|
|
942
|
+
return false, false
|
|
943
|
+
}
|
|
944
|
+
switch expr.Kind {
|
|
945
|
+
case shimast.KindTrueKeyword:
|
|
946
|
+
return true, false
|
|
947
|
+
case shimast.KindFalseKeyword, shimast.KindNullKeyword:
|
|
948
|
+
return false, true
|
|
949
|
+
case shimast.KindRegularExpressionLiteral:
|
|
950
|
+
return true, false
|
|
951
|
+
case shimast.KindStringLiteral:
|
|
952
|
+
if lit := expr.AsStringLiteral(); lit != nil {
|
|
953
|
+
return lit.Text != "", lit.Text == ""
|
|
954
|
+
}
|
|
955
|
+
case shimast.KindNumericLiteral:
|
|
956
|
+
if lit := expr.AsNumericLiteral(); lit != nil {
|
|
957
|
+
if value, err := strconv.ParseFloat(lit.Text, 64); err == nil {
|
|
958
|
+
return value != 0, value == 0
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
case shimast.KindBigIntLiteral:
|
|
962
|
+
if lit := expr.AsBigIntLiteral(); lit != nil {
|
|
963
|
+
return bigIntLiteralTruthiness(lit.Text)
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
return false, false
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// bigIntLiteralTruthiness determines zero/non-zero without converting the
|
|
970
|
+
// value to a fixed-width integer. The scanner can preserve any accepted
|
|
971
|
+
// radix prefix, digit separators, and arbitrarily wide values.
|
|
972
|
+
func bigIntLiteralTruthiness(text string) (constTrue, constFalse bool) {
|
|
973
|
+
if !strings.HasSuffix(text, "n") {
|
|
974
|
+
return false, false
|
|
975
|
+
}
|
|
976
|
+
digits := strings.ReplaceAll(strings.TrimSuffix(text, "n"), "_", "")
|
|
977
|
+
base := byte(10)
|
|
978
|
+
if len(digits) >= 2 && digits[0] == '0' {
|
|
979
|
+
switch digits[1] {
|
|
980
|
+
case 'b', 'B':
|
|
981
|
+
base, digits = 2, digits[2:]
|
|
982
|
+
case 'o', 'O':
|
|
983
|
+
base, digits = 8, digits[2:]
|
|
984
|
+
case 'x', 'X':
|
|
985
|
+
base, digits = 16, digits[2:]
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
if digits == "" {
|
|
989
|
+
return false, false
|
|
990
|
+
}
|
|
991
|
+
nonZero := false
|
|
992
|
+
for i := 0; i < len(digits); i++ {
|
|
993
|
+
digit := digits[i]
|
|
994
|
+
value := byte(255)
|
|
995
|
+
switch {
|
|
996
|
+
case digit >= '0' && digit <= '9':
|
|
997
|
+
value = digit - '0'
|
|
998
|
+
case digit >= 'a' && digit <= 'f':
|
|
999
|
+
value = digit - 'a' + 10
|
|
1000
|
+
case digit >= 'A' && digit <= 'F':
|
|
1001
|
+
value = digit - 'A' + 10
|
|
1002
|
+
}
|
|
1003
|
+
if value >= base {
|
|
1004
|
+
return false, false
|
|
1005
|
+
}
|
|
1006
|
+
nonZero = nonZero || value != 0
|
|
1007
|
+
}
|
|
1008
|
+
return nonZero, !nonZero
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
func init() {
|
|
1012
|
+
Register(noFallthrough{})
|
|
1013
|
+
}
|