Skip to content

Commit f3cdeb7

Browse files
committed
fix(lint): preserve explicit switch coverage semantics
Real defaults were hiding finite missing members while singleton and mixed open unions escaped the checker walk. Align switch coverage with the upstream scalar policies and keep generated branches as explicit editor suggestions. Constraint: Validation is delegated to targeted local experiments after rebasing and GitHub CI Rejected: Treat every default as exhaustive | violates the upstream default and issue reproduction Rejected: Reuse Finding.Fix for missing cases | would leak throw stubs into automatic fix-all Confidence: high Scope-risk: moderate Directive: Keep finite-member coverage and open-type default policy independent Tested: Exhaustive source review across production, shim, LSP, tests, fixtures, and docs Not-tested: Executable validation had not run at commit time Related: #416
1 parent b3cb5b2 commit f3cdeb7

26 files changed

Lines changed: 2139 additions & 171 deletions

packages/lint/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -445,7 +445,7 @@ Source: [`typescript-eslint`](https://github.com/typescript-eslint/typescript-es
445445
- [`typescript/return-await`](https://github.com/samchon/ttsc/blob/master/tests/test-lint/src/cases/return-await.ts): reject `return promise` inside `try`, `catch`, or `finally`; require `return await promise`.
446446
- [`typescript/sort-type-constituents`](https://github.com/samchon/ttsc/blob/master/tests/test-lint/src/cases/typescript-sort-type-constituents.ts): sort the members of union (`A | B | C`) and intersection (`A & B & C`) types into a canonical order so reorderings don't show up as diffs.
447447
- [`typescript/strict-boolean-expressions`](https://github.com/samchon/ttsc/blob/master/tests/test-lint/src/cases/typescript-strict-boolean-expressions.ts): rejects non-boolean values used in a boolean context such as `if`, `&&`, `||`, or `!` (type-aware).
448-
- [`typescript/switch-exhaustiveness-check`](https://github.com/samchon/ttsc/blob/master/tests/test-lint/src/cases/typescript-switch-exhaustiveness-check.ts): requires every member of a union or `enum` discriminant to be covered by a `case`, or a `default` clause to be present (type-aware).
448+
- [`typescript/switch-exhaustiveness-check`](https://github.com/samchon/ttsc/blob/master/tests/test-lint/src/cases/typescript-switch-exhaustiveness-check.ts): requires every enumerable discriminant member to have an explicit `case` by default; typed options control real/comment defaults and open types (type-aware).
449449
- [`typescript/triple-slash-reference`](https://github.com/samchon/ttsc/blob/master/tests/test-lint/src/cases/triple-slash-reference/violation.ts): rejects triple-slash reference directives.
450450
- [`typescript/unbound-method`](https://github.com/samchon/ttsc/blob/master/tests/test-lint/src/cases/typescript-unbound-method.ts): reject referencing a class instance method as a value instead of calling it (`obj.method` passed as a callback, aliased to a variable, or stored on another object).
451451
- [`typescript/use-unknown-in-catch-callback-variable`](https://github.com/samchon/ttsc/blob/master/tests/test-lint/src/cases/use-unknown-in-catch-callback-variable.ts): require the callback parameter of `.catch(...)` and the second argument of `.then(...)` to be typed `unknown`.

packages/lint/linthost/engine.go

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -120,17 +120,18 @@ func (c *Context) DecodeOptions(out interface{}) error {
120120
// both categories — no filter — because `ttsc fix` is the
121121
// run-everything entry point.
122122
type Finding struct {
123-
Rule string
124-
Severity Severity
125-
File *shimast.SourceFile
126-
Pos int
127-
End int
128-
Message string
129-
Fix []TextEdit
130-
IsFormat bool
131-
}
132-
133-
// TextEdit is one byte-range replacement offered by an autofixable finding.
123+
Rule string
124+
Severity Severity
125+
File *shimast.SourceFile
126+
Pos int
127+
End int
128+
Message string
129+
Fix []TextEdit
130+
Suggestions []Suggestion
131+
IsFormat bool
132+
}
133+
134+
// TextEdit is one byte-range source replacement used by a fix or suggestion.
134135
// Positions use the same byte offsets as shim AST nodes and must point inside
135136
// the finding's source file.
136137
type TextEdit struct {
@@ -139,6 +140,14 @@ type TextEdit struct {
139140
Text string
140141
}
141142

143+
// Suggestion is an opt-in editor action attached to a finding. Unlike Fix,
144+
// suggestion edits are never consumed by `ttsc fix` or source.fixAll.ttsc;
145+
// the LSP host exposes them as individual quick fixes selected by the user.
146+
type Suggestion struct {
147+
Title string
148+
Edits []TextEdit
149+
}
150+
142151
// Report records a finding at the given node's source range. The pos is
143152
// trimmed past leading trivia (whitespace + comments) so the renderer's
144153
// `path:line:col` banner points at the offending token, not the start of
@@ -170,6 +179,29 @@ func (c *Context) ReportFix(node *shimast.Node, message string, edits ...TextEdi
170179
})
171180
}
172181

182+
// ReportSuggestion records a node-scoped finding with one opt-in editor
183+
// action. The diagnostic is still reported when edits is empty, but no quick
184+
// fix is advertised.
185+
func (c *Context) ReportSuggestion(node *shimast.Node, message string, title string, edits ...TextEdit) {
186+
if c.Severity == SeverityOff || node == nil {
187+
return
188+
}
189+
pos := node.Pos()
190+
if c.File != nil {
191+
pos = shimscanner.SkipTrivia(c.File.Text(), pos)
192+
}
193+
c.collect(&Finding{
194+
Rule: c.rule.Name(),
195+
Severity: c.Severity,
196+
File: c.File,
197+
Pos: pos,
198+
End: node.End(),
199+
Message: message,
200+
Suggestions: newSuggestions(title, edits),
201+
IsFormat: c.isFormat,
202+
})
203+
}
204+
173205
// ReportRange records a finding at an explicit byte range inside the
174206
// current file. Use this when the rule wants to highlight a sub-token of
175207
// a node (e.g. an operator inside a BinaryExpression).
@@ -210,6 +242,14 @@ func cloneTextEdits(edits []TextEdit) []TextEdit {
210242
return out
211243
}
212244

245+
func newSuggestions(title string, edits []TextEdit) []Suggestion {
246+
cloned := cloneTextEdits(edits)
247+
if title == "" || len(cloned) == 0 {
248+
return nil
249+
}
250+
return []Suggestion{{Title: title, Edits: cloned}}
251+
}
252+
213253
// registry stores the package-global rule list keyed by name. Tests can
214254
// also reach into it via `LookupRule`.
215255
type registry struct {

0 commit comments

Comments
 (0)