@ttsc/lint 0.19.3 → 0.20.0
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 +1 -1
- package/lib/index.js +26 -4
- package/lib/index.js.map +1 -1
- package/lib/structures/format/ITtscLintFormat.d.ts +14 -10
- package/lib/structures/rules/ITtscLintRegexpRules.d.ts +24 -9
- package/lib/structures/rules/ITtscLintSecurityRules.d.ts +18 -0
- package/lib/structures/rules/ITtscLintSolidRules.d.ts +22 -2
- package/lib/structures/rules/ITtscLintStorybookRules.d.ts +21 -0
- package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.d.ts +10 -1
- package/lib/structures/rules/ITtscLintTypeScriptRules.d.ts +13 -2
- package/linthost/compile.go +9 -4
- package/linthost/config.go +18 -14
- package/linthost/contrib_adapter.go +57 -0
- package/linthost/dispatch.go +5 -1
- package/linthost/display_width.go +179 -41
- package/linthost/engine.go +120 -4
- package/linthost/flags_gen.go +4 -4
- package/linthost/hints.go +207 -0
- package/linthost/host.go +68 -0
- package/linthost/lsp.go +148 -29
- package/linthost/print_dispatch.go +24 -0
- package/linthost/print_nodes_array.go +88 -15
- package/linthost/print_nodes_call.go +38 -31
- package/linthost/print_nodes_control_flow.go +319 -0
- package/linthost/print_nodes_function.go +140 -13
- package/linthost/print_nodes_list.go +22 -10
- package/linthost/project_engine.go +18 -1
- package/linthost/project_rules.go +47 -0
- package/linthost/rule_docs.go +114 -0
- package/linthost/rules_boundaries.go +80 -2
- package/linthost/rules_format_bracket_spacing.go +18 -6
- package/linthost/rules_format_clause_join.go +11 -8
- package/linthost/rules_format_indent.go +155 -4
- package/linthost/rules_format_print_width.go +12 -35
- package/linthost/rules_format_quote_props.go +88 -27
- package/linthost/rules_format_statement_split.go +81 -0
- package/linthost/rules_format_trailing_comma.go +62 -94
- package/linthost/rules_gap.go +26 -17
- package/linthost/rules_jsdoc.go +54 -0
- package/linthost/rules_logic.go +30 -15
- package/linthost/rules_no_redeclare.go +12 -3
- package/linthost/rules_promise.go +37 -15
- package/linthost/rules_regexp.go +443 -49
- package/linthost/rules_security.go +243 -4
- package/linthost/rules_solid.go +430 -10
- package/linthost/rules_storybook.go +255 -10
- package/linthost/rules_suggestions.go +60 -39
- package/linthost/rules_ts.go +7 -0
- package/linthost/rules_ts_async.go +80 -2
- package/linthost/rules_ts_extra.go +21 -7
- package/linthost/serve.go +318 -0
- package/linthost/width_tables_gen.go +219 -0
- package/package.json +2 -2
- package/rule/hint.go +142 -0
- package/rule/rule.go +197 -1
- package/src/index.ts +35 -4
- package/src/structures/format/ITtscLintFormat.ts +14 -10
- package/src/structures/rules/ITtscLintRegexpRules.ts +24 -9
- package/src/structures/rules/ITtscLintSecurityRules.ts +18 -0
- package/src/structures/rules/ITtscLintSolidRules.ts +22 -2
- package/src/structures/rules/ITtscLintStorybookRules.ts +21 -0
- package/src/structures/rules/ITtscLintTypeScriptRuleOptions.ts +10 -1
- package/src/structures/rules/ITtscLintTypeScriptRules.ts +13 -2
package/rule/hint.go
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
package rule
|
|
2
|
+
|
|
3
|
+
import "encoding/json"
|
|
4
|
+
|
|
5
|
+
// HintScope names the syntactic region a hint is offered in. It exists because
|
|
6
|
+
// a line prefix alone cannot tell `@evidence` in a doc comment from
|
|
7
|
+
// `@Injectable` above a class: both lines end in `@`, and a corpus that ignored
|
|
8
|
+
// the difference would offer doc-comment tags in every decorator position.
|
|
9
|
+
//
|
|
10
|
+
// One value ships today. The field is present anyway: a hint with no scope
|
|
11
|
+
// would mean "anywhere on any line", which is never what a rule meant, and
|
|
12
|
+
// widening that default later would break every corpus already published.
|
|
13
|
+
type HintScope string
|
|
14
|
+
|
|
15
|
+
const (
|
|
16
|
+
// HintScopeJSDoc offers the hint inside a `/** */` documentation comment.
|
|
17
|
+
HintScopeJSDoc HintScope = "jsdoc"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
// HintTrigger is the declarative answer to "does this hint apply at the
|
|
21
|
+
// cursor?".
|
|
22
|
+
//
|
|
23
|
+
// It must be declarative because the rule that produced it is gone. The lint
|
|
24
|
+
// engine is a separate process that reloads the Program on every invocation, so
|
|
25
|
+
// nothing can ask a rule a question per keystroke. A Go predicate is the obvious
|
|
26
|
+
// API and is exactly the one that cannot ship: it does not survive the process
|
|
27
|
+
// boundary. Every editor-assistance system that lets a rule answer live does so
|
|
28
|
+
// in-process on a shared AST; this host cannot, so the corpus travels instead of
|
|
29
|
+
// the question.
|
|
30
|
+
//
|
|
31
|
+
// The host matches a trigger against the current line up to the cursor: the hint
|
|
32
|
+
// applies when the cursor sits inside Scope and the line prefix contains After.
|
|
33
|
+
// Text following the LAST occurrence of After is the filter the editor matches
|
|
34
|
+
// against, and the range the completion replaces. Two consequences to design
|
|
35
|
+
// around:
|
|
36
|
+
//
|
|
37
|
+
// After must end exactly where the completed token begins — `"@evidence "` with
|
|
38
|
+
// its trailing space, not `"@evidence"` — or the token swallows the separator
|
|
39
|
+
// and nothing filters.
|
|
40
|
+
//
|
|
41
|
+
// When several triggers match one line, the occurrence nearest the cursor wins.
|
|
42
|
+
// At that occurrence, the longest After wins, and only hints with that same
|
|
43
|
+
// trigger merge. That keeps a corpus layerable while preventing an earlier,
|
|
44
|
+
// longer trigger from drowning a later one.
|
|
45
|
+
type HintTrigger struct {
|
|
46
|
+
Scope HintScope `json:"scope"`
|
|
47
|
+
After string `json:"after"`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Hint is one completion an editor may offer.
|
|
51
|
+
//
|
|
52
|
+
// It is a value, not a behavior: the host serializes the corpus and hands it to
|
|
53
|
+
// the LSP proxy, which answers from cache long after the lint process exited. A
|
|
54
|
+
// closure, a channel, or an AST node cannot be carried here, and that constraint
|
|
55
|
+
// is the whole shape of the type.
|
|
56
|
+
type Hint struct {
|
|
57
|
+
// Insert is the text replacing the token being completed. Plain text,
|
|
58
|
+
// inserted verbatim: there is no snippet expansion, so `$` and tabs are
|
|
59
|
+
// literal.
|
|
60
|
+
Insert string `json:"insert"`
|
|
61
|
+
|
|
62
|
+
// Label is what the editor lists and filters on. Empty means Insert, which
|
|
63
|
+
// is the common case. Set it only when the two genuinely differ, and
|
|
64
|
+
// remember the filter is what the user typed AFTER the trigger: a Label
|
|
65
|
+
// repeating the trigger text will not prefix-match anything.
|
|
66
|
+
Label string `json:"label,omitempty"`
|
|
67
|
+
|
|
68
|
+
// Detail is a short annotation rendered beside Label. Use it for the fact
|
|
69
|
+
// distinguishing two similar entries — a heading's text, a count. It is not
|
|
70
|
+
// documentation: editors truncate it, so a sentence is wasted.
|
|
71
|
+
Detail string `json:"detail,omitempty"`
|
|
72
|
+
|
|
73
|
+
// Trigger is where this hint applies. A zero Trigger is dropped by the host
|
|
74
|
+
// rather than offered everywhere: a hint with no scope is one nobody asked
|
|
75
|
+
// for, surfacing in every decorator and every string literal.
|
|
76
|
+
Trigger HintTrigger `json:"trigger"`
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// HintContext is the read-only handle the host passes to Hints.
|
|
80
|
+
//
|
|
81
|
+
// It carries State because a rule value is stateless: contributors register
|
|
82
|
+
// `myRule{}`, not a pointer with fields, and the host owns everything Check
|
|
83
|
+
// produced. Without State here, Hints could only ever return constants.
|
|
84
|
+
type HintContext struct {
|
|
85
|
+
// Identity names the Program this corpus is built for, as during Check.
|
|
86
|
+
Identity ProjectIdentity
|
|
87
|
+
|
|
88
|
+
// State is the value the rule passed to ProjectContext.SetState.
|
|
89
|
+
// Type-assert it back, exactly as a file rule does with
|
|
90
|
+
// ProjectRuleResult.State. The host calls Hints only for a rule that passed
|
|
91
|
+
// and published, so a failed assertion means the rule published something
|
|
92
|
+
// other than it believes.
|
|
93
|
+
State any
|
|
94
|
+
|
|
95
|
+
// Severity and Options are the resolved configuration Check ran under,
|
|
96
|
+
// repeated so a rule shaping its corpus by option need not stash a decoded
|
|
97
|
+
// struct inside State.
|
|
98
|
+
Severity Severity
|
|
99
|
+
Options json.RawMessage
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// DecodeOptions unmarshals the configured options into out. A missing options
|
|
103
|
+
// tuple leaves out unchanged and returns nil.
|
|
104
|
+
func (c *HintContext) DecodeOptions(out interface{}) error {
|
|
105
|
+
if c == nil || len(c.Options) == 0 {
|
|
106
|
+
return nil
|
|
107
|
+
}
|
|
108
|
+
return json.Unmarshal(c.Options, out)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// HintRule is an optional marker a ProjectRule implements to publish editor
|
|
112
|
+
// completions for the Program it just indexed.
|
|
113
|
+
//
|
|
114
|
+
// The host calls Hints at most once per Program, always after Check, and only
|
|
115
|
+
// when a consumer asks for the corpus — never during `ttsc check`. It is not
|
|
116
|
+
// called unless Check passed and published state, the same gate a file rule
|
|
117
|
+
// writes by hand against ProjectRulePassed. A rule configured off is never
|
|
118
|
+
// asked, so `off` means no hints with no code in the rule, and a rule's options
|
|
119
|
+
// shape its corpus for free because the corpus is a projection of the state
|
|
120
|
+
// Check built under them.
|
|
121
|
+
//
|
|
122
|
+
// Pull, not push. Report is push because a finding is discovered mid-walk and
|
|
123
|
+
// belongs to the node under it. A corpus is the opposite: a projection of
|
|
124
|
+
// FINISHED state. A rule pushing hints while building that state would publish
|
|
125
|
+
// the anchors it had found so far rather than the ones the document has.
|
|
126
|
+
//
|
|
127
|
+
// The corpus outlives the process, so slice order is the only ranking channel
|
|
128
|
+
// there is — the host preserves it and derives the editor's sort key from it.
|
|
129
|
+
// Return what should be offered first, first. Nothing else about a Hint
|
|
130
|
+
// influences ordering, by design: a sort key field would be a second, silently
|
|
131
|
+
// conflicting answer to a question the slice already answers.
|
|
132
|
+
//
|
|
133
|
+
// This embeds ProjectRule rather than standing alone as OptionsRule does,
|
|
134
|
+
// because a per-file corpus is not a coherent thing. File rules run in a
|
|
135
|
+
// parallel walk, so their hints would arrive — and therefore rank —
|
|
136
|
+
// nondeterministically, and a corpus keyed to one file cannot answer a keystroke
|
|
137
|
+
// in another. A contributor wanting hints from file-level facts registers a
|
|
138
|
+
// ProjectRule alongside, which is what those facts wanted anyway.
|
|
139
|
+
type HintRule interface {
|
|
140
|
+
ProjectRule
|
|
141
|
+
Hints(ctx *HintContext) []Hint
|
|
142
|
+
}
|
package/rule/rule.go
CHANGED
|
@@ -112,6 +112,44 @@ type DeclarationFileRule interface {
|
|
|
112
112
|
VisitsDeclarationFiles() bool
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
// DiagnosticTag classifies what a finding IS, orthogonally to how severe it is.
|
|
116
|
+
// The values match the LSP DiagnosticTag enum, and an editor renders them
|
|
117
|
+
// distinctively: unnecessary code is greyed out, deprecated code struck through.
|
|
118
|
+
type DiagnosticTag int
|
|
119
|
+
|
|
120
|
+
const (
|
|
121
|
+
// DiagnosticTagUnnecessary marks code that is safe to delete — an unused
|
|
122
|
+
// import, an unreachable branch. The editor fades it.
|
|
123
|
+
//
|
|
124
|
+
// This is a claim about what the code is, not how bad it is, and the
|
|
125
|
+
// distinction bites: "unnecessary" says "remove this." A finding that means
|
|
126
|
+
// "this is not done yet" is the opposite and must never carry it, or the
|
|
127
|
+
// editor tells the author to delete the work they have not finished. Tag by
|
|
128
|
+
// what deletion would mean, never by severity.
|
|
129
|
+
DiagnosticTagUnnecessary DiagnosticTag = 1
|
|
130
|
+
// DiagnosticTagDeprecated marks code that still works but should be migrated
|
|
131
|
+
// away from. The editor strikes it through.
|
|
132
|
+
DiagnosticTagDeprecated DiagnosticTag = 2
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
// TaggedRule is an optional marker a rule implements to classify its findings
|
|
136
|
+
// with DiagnosticTags. Every finding the rule produces carries the returned
|
|
137
|
+
// tags — the rule-level grain fits the rules that want this, since a rule that
|
|
138
|
+
// flags unused code flags only unused code.
|
|
139
|
+
//
|
|
140
|
+
// It is separate from severity on purpose. Severity is how much a finding
|
|
141
|
+
// matters and is the user's to configure; a tag is what the finding is and is
|
|
142
|
+
// the rule's to state. A host that does not read tags loses the greying, not the
|
|
143
|
+
// diagnostic — the same graceful degradation the other optional markers give.
|
|
144
|
+
//
|
|
145
|
+
// Return nil, or do not implement it, for a rule whose findings are neither
|
|
146
|
+
// unnecessary nor deprecated. Most findings are neither, and guessing wrong is
|
|
147
|
+
// worse than saying nothing: a spurious Unnecessary tells the author to delete
|
|
148
|
+
// correct code.
|
|
149
|
+
type TaggedRule interface {
|
|
150
|
+
DiagnosticTags() []DiagnosticTag
|
|
151
|
+
}
|
|
152
|
+
|
|
115
153
|
// TypeAwareRule is an optional marker contributors implement to declare
|
|
116
154
|
// whether their rule reads `Context.Checker`. The host cannot infer a
|
|
117
155
|
// third-party rule's shape, so a contributor that does not implement this
|
|
@@ -128,8 +166,12 @@ type DeclarationFileRule interface {
|
|
|
128
166
|
// preserving the engine's parallel file walk. Returning `true` is equivalent
|
|
129
167
|
// to not implementing the interface at all. A rule that returns `false` must
|
|
130
168
|
// not read `Context.Checker`: the host is free to leave it nil.
|
|
169
|
+
//
|
|
170
|
+
// The method name is domain-specific so an unrelated generic method on an
|
|
171
|
+
// existing contributor cannot opt out by accident. ProjectRule implementations
|
|
172
|
+
// may use the same marker; the serial walk it governs is engine-wide, so one
|
|
173
|
+
// type-aware project rule serializes every file rule in the run.
|
|
131
174
|
type TypeAwareRule interface {
|
|
132
|
-
Rule
|
|
133
175
|
NeedsTypeChecker() bool
|
|
134
176
|
}
|
|
135
177
|
|
|
@@ -176,6 +218,43 @@ type FixReporter interface {
|
|
|
176
218
|
ReportRangeFix(pos, end int, message string, edits ...TextEdit)
|
|
177
219
|
}
|
|
178
220
|
|
|
221
|
+
// RelatedReporter is the optional extension a host implements to receive a
|
|
222
|
+
// finding's related source locations. Like FixReporter, the public
|
|
223
|
+
// `rule.Context` type-asserts against this shape, so any host whose reporter
|
|
224
|
+
// exposes both methods opts into related locations without depending on a
|
|
225
|
+
// private interface name. A host that does not implement it loses the related
|
|
226
|
+
// locations, not the diagnostic — the same graceful degradation the other
|
|
227
|
+
// optional reporter extensions give.
|
|
228
|
+
//
|
|
229
|
+
// Rule production code does NOT touch RelatedReporter directly — call
|
|
230
|
+
// `ctx.ReportRelated` / `ctx.ReportRangeRelated`, and the host's reporter
|
|
231
|
+
// receives the locations. The only place a contributor sees this interface is in
|
|
232
|
+
// test code that fakes the reporter: such a fake must implement BOTH `Reporter`
|
|
233
|
+
// AND `RelatedReporter` to observe the locations, because Go interface
|
|
234
|
+
// satisfaction is all-or-nothing. Declaring `var _ rule.RelatedReporter =
|
|
235
|
+
// &myFake{}` compile-checks the fake covers the related surface.
|
|
236
|
+
type RelatedReporter interface {
|
|
237
|
+
ReportRelated(node *shimast.Node, message string, related ...RelatedInformation)
|
|
238
|
+
ReportRangeRelated(pos, end int, message string, related ...RelatedInformation)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// RelatedInformation is a secondary source location a finding points at, paired
|
|
242
|
+
// with a message naming the connection. LSP renders each as its own clickable
|
|
243
|
+
// line beneath the diagnostic, so "'x' is already defined." can lead the reader
|
|
244
|
+
// to the first definition instead of only naming it.
|
|
245
|
+
//
|
|
246
|
+
// Pos/End are byte offsets into the CURRENT file — the same offsets a shim AST
|
|
247
|
+
// node exposes and `ReportRange` consumes — so a related location lives in the
|
|
248
|
+
// file the finding is in, and the host fills in that file's URI. A location in
|
|
249
|
+
// ANOTHER file would need a URI this API does not yet carry, and is a separate
|
|
250
|
+
// extension left deliberately out of scope so the same-file case ships without
|
|
251
|
+
// waiting on it.
|
|
252
|
+
type RelatedInformation struct {
|
|
253
|
+
Pos int
|
|
254
|
+
End int
|
|
255
|
+
Message string
|
|
256
|
+
}
|
|
257
|
+
|
|
179
258
|
// TextEdit is one byte-range replacement offered by an autofixable finding.
|
|
180
259
|
// Positions use the same byte offsets as shim AST nodes and must point inside
|
|
181
260
|
// the current source file. An empty `Text` deletes the range; positions are
|
|
@@ -198,6 +277,42 @@ type TextEdit struct {
|
|
|
198
277
|
Text string
|
|
199
278
|
}
|
|
200
279
|
|
|
280
|
+
// Suggestion is one of several candidate fixes offered for a finding, each with
|
|
281
|
+
// its own title. It exists for the case `ReportFix` cannot serve: when a rule
|
|
282
|
+
// knows more than one valid repair and cannot pick among them for the author.
|
|
283
|
+
//
|
|
284
|
+
// The distinction is the same one the built-in rules already draw and, until
|
|
285
|
+
// now, kept to themselves. A fix is imposed; a suggestion is chosen. A rule that
|
|
286
|
+
// found three valid renames must either impose one arbitrarily through
|
|
287
|
+
// `ReportFix` or describe the three in prose and offer none — both worse than
|
|
288
|
+
// letting the editor present the choice, which is what the built-ins do through
|
|
289
|
+
// this shape and contributors could not reach.
|
|
290
|
+
//
|
|
291
|
+
// Edits within one Suggestion follow the same non-overlap policy as `TextEdit`
|
|
292
|
+
// in a `ReportFix` call.
|
|
293
|
+
type Suggestion struct {
|
|
294
|
+
// Title is what the editor shows for this choice, e.g. "Rename to `frames`".
|
|
295
|
+
Title string
|
|
296
|
+
// Edits apply this suggestion. Empty means the suggestion is a label with no
|
|
297
|
+
// edit — a "did you mean" the author acts on by hand.
|
|
298
|
+
Edits []TextEdit
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// SuggestionReporter is the optional half of the reporter a host implements to
|
|
302
|
+
// carry suggestions. A host that does not implement it still receives the
|
|
303
|
+
// finding through `Reporter`, without the choices — the same graceful
|
|
304
|
+
// degradation `FixReporter` gives autofixes.
|
|
305
|
+
//
|
|
306
|
+
// It is separate from `FixReporter` rather than folded into it because the two
|
|
307
|
+
// answer different questions: `ReportFix` offers the one right rewrite, this
|
|
308
|
+
// offers a choice among several. A rule reaches it through
|
|
309
|
+
// `Context.ReportSuggestion` / `ReportRangeSuggestion`; it is not called
|
|
310
|
+
// directly.
|
|
311
|
+
type SuggestionReporter interface {
|
|
312
|
+
ReportSuggestion(node *shimast.Node, message string, suggestions ...Suggestion)
|
|
313
|
+
ReportRangeSuggestion(pos, end int, message string, suggestions ...Suggestion)
|
|
314
|
+
}
|
|
315
|
+
|
|
201
316
|
// Context is the per-(file, rule) handle the engine passes to `Check`.
|
|
202
317
|
// The `Reporter` is supplied by the host when constructing the context;
|
|
203
318
|
// contributors call `ctx.Report` / `ctx.ReportRange` directly through
|
|
@@ -342,6 +457,87 @@ func (c *Context) ReportRangeFix(pos, end int, message string, edits ...TextEdit
|
|
|
342
457
|
fixer.ReportRangeFix(pos, end, message, edits...)
|
|
343
458
|
}
|
|
344
459
|
|
|
460
|
+
// ReportSuggestion records a finding at the node's range with a choice of
|
|
461
|
+
// candidate fixes. A host that does not implement `SuggestionReporter` receives
|
|
462
|
+
// the diagnostic without the choices, so design the rule so the message alone is
|
|
463
|
+
// useful — the same best-effort contract as `ReportFix`.
|
|
464
|
+
//
|
|
465
|
+
// Use this over `ReportFix` only when there genuinely is a choice. One correct
|
|
466
|
+
// rewrite is a fix; imposing it is the right thing. Several valid rewrites is a
|
|
467
|
+
// suggestion; imposing one arbitrarily is not.
|
|
468
|
+
func (c *Context) ReportSuggestion(node *shimast.Node, message string, suggestions ...Suggestion) {
|
|
469
|
+
if c == nil || c.reporter == nil || c.Severity == SeverityOff || node == nil {
|
|
470
|
+
return
|
|
471
|
+
}
|
|
472
|
+
if len(suggestions) == 0 {
|
|
473
|
+
c.reporter.Report(node, message)
|
|
474
|
+
return
|
|
475
|
+
}
|
|
476
|
+
suggester, ok := c.reporter.(SuggestionReporter)
|
|
477
|
+
if !ok {
|
|
478
|
+
c.reporter.Report(node, message)
|
|
479
|
+
return
|
|
480
|
+
}
|
|
481
|
+
suggester.ReportSuggestion(node, message, suggestions...)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// ReportRangeSuggestion records a finding at an explicit byte range with a
|
|
485
|
+
// choice of candidate fixes. See `ReportSuggestion`.
|
|
486
|
+
func (c *Context) ReportRangeSuggestion(pos, end int, message string, suggestions ...Suggestion) {
|
|
487
|
+
if c == nil || c.reporter == nil || c.Severity == SeverityOff {
|
|
488
|
+
return
|
|
489
|
+
}
|
|
490
|
+
if len(suggestions) == 0 {
|
|
491
|
+
c.reporter.ReportRange(pos, end, message)
|
|
492
|
+
return
|
|
493
|
+
}
|
|
494
|
+
suggester, ok := c.reporter.(SuggestionReporter)
|
|
495
|
+
if !ok {
|
|
496
|
+
c.reporter.ReportRange(pos, end, message)
|
|
497
|
+
return
|
|
498
|
+
}
|
|
499
|
+
suggester.ReportRangeSuggestion(pos, end, message, suggestions...)
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// ReportRelated records a finding at the given node's source range with related
|
|
503
|
+
// source locations. Older hosts that do not implement RelatedReporter receive
|
|
504
|
+
// the diagnostic without them, so design the rule to read well from the message
|
|
505
|
+
// alone. With no related locations it is exactly `Report`.
|
|
506
|
+
func (c *Context) ReportRelated(node *shimast.Node, message string, related ...RelatedInformation) {
|
|
507
|
+
if c == nil || c.reporter == nil || c.Severity == SeverityOff || node == nil {
|
|
508
|
+
return
|
|
509
|
+
}
|
|
510
|
+
if len(related) == 0 {
|
|
511
|
+
c.reporter.Report(node, message)
|
|
512
|
+
return
|
|
513
|
+
}
|
|
514
|
+
reporter, ok := c.reporter.(RelatedReporter)
|
|
515
|
+
if !ok {
|
|
516
|
+
c.reporter.Report(node, message)
|
|
517
|
+
return
|
|
518
|
+
}
|
|
519
|
+
reporter.ReportRelated(node, message, related...)
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// ReportRangeRelated records a finding at an explicit byte range with related
|
|
523
|
+
// source locations. Falls back to a plain range finding on a host without
|
|
524
|
+
// RelatedReporter, and equals `ReportRange` when no related locations are given.
|
|
525
|
+
func (c *Context) ReportRangeRelated(pos, end int, message string, related ...RelatedInformation) {
|
|
526
|
+
if c == nil || c.reporter == nil || c.Severity == SeverityOff {
|
|
527
|
+
return
|
|
528
|
+
}
|
|
529
|
+
if len(related) == 0 {
|
|
530
|
+
c.reporter.ReportRange(pos, end, message)
|
|
531
|
+
return
|
|
532
|
+
}
|
|
533
|
+
reporter, ok := c.reporter.(RelatedReporter)
|
|
534
|
+
if !ok {
|
|
535
|
+
c.reporter.ReportRange(pos, end, message)
|
|
536
|
+
return
|
|
537
|
+
}
|
|
538
|
+
reporter.ReportRangeRelated(pos, end, message, related...)
|
|
539
|
+
}
|
|
540
|
+
|
|
345
541
|
var registry []Rule
|
|
346
542
|
|
|
347
543
|
// Register adds a contributor rule to the global registry. Called from a
|
package/src/index.ts
CHANGED
|
@@ -223,10 +223,10 @@ function resolveConfigFileContributors(
|
|
|
223
223
|
if (!configPath || !fs.existsSync(configPath)) return [];
|
|
224
224
|
|
|
225
225
|
const entries = readConfigPluginEntries(configPath, context);
|
|
226
|
-
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
//
|
|
226
|
+
assertContributorNamespacesDoNotCollide(entries, configPath);
|
|
227
|
+
// Dedup exact repeated namespaces on the Go-subpackage form. Config-array
|
|
228
|
+
// folding can surface the same namespace more than once; that existing
|
|
229
|
+
// behavior stays intact after distinct namespaces are rejected above.
|
|
230
230
|
const occupied = new Set<string>();
|
|
231
231
|
const out: TtscPluginContributor[] = [];
|
|
232
232
|
for (const entry of entries) {
|
|
@@ -238,6 +238,37 @@ function resolveConfigFileContributors(
|
|
|
238
238
|
return out;
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
+
function assertContributorNamespacesDoNotCollide(
|
|
242
|
+
entries: ConfigPluginEntry[],
|
|
243
|
+
configPath: string,
|
|
244
|
+
): void {
|
|
245
|
+
const namespacesByGoName = new Map<string, Set<string>>();
|
|
246
|
+
for (const entry of entries) {
|
|
247
|
+
const goName = goSubpackageName(entry.namespace);
|
|
248
|
+
let namespaces = namespacesByGoName.get(goName);
|
|
249
|
+
if (namespaces === undefined) {
|
|
250
|
+
namespaces = new Set<string>();
|
|
251
|
+
namespacesByGoName.set(goName, namespaces);
|
|
252
|
+
}
|
|
253
|
+
namespaces.add(entry.namespace);
|
|
254
|
+
}
|
|
255
|
+
const collisions = [...namespacesByGoName]
|
|
256
|
+
.map(([goName, namespaces]) => [goName, [...namespaces].sort()] as const)
|
|
257
|
+
.filter(([, namespaces]) => namespaces.length > 1)
|
|
258
|
+
.sort(([left], [right]) => left.localeCompare(right));
|
|
259
|
+
if (collisions.length === 0) return;
|
|
260
|
+
|
|
261
|
+
const details = collisions
|
|
262
|
+
.map(
|
|
263
|
+
([goName, namespaces]) =>
|
|
264
|
+
`${namespaces.map((namespace) => JSON.stringify(namespace)).join(", ")} all normalize to ${JSON.stringify(goName)}`,
|
|
265
|
+
)
|
|
266
|
+
.join("; ");
|
|
267
|
+
throw new Error(
|
|
268
|
+
`@ttsc/lint: lint config ${configPath} contributor namespaces collide after Go normalization: ${details}`,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
241
272
|
/**
|
|
242
273
|
* Rejects any tsconfig plugin-entry key that is neither a host framework key
|
|
243
274
|
* nor the single lint-specific `configFile` key. Rule, format, and plugin
|
|
@@ -61,28 +61,32 @@ export interface ITtscLintFormat {
|
|
|
61
61
|
* Pad the inside of single-line braces with one space. Mirrors Prettier's
|
|
62
62
|
* `bracketSpacing`. `true` (the default) gives `{ x: 1 }`, `import { foo }`;
|
|
63
63
|
* `false` gives `{x: 1}`, `import {foo}`. Applies to object literals, object
|
|
64
|
-
* destructuring patterns, named imports/exports,
|
|
65
|
-
* class, interface, and enum braces are
|
|
64
|
+
* destructuring patterns, named imports/exports, type literals, mapped types,
|
|
65
|
+
* and import attributes; block, class, interface, and enum braces are
|
|
66
|
+
* unaffected.
|
|
66
67
|
*
|
|
67
68
|
* @default true
|
|
68
69
|
*/
|
|
69
70
|
bracketSpacing?: boolean;
|
|
70
71
|
|
|
71
72
|
/**
|
|
72
|
-
* Quoting policy for object-literal
|
|
73
|
-
* `quoteProps`. `"as-needed"` (the default) removes
|
|
74
|
-
* a valid identifier (`{ "foo": 1 }` becomes `{
|
|
75
|
-
* non-identifier or numeric keys (`"bar-baz"`,
|
|
76
|
-
*
|
|
77
|
-
* changes quoting.
|
|
73
|
+
* Quoting policy for object-literal keys plus class-method and type-member
|
|
74
|
+
* names. Mirrors Prettier's `quoteProps`. `"as-needed"` (the default) removes
|
|
75
|
+
* quotes from a key that is a valid identifier (`{ "foo": 1 }` becomes `{
|
|
76
|
+
* foo: 1 }`), keeping them on non-identifier or numeric keys (`"bar-baz"`,
|
|
77
|
+
* `"123"`). `"consistent"` quotes every object-literal identifier key when a
|
|
78
|
+
* sibling requires quotes. `"preserve"` never changes quoting. ttsc keeps
|
|
79
|
+
* `"__proto__"` and non-ASCII identifier keys quoted because unquoting can
|
|
80
|
+
* change runtime semantics or exceed its conservative identifier policy.
|
|
78
81
|
*
|
|
79
82
|
* @default "as-needed"
|
|
80
83
|
*/
|
|
81
84
|
quoteProps?: "as-needed" | "consistent" | "preserve";
|
|
82
85
|
|
|
83
86
|
/**
|
|
84
|
-
* Trailing-comma policy. Mirrors Prettier's `trailingComma`.
|
|
85
|
-
*
|
|
87
|
+
* Trailing-comma policy. Mirrors Prettier's `trailingComma`. `"none"` removes
|
|
88
|
+
* an existing governed trailing comma, `"es5"` keeps it on ES5-level lists,
|
|
89
|
+
* and `"all"` also keeps it on calls and parameter lists.
|
|
86
90
|
*
|
|
87
91
|
* @default "all"
|
|
88
92
|
*/
|
|
@@ -103,7 +103,8 @@ export interface ITtscLintRegexpRules {
|
|
|
103
103
|
/**
|
|
104
104
|
* Reject regex flags that the literal does not exercise — `i` on a pattern
|
|
105
105
|
* with no case-variable character, `m` on a pattern with no `^`/`$`
|
|
106
|
-
* assertion.
|
|
106
|
+
* assertion. Autofixable: the diagnostic names the dead flags and the fix
|
|
107
|
+
* deletes exactly those, leaving the live ones in place.
|
|
107
108
|
*
|
|
108
109
|
* Cleans up flag combos that suggest behavior the pattern can never trigger.
|
|
109
110
|
* A character is case-variable wherever it sits, so `/[a-z]/i` keeps its flag
|
|
@@ -121,12 +122,18 @@ export interface ITtscLintRegexpRules {
|
|
|
121
122
|
* (`/a{1}/`), `?` on patterns already matching the empty string
|
|
122
123
|
* (`/(?:a+|b*)?/`), and quantifiers on non-consuming atoms (`/(?:\b)+/`).
|
|
123
124
|
*
|
|
125
|
+
* Autofixable for the constant-one count, except where the next character
|
|
126
|
+
* would change the meaning of the deletion: a lazy marker (`/a{1}?/` is
|
|
127
|
+
* "exactly one", not optional) or a digit (`/\1{1}2/` would fuse into
|
|
128
|
+
* backreference twelve).
|
|
129
|
+
*
|
|
124
130
|
* @reference https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-quantifier.html
|
|
125
131
|
*/
|
|
126
132
|
"regexp/no-useless-quantifier"?: TtscLintRuleSetting;
|
|
127
133
|
|
|
128
134
|
/**
|
|
129
135
|
* Reject equal min/max quantifiers (`/a{2,2}/`) in favor of `/a{2}/`.
|
|
136
|
+
* Autofixable.
|
|
130
137
|
*
|
|
131
138
|
* @reference https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-two-nums-quantifier.html
|
|
132
139
|
*/
|
|
@@ -136,42 +143,45 @@ export interface ITtscLintRegexpRules {
|
|
|
136
143
|
* Reject zero-repeat quantifiers (`/a{0}/`, `/a{0,0}/`) — the atom never
|
|
137
144
|
* matches, so the quantifier is either dead code or a typo for `{1,…}`.
|
|
138
145
|
*
|
|
139
|
-
*
|
|
146
|
+
* Diagnostic-only: the correction is to delete the atom or repair the bound,
|
|
147
|
+
* and which one was meant is not recoverable from the source.
|
|
140
148
|
*
|
|
141
149
|
* @reference https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-zero-quantifier.html
|
|
142
150
|
*/
|
|
143
151
|
"regexp/no-zero-quantifier"?: TtscLintRuleSetting;
|
|
144
152
|
|
|
145
153
|
/**
|
|
146
|
-
* Prefer `\d` over `[0-9]` in regex literals.
|
|
154
|
+
* Prefer `\d` over `[0-9]` in regex literals. Autofixable.
|
|
147
155
|
*
|
|
148
156
|
* @reference https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-d.html
|
|
149
157
|
*/
|
|
150
158
|
"regexp/prefer-d"?: TtscLintRuleSetting;
|
|
151
159
|
|
|
152
160
|
/**
|
|
153
|
-
* Prefer `+` over `{1,}` in regex literals.
|
|
161
|
+
* Prefer `+` over `{1,}` in regex literals. Autofixable.
|
|
154
162
|
*
|
|
155
163
|
* @reference https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-plus-quantifier.html
|
|
156
164
|
*/
|
|
157
165
|
"regexp/prefer-plus-quantifier"?: TtscLintRuleSetting;
|
|
158
166
|
|
|
159
167
|
/**
|
|
160
|
-
* Prefer `?` over `{0,1}` in regex literals.
|
|
168
|
+
* Prefer `?` over `{0,1}` in regex literals. Autofixable; a lazy `{0,1}`
|
|
169
|
+
* correctly becomes `??`.
|
|
161
170
|
*
|
|
162
171
|
* @reference https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-question-quantifier.html
|
|
163
172
|
*/
|
|
164
173
|
"regexp/prefer-question-quantifier"?: TtscLintRuleSetting;
|
|
165
174
|
|
|
166
175
|
/**
|
|
167
|
-
* Prefer `*` over `{0,}` in regex literals.
|
|
176
|
+
* Prefer `*` over `{0,}` in regex literals. Autofixable.
|
|
168
177
|
*
|
|
169
178
|
* @reference https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-star-quantifier.html
|
|
170
179
|
*/
|
|
171
180
|
"regexp/prefer-star-quantifier"?: TtscLintRuleSetting;
|
|
172
181
|
|
|
173
182
|
/**
|
|
174
|
-
* Prefer `\w` over `[A-Za-z0-9_]` in regex literals.
|
|
183
|
+
* Prefer `\w` over `[A-Za-z0-9_]` in regex literals. Autofixable for both
|
|
184
|
+
* accepted spellings.
|
|
175
185
|
*
|
|
176
186
|
* @reference https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-w.html
|
|
177
187
|
*/
|
|
@@ -181,6 +191,9 @@ export interface ITtscLintRegexpRules {
|
|
|
181
191
|
* Require regex literals to use the `u` or `v` flag, so Unicode-property
|
|
182
192
|
* escapes and surrogate-pair handling stay predictable.
|
|
183
193
|
*
|
|
194
|
+
* Offers `u` and `v` as editor suggestions rather than an automatic fix: both
|
|
195
|
+
* satisfy the rule, and both change what the pattern matches.
|
|
196
|
+
*
|
|
184
197
|
* @reference https://ota-meshi.github.io/eslint-plugin-regexp/rules/require-unicode-regexp.html
|
|
185
198
|
*/
|
|
186
199
|
"regexp/require-unicode-regexp"?: TtscLintRuleSetting;
|
|
@@ -191,7 +204,8 @@ export interface ITtscLintRegexpRules {
|
|
|
191
204
|
* stricter escape rules on top of `u`.
|
|
192
205
|
*
|
|
193
206
|
* Choose this over `require-unicode-regexp` only on engines that ship
|
|
194
|
-
* ES2024-era regex.
|
|
207
|
+
* ES2024-era regex. Offers one editor suggestion, which replaces an existing
|
|
208
|
+
* `u` because the two flags are mutually exclusive.
|
|
195
209
|
*
|
|
196
210
|
* @reference https://ota-meshi.github.io/eslint-plugin-regexp/rules/require-unicode-sets-regexp.html
|
|
197
211
|
*/
|
|
@@ -201,7 +215,8 @@ export interface ITtscLintRegexpRules {
|
|
|
201
215
|
* Require regex flags to appear in canonical alphabetical order (`dgimsuvy`).
|
|
202
216
|
*
|
|
203
217
|
* Stable ordering keeps diffs small and lets readers compare flag sets at a
|
|
204
|
-
* glance.
|
|
218
|
+
* glance. Autofixable: a permutation of a flag run cannot change what the
|
|
219
|
+
* literal matches.
|
|
205
220
|
*
|
|
206
221
|
* @reference https://ota-meshi.github.io/eslint-plugin-regexp/rules/sort-flags.html
|
|
207
222
|
*/
|
|
@@ -67,6 +67,13 @@ export interface ITtscLintSecurityRules {
|
|
|
67
67
|
* Detect `new Buffer(input)` constructions with non-literal input —
|
|
68
68
|
* historical source of allocation-disclosure bugs.
|
|
69
69
|
*
|
|
70
|
+
* The three successors — `Buffer.from`, `Buffer.alloc`, `Buffer.allocUnsafe`
|
|
71
|
+
* — are offered as editor suggestions and none is applied automatically:
|
|
72
|
+
* which one is correct depends on the argument, which this rule fires
|
|
73
|
+
* precisely because it cannot read. The rule stays untagged because its
|
|
74
|
+
* upstream-compatible name match also admits a user-defined constructor named
|
|
75
|
+
* `Buffer`.
|
|
76
|
+
*
|
|
70
77
|
* @reference https://github.com/eslint-community/eslint-plugin-security/blob/main/docs/rules/detect-new-buffer.md
|
|
71
78
|
*/
|
|
72
79
|
"security/detect-new-buffer"?: TtscLintRuleSetting;
|
|
@@ -138,6 +145,17 @@ export interface ITtscLintSecurityRules {
|
|
|
138
145
|
* Tokens, session ids, and key material must use `crypto.randomBytes` (or Web
|
|
139
146
|
* Crypto's `getRandomValues`) instead.
|
|
140
147
|
*
|
|
148
|
+
* Type-aware via the Checker, which resolves the object at the use site so an
|
|
149
|
+
* automatic rewrite is never applied to a shadowed binding. Enabling this
|
|
150
|
+
* rule therefore puts the whole run on the checker path. The diagnostic
|
|
151
|
+
* itself stays name-based, so it still reports without one.
|
|
152
|
+
*
|
|
153
|
+
* The member name alone is rewritten to `randomBytes`. The edit is automatic
|
|
154
|
+
* when the object is proven to be an import or require of Node `crypto`, and
|
|
155
|
+
* an editor suggestion otherwise, because a local application object can also
|
|
156
|
+
* be named `crypto`. That name-based diagnostic surface also keeps the rule
|
|
157
|
+
* untagged.
|
|
158
|
+
*
|
|
141
159
|
* @reference https://github.com/eslint-community/eslint-plugin-security/blob/main/docs/rules/detect-pseudoRandomBytes.md
|
|
142
160
|
*/
|
|
143
161
|
"security/detect-pseudoRandomBytes"?: TtscLintRuleSetting;
|
|
@@ -31,8 +31,14 @@ export interface ITtscLintSolidRules {
|
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* Route each Solid export to the correct entry point (`solid-js`,
|
|
34
|
-
* `solid-js/web`, or `solid-js/store`) and
|
|
35
|
-
*
|
|
34
|
+
* `solid-js/web`, or `solid-js/store`) and relocate a misrouted one to where
|
|
35
|
+
* it belongs, joining an existing import from that entry when the file
|
|
36
|
+
* already has one. The diagnostic names the symbol and its entry point.
|
|
37
|
+
*
|
|
38
|
+
* Autofixable. A specifier that stands alone has its declaration's module
|
|
39
|
+
* specifier rewritten; one with siblings, or one beside a default binding, is
|
|
40
|
+
* cut out and relocated. A type-only declaration relocates into a type-only
|
|
41
|
+
* one, never into a value import.
|
|
36
42
|
*
|
|
37
43
|
* @reference https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/imports.md
|
|
38
44
|
*/
|
|
@@ -103,6 +109,15 @@ export interface ITtscLintSolidRules {
|
|
|
103
109
|
* Reject React-style dependency arrays in Solid tracked scopes
|
|
104
110
|
* (`createEffect(() => ..., [deps])`).
|
|
105
111
|
*
|
|
112
|
+
* Type-aware via the Checker. The tag below claims the array is dead, so the
|
|
113
|
+
* callee has to be the Solid primitive itself rather than a same-named local
|
|
114
|
+
* or a shadowing parameter, and only symbol resolution can say that. Enabling
|
|
115
|
+
* this rule therefore puts the whole run on the checker path.
|
|
116
|
+
*
|
|
117
|
+
* Tagged `Unnecessary`: the reported range is the array literal alone, and
|
|
118
|
+
* Solid tracks dependencies automatically, so deleting it is the whole
|
|
119
|
+
* resolution and an editor greys it out.
|
|
120
|
+
*
|
|
106
121
|
* @reference https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/no-react-deps.md
|
|
107
122
|
*/
|
|
108
123
|
"solid/no-react-deps"?: TtscLintRuleSetting;
|
|
@@ -111,6 +126,11 @@ export interface ITtscLintSolidRules {
|
|
|
111
126
|
* Reject React-specific JSX props such as `className` and `htmlFor` — Solid
|
|
112
127
|
* uses `class` and `for`.
|
|
113
128
|
*
|
|
129
|
+
* The two renames are autofixed by rewriting the name token alone, so the
|
|
130
|
+
* value survives untouched. The `key` arm stays diagnostic-only: a Solid DOM
|
|
131
|
+
* element does not consume `key`, so its resolution is a deletion that has to
|
|
132
|
+
* take the surrounding whitespace with it.
|
|
133
|
+
*
|
|
114
134
|
* @reference https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/no-react-specific-props.md
|
|
115
135
|
*/
|
|
116
136
|
"solid/no-react-specific-props"?: TtscLintRuleSetting;
|