@ttsc/lint 0.19.3 → 0.20.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 +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
|
@@ -57,9 +57,51 @@ func printArrowFunction(ctx *PrintContext, node *shimast.Node) (Doc, bool) {
|
|
|
57
57
|
if arrow == nil || arrow.Body == nil {
|
|
58
58
|
return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
|
|
59
59
|
}
|
|
60
|
+
if arrowBodyBreaksAfterArrow(arrow.Body) {
|
|
61
|
+
return printArrowWithBreakableExpressionBody(ctx, node, arrow.Body)
|
|
62
|
+
}
|
|
60
63
|
return printFunctionLike(ctx, node, arrow.Body)
|
|
61
64
|
}
|
|
62
65
|
|
|
66
|
+
// arrowBodyBreaksAfterArrow reports the expression-body shapes for which
|
|
67
|
+
// Prettier can keep an arrow hugged to a call's opening line and break after
|
|
68
|
+
// `=>`. Object and array bodies already carry their own group and use the
|
|
69
|
+
// ordinary arrow printer; these shapes need a group owned by the arrow itself.
|
|
70
|
+
func arrowBodyBreaksAfterArrow(body *shimast.Node) bool {
|
|
71
|
+
if body == nil {
|
|
72
|
+
return false
|
|
73
|
+
}
|
|
74
|
+
switch body.Kind {
|
|
75
|
+
case shimast.KindCallExpression,
|
|
76
|
+
shimast.KindConditionalExpression,
|
|
77
|
+
shimast.KindJsxElement,
|
|
78
|
+
shimast.KindJsxSelfClosingElement,
|
|
79
|
+
shimast.KindJsxFragment:
|
|
80
|
+
return true
|
|
81
|
+
}
|
|
82
|
+
return false
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// printArrowWithBreakableExpressionBody renders a concise arrow as one group
|
|
86
|
+
// with a Line immediately after `=>`. Flat mode emits the ordinary space;
|
|
87
|
+
// broken mode places the expression one indentation level below the arrow.
|
|
88
|
+
func printArrowWithBreakableExpressionBody(ctx *PrintContext, node, body *shimast.Node) (Doc, bool) {
|
|
89
|
+
nodeStart := shimscanner.SkipTrivia(ctx.Source, node.Pos())
|
|
90
|
+
bodyStart := shimscanner.SkipTrivia(ctx.Source, body.Pos())
|
|
91
|
+
if nodeStart < 0 || bodyStart < nodeStart || bodyStart > len(ctx.Source) {
|
|
92
|
+
return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
|
|
93
|
+
}
|
|
94
|
+
prefix := strings.TrimRight(ctx.Source[nodeStart:bodyStart], " \t\r\n")
|
|
95
|
+
if strings.Contains(prefix, "\n") || !strings.HasSuffix(prefix, "=>") {
|
|
96
|
+
return printFunctionLike(ctx, node, body)
|
|
97
|
+
}
|
|
98
|
+
bodyDoc, covered := PrintNode(ctx, body)
|
|
99
|
+
return Group(
|
|
100
|
+
Text(prefix),
|
|
101
|
+
Indent(ctx.indentUnit(), Line(), bodyDoc),
|
|
102
|
+
), covered
|
|
103
|
+
}
|
|
104
|
+
|
|
63
105
|
// printFunctionExpression renders a `function` expression. Like the
|
|
64
106
|
// arrow printer, the signature is verbatim and only the body reflows.
|
|
65
107
|
// Function expressions always carry a block body, so the body always
|
|
@@ -100,6 +142,66 @@ func printFunctionLike(ctx *PrintContext, node, body *shimast.Node) (Doc, bool)
|
|
|
100
142
|
return Concat(prefix, bodyDoc), prefixCovered && bodyCovered
|
|
101
143
|
}
|
|
102
144
|
|
|
145
|
+
// printObjectMember renders one member of an object literal by slicing its
|
|
146
|
+
// head verbatim and dispatching what follows.
|
|
147
|
+
//
|
|
148
|
+
// A member is the same shape as a function expression — a signature the printer
|
|
149
|
+
// keeps as written, then a body it lays out — so it goes through
|
|
150
|
+
// printFunctionLike. A shorthand method's body starts at its `{`, an accessor's
|
|
151
|
+
// likewise, and a property assignment's "body" is its initializer, which makes
|
|
152
|
+
// `m: () => { … }` reflow through the arrow printer instead of freezing.
|
|
153
|
+
//
|
|
154
|
+
// Without this every member printed verbatim, so an object literal held a
|
|
155
|
+
// single-line slice no enclosing reflow could break into: `{ m() { return 1; }
|
|
156
|
+
// }` had no hardline to honour however the layout was forced, which is why
|
|
157
|
+
// denying the print-width fast path for it changed nothing.
|
|
158
|
+
//
|
|
159
|
+
// The second return value is the `covered` flag: see PrintNode.
|
|
160
|
+
func printObjectMember(ctx *PrintContext, node *shimast.Node) (Doc, bool) {
|
|
161
|
+
if node == nil {
|
|
162
|
+
return Doc{}, true
|
|
163
|
+
}
|
|
164
|
+
body := objectMemberBody(node)
|
|
165
|
+
if body == nil {
|
|
166
|
+
// A shorthand property (`{ a }`), a spread (`{ ...rest }`), or a member
|
|
167
|
+
// whose body the parser did not produce. There is nothing to lay out, so
|
|
168
|
+
// the source bytes round-trip.
|
|
169
|
+
return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
|
|
170
|
+
}
|
|
171
|
+
// A comment between the head and the body would sit inside the verbatim
|
|
172
|
+
// prefix and survive, but one after the body's close brace would not, so the
|
|
173
|
+
// same guard the sibling printers use applies here.
|
|
174
|
+
if listHasInterItemComments(ctx, node) {
|
|
175
|
+
return verbatim(ctx, node), false
|
|
176
|
+
}
|
|
177
|
+
return printFunctionLike(ctx, node, body)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// objectMemberBody returns the part of an object-literal member the printer
|
|
181
|
+
// lays out — a callable member's block, or a property assignment's initializer
|
|
182
|
+
// — or nil when the member has neither.
|
|
183
|
+
func objectMemberBody(node *shimast.Node) *shimast.Node {
|
|
184
|
+
switch node.Kind {
|
|
185
|
+
case shimast.KindPropertyAssignment:
|
|
186
|
+
if p := node.AsPropertyAssignment(); p != nil {
|
|
187
|
+
return p.Initializer
|
|
188
|
+
}
|
|
189
|
+
case shimast.KindMethodDeclaration:
|
|
190
|
+
if m := node.AsMethodDeclaration(); m != nil {
|
|
191
|
+
return m.Body
|
|
192
|
+
}
|
|
193
|
+
case shimast.KindGetAccessor:
|
|
194
|
+
if g := node.AsGetAccessorDeclaration(); g != nil {
|
|
195
|
+
return g.Body
|
|
196
|
+
}
|
|
197
|
+
case shimast.KindSetAccessor:
|
|
198
|
+
if s := node.AsSetAccessorDeclaration(); s != nil {
|
|
199
|
+
return s.Body
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return nil
|
|
203
|
+
}
|
|
204
|
+
|
|
103
205
|
// printParenthesizedExpression renders `( expr )`. The parentheses are
|
|
104
206
|
// fixed punctuation; the inner expression is dispatched so a call or
|
|
105
207
|
// object literal wrapped in parens still reflows.
|
|
@@ -164,8 +266,27 @@ func printBlock(ctx *PrintContext, node *shimast.Node) (Doc, bool) {
|
|
|
164
266
|
hasComment := blockHasNonStatementComment(ctx, node, block.Statements.Nodes)
|
|
165
267
|
if len(block.Statements.Nodes) == 0 {
|
|
166
268
|
if hasComment {
|
|
167
|
-
//
|
|
168
|
-
|
|
269
|
+
// A one-line comment-only block is non-empty to Prettier: keep the
|
|
270
|
+
// comment and give it the same hardline layout as a statement. A
|
|
271
|
+
// multi-line comment body keeps its original columns and therefore
|
|
272
|
+
// remains uncovered.
|
|
273
|
+
start := shimscanner.SkipTrivia(ctx.Source, node.Pos())
|
|
274
|
+
end := node.End()
|
|
275
|
+
if start < 0 || end <= start+1 || end > len(ctx.Source) ||
|
|
276
|
+
ctx.Source[start] != '{' || ctx.Source[end-1] != '}' {
|
|
277
|
+
return verbatim(ctx, node), !nodeSpansMultipleLines(ctx, node)
|
|
278
|
+
}
|
|
279
|
+
rawInner := ctx.Source[start+1 : end-1]
|
|
280
|
+
inner := strings.TrimSpace(rawInner)
|
|
281
|
+
if inner == "" || strings.ContainsAny(rawInner, "\r\n") {
|
|
282
|
+
return verbatim(ctx, node), false
|
|
283
|
+
}
|
|
284
|
+
return Concat(
|
|
285
|
+
Text("{"),
|
|
286
|
+
Indent(ctx.indentUnit(), Hardline(), Text(inner)),
|
|
287
|
+
Hardline(),
|
|
288
|
+
Text("}"),
|
|
289
|
+
), true
|
|
169
290
|
}
|
|
170
291
|
return Text("{}"), true
|
|
171
292
|
}
|
|
@@ -254,30 +375,29 @@ func blankLineBetweenStatements(src string, prevEnd, nextPos int) bool {
|
|
|
254
375
|
return false
|
|
255
376
|
}
|
|
256
377
|
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
// the formatPrintWidth rule abstain.
|
|
378
|
+
// nodeHasNonItemComment reports whether the node's byte range holds a `//` or
|
|
379
|
+
// `/*` outside every supplied item's token range. Block and switch printers
|
|
380
|
+
// join their items with minted Hardlines that have no slot for inter-item
|
|
381
|
+
// trivia, so a stray comment would be dropped by a reflow. Detecting it lets
|
|
382
|
+
// the printer report the node uncovered and the formatPrintWidth rule abstain.
|
|
263
383
|
//
|
|
264
384
|
// The scan mirrors rules_format_print_width.go::hasNonChildComments:
|
|
265
385
|
// comment-shaped bytes inside a complete statement token range (string
|
|
266
386
|
// literals, nested comments) are masked, so only genuine
|
|
267
387
|
// inter-statement comments surface.
|
|
268
|
-
func
|
|
388
|
+
func nodeHasNonItemComment(ctx *PrintContext, node *shimast.Node, items []*shimast.Node) bool {
|
|
269
389
|
start := shimscanner.SkipTrivia(ctx.Source, node.Pos())
|
|
270
390
|
end := node.End()
|
|
271
391
|
if start < 0 || end < start || end > len(ctx.Source) {
|
|
272
392
|
return false
|
|
273
393
|
}
|
|
274
394
|
type span struct{ pos, end int }
|
|
275
|
-
ranges := make([]span, 0, len(
|
|
276
|
-
for _,
|
|
277
|
-
if
|
|
395
|
+
ranges := make([]span, 0, len(items))
|
|
396
|
+
for _, item := range items {
|
|
397
|
+
if item == nil {
|
|
278
398
|
continue
|
|
279
399
|
}
|
|
280
|
-
ranges = append(ranges, span{shimscanner.SkipTrivia(ctx.Source,
|
|
400
|
+
ranges = append(ranges, span{shimscanner.SkipTrivia(ctx.Source, item.Pos()), item.End()})
|
|
281
401
|
}
|
|
282
402
|
inStatement := func(i int) bool {
|
|
283
403
|
for _, r := range ranges {
|
|
@@ -299,6 +419,13 @@ func blockHasNonStatementComment(ctx *PrintContext, node *shimast.Node, stmts []
|
|
|
299
419
|
return false
|
|
300
420
|
}
|
|
301
421
|
|
|
422
|
+
// blockHasNonStatementComment is the block-specific name retained for callers
|
|
423
|
+
// and focused tests; switch printers use the generalized item-range helper
|
|
424
|
+
// directly for clauses and statements.
|
|
425
|
+
func blockHasNonStatementComment(ctx *PrintContext, node *shimast.Node, stmts []*shimast.Node) bool {
|
|
426
|
+
return nodeHasNonItemComment(ctx, node, stmts)
|
|
427
|
+
}
|
|
428
|
+
|
|
302
429
|
// printExpressionStatement renders an `expr;` statement. The expression
|
|
303
430
|
// is dispatched so a callback-body statement that is itself a call or
|
|
304
431
|
// object literal reflows; the trailing `;` is preserved when the source
|
|
@@ -52,14 +52,17 @@ type listShape struct {
|
|
|
52
52
|
Items []Doc
|
|
53
53
|
Space bool // emit a space after OPEN / before CLOSE in flat mode
|
|
54
54
|
AddComma bool // emit a trailing comma in broken mode
|
|
55
|
-
// HugLast keeps the final item attached to
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
// against OPEN…CLOSE with no leading/trailing soft break, so a
|
|
60
|
-
// multi-line callback body does not force every preceding argument
|
|
61
|
-
// onto its own line.
|
|
55
|
+
// HugLast keeps the final item attached to OPEN instead of exploding the
|
|
56
|
+
// whole list. Ordinarily it also stays against CLOSE, so a multi-line
|
|
57
|
+
// callback body does not force preceding arguments onto separate lines.
|
|
58
|
+
// HugLastBreakClose supplies the expression-bodied-arrow exception.
|
|
62
59
|
HugLast bool
|
|
60
|
+
// HugLastBreakClose keeps an expression-bodied trailing arrow attached to
|
|
61
|
+
// OPEN through its `=>`, but finishes a broken argument list with the
|
|
62
|
+
// optional trailing comma and CLOSE on a fresh line. Unlike a block-bodied
|
|
63
|
+
// callback, the arrow's expression does not own the call's closing line:
|
|
64
|
+
// `run(() =>\n nested(),\n)`. Requires HugLast.
|
|
65
|
+
HugLastBreakClose bool
|
|
63
66
|
// HugFirst keeps the FIRST item attached to the open paren and flows
|
|
64
67
|
// the remaining simple items after it, the mirror of HugLast. The
|
|
65
68
|
// call-argument printer sets it for the two-argument
|
|
@@ -274,8 +277,9 @@ func printListPlain(ctx *PrintContext, shape listShape) Doc {
|
|
|
274
277
|
|
|
275
278
|
// printListHuggingLast renders the "last-argument hugging" shape that
|
|
276
279
|
// Prettier uses for `foo(a, b, () => { … })`: the leading items flow
|
|
277
|
-
// comma-separated and the final item stays attached to the
|
|
278
|
-
//
|
|
280
|
+
// comma-separated and the final item stays attached to the opening line
|
|
281
|
+
// instead of being pushed onto its own indented line. Most expandable items
|
|
282
|
+
// hug CLOSE too; an expression-bodied arrow uses HugLastBreakClose.
|
|
279
283
|
//
|
|
280
284
|
// hugged: OPEN a, b, OPEN-of-last … CLOSE-of-last CLOSE
|
|
281
285
|
//
|
|
@@ -308,7 +312,15 @@ func printListHuggingLast(ctx *PrintContext, shape listShape) Doc {
|
|
|
308
312
|
for _, item := range lead {
|
|
309
313
|
parts = append(parts, item, Text(", "))
|
|
310
314
|
}
|
|
311
|
-
parts = append(parts, last
|
|
315
|
+
parts = append(parts, last)
|
|
316
|
+
if shape.HugLastBreakClose {
|
|
317
|
+
if shape.AddComma {
|
|
318
|
+
parts = append(parts, Text(","))
|
|
319
|
+
}
|
|
320
|
+
parts = append(parts, Hardline(), Text(shape.CloseTok))
|
|
321
|
+
} else {
|
|
322
|
+
parts = append(parts, Text(shape.CloseTok))
|
|
323
|
+
}
|
|
312
324
|
return Concat(parts...)
|
|
313
325
|
}
|
|
314
326
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
package linthost
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
+
"encoding/json"
|
|
4
5
|
"fmt"
|
|
5
6
|
"sort"
|
|
6
7
|
"sync"
|
|
@@ -18,6 +19,11 @@ type projectCycleResult struct {
|
|
|
18
19
|
status publicrule.ProjectRuleStatus
|
|
19
20
|
severity Severity
|
|
20
21
|
reporter *projectReporter
|
|
22
|
+
// identity and options are the configuration this rule ran under, retained so
|
|
23
|
+
// a later consumer of its state — a hint corpus, say — can be handed the same
|
|
24
|
+
// context Check saw without the rule stashing a copy inside its own state.
|
|
25
|
+
identity publicrule.ProjectIdentity
|
|
26
|
+
options json.RawMessage
|
|
21
27
|
}
|
|
22
28
|
|
|
23
29
|
func (r *projectCycleResults) ProjectResult(name string) publicrule.ProjectRuleResult {
|
|
@@ -178,11 +184,20 @@ func (e *Engine) evaluateProject(
|
|
|
178
184
|
sources = e.projectSources(files)
|
|
179
185
|
sourcesResolved = true
|
|
180
186
|
}
|
|
187
|
+
// A rule that declared it needs no checker is handed none, matching the
|
|
188
|
+
// TypeAwareRule contract that the host is free to leave it nil. Passing one
|
|
189
|
+
// anyway would let such a rule read the checker and keep working while its
|
|
190
|
+
// own declaration says it does not — until the day the engine acts on that
|
|
191
|
+
// declaration and the rule faults far from the marker that caused it.
|
|
192
|
+
ruleChecker := checker
|
|
193
|
+
if adapter.declinesTypeChecker {
|
|
194
|
+
ruleChecker = nil
|
|
195
|
+
}
|
|
181
196
|
reporter := &projectReporter{active: true}
|
|
182
197
|
context := publicrule.NewProjectContext(
|
|
183
198
|
identity,
|
|
184
199
|
sources,
|
|
185
|
-
|
|
200
|
+
ruleChecker,
|
|
186
201
|
publicrule.Severity(setting.Severity),
|
|
187
202
|
setting.Options,
|
|
188
203
|
reporter,
|
|
@@ -191,6 +206,8 @@ func (e *Engine) evaluateProject(
|
|
|
191
206
|
results.byName[name] = projectCycleResult{
|
|
192
207
|
severity: setting.Severity,
|
|
193
208
|
reporter: reporter,
|
|
209
|
+
identity: identity,
|
|
210
|
+
options: setting.Options,
|
|
194
211
|
}
|
|
195
212
|
}
|
|
196
213
|
return cycle
|
|
@@ -14,10 +14,48 @@ type projectRuleAdapter struct {
|
|
|
14
14
|
inner publicrule.ProjectRule
|
|
15
15
|
name string
|
|
16
16
|
acceptsOptions bool
|
|
17
|
+
// declinesTypeChecker records an explicit opt-out, so the zero value keeps
|
|
18
|
+
// the conservative default: a rule that never spoke still receives a checker.
|
|
19
|
+
//
|
|
20
|
+
// The negative spelling is deliberate. Adapters are constructed directly in
|
|
21
|
+
// several test helpers rather than through inspectProjectContributor, and a
|
|
22
|
+
// positive needsTypeChecker field would make those zero values silently deny
|
|
23
|
+
// a checker to a rule that reads one — the unsafe direction, reached by
|
|
24
|
+
// forgetting rather than by deciding.
|
|
25
|
+
declinesTypeChecker bool
|
|
17
26
|
}
|
|
18
27
|
|
|
19
28
|
var registeredProjectRules = map[string]projectRuleAdapter{}
|
|
20
29
|
|
|
30
|
+
// registerBuiltInProjectCompanion attaches a project lifecycle to an existing
|
|
31
|
+
// built-in file rule. Both halves deliberately share one public rule name and
|
|
32
|
+
// therefore one config setting: the file rule keeps reporting source ranges,
|
|
33
|
+
// while the project companion can publish finished state for consumers such as
|
|
34
|
+
// editor hints.
|
|
35
|
+
//
|
|
36
|
+
// Contributor rules remain single-lifecycle registrations. Their collision
|
|
37
|
+
// checks in registerProjectContributors still reject a file/project name pair,
|
|
38
|
+
// because only built-ins can be audited together as one rule implementation.
|
|
39
|
+
func registerBuiltInProjectCompanion(project publicrule.ProjectRule) {
|
|
40
|
+
adapter, err := inspectProjectContributor(project)
|
|
41
|
+
if err != nil {
|
|
42
|
+
panic(fmt.Sprintf("@ttsc/lint: invalid built-in project companion: %v", err))
|
|
43
|
+
}
|
|
44
|
+
if adapter.name == "" {
|
|
45
|
+
panic("@ttsc/lint: built-in project companion has an empty name")
|
|
46
|
+
}
|
|
47
|
+
if LookupRule(adapter.name) == nil {
|
|
48
|
+
panic(fmt.Sprintf("@ttsc/lint: built-in project companion %q has no file rule", adapter.name))
|
|
49
|
+
}
|
|
50
|
+
if _, builtIn := builtInRuleCodes[adapter.name]; !builtIn {
|
|
51
|
+
panic(fmt.Sprintf("@ttsc/lint: project companion %q is not a built-in rule", adapter.name))
|
|
52
|
+
}
|
|
53
|
+
if _, exists := registeredProjectRules[adapter.name]; exists {
|
|
54
|
+
panic(fmt.Sprintf("@ttsc/lint: project rule %q registered twice", adapter.name))
|
|
55
|
+
}
|
|
56
|
+
registeredProjectRules[adapter.name] = adapter
|
|
57
|
+
}
|
|
58
|
+
|
|
21
59
|
func registerProjectContributors() {
|
|
22
60
|
projects := publicrule.RegisteredProjects()
|
|
23
61
|
adapters := make([]projectRuleAdapter, 0, len(projects))
|
|
@@ -65,9 +103,18 @@ func inspectProjectContributor(project publicrule.ProjectRule) (adapter projectR
|
|
|
65
103
|
if optionsRule, ok := project.(publicrule.OptionsRule); ok {
|
|
66
104
|
adapter.acceptsOptions = optionsRule.AcceptsTtscLintOptions()
|
|
67
105
|
}
|
|
106
|
+
if typeAware, ok := project.(publicrule.TypeAwareRule); ok {
|
|
107
|
+
adapter.declinesTypeChecker = !typeAware.NeedsTypeChecker()
|
|
108
|
+
}
|
|
68
109
|
return adapter, nil
|
|
69
110
|
}
|
|
70
111
|
|
|
112
|
+
// projectRuleNeedsTypeChecker reports whether a registered project rule wants a
|
|
113
|
+
// live checker. An unknown name and a rule that never spoke both answer true.
|
|
114
|
+
func projectRuleNeedsTypeChecker(name string) bool {
|
|
115
|
+
return !registeredProjectRules[name].declinesTypeChecker
|
|
116
|
+
}
|
|
117
|
+
|
|
71
118
|
func allProjectRuleNames() []string {
|
|
72
119
|
names := make([]string, 0, len(registeredProjectRules))
|
|
73
120
|
for name := range registeredProjectRules {
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
package linthost
|
|
2
|
+
|
|
3
|
+
import "strings"
|
|
4
|
+
|
|
5
|
+
// Rule documentation lives in one of two places, and which one is decided by
|
|
6
|
+
// the rule's family alone — no per-rule table is maintained here.
|
|
7
|
+
//
|
|
8
|
+
// Families ported from a plugin that publishes its own rule reference keep
|
|
9
|
+
// pointing at that upstream reference, because those pages are the behavioral
|
|
10
|
+
// specification `@ttsc/lint` ports against and the one the rule sources already
|
|
11
|
+
// cite in their header comments. Every other family is documented only by the
|
|
12
|
+
// ttsc website's rule catalog, so its rules link there.
|
|
13
|
+
const (
|
|
14
|
+
eslintRuleDocsBaseURL = "https://eslint.org/docs/latest/rules/"
|
|
15
|
+
unicornRuleDocsBaseURL = "https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/"
|
|
16
|
+
typescriptRuleDocsBaseURL = "https://typescript-eslint.io/rules/"
|
|
17
|
+
websiteRuleDocsBaseURL = "https://ttsc.dev/docs/lint/rules/"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
// websiteDocumentedRuleFamilies lists the families whose whole rule set is
|
|
21
|
+
// catalogued on the ttsc website, as one level-3 heading per rule (the rule
|
|
22
|
+
// name in a code span) in website/src/content/docs/lint/rules/<family>.mdx.
|
|
23
|
+
//
|
|
24
|
+
// `format` is deliberately absent. Its 17 rules have no page of their own: the
|
|
25
|
+
// `lint/format` guide documents the configuration keys (`semi`, `printWidth`,
|
|
26
|
+
// …), which do not correspond one-to-one with the rule names (`format/quotes`,
|
|
27
|
+
// `format/clause-join`, `format/whitespace`, …). Emitting an anchor there would
|
|
28
|
+
// produce a link to a heading that does not exist, so format findings carry no
|
|
29
|
+
// codeDescription at all.
|
|
30
|
+
//
|
|
31
|
+
// A family missing from this set simply gets no documentation link, which keeps
|
|
32
|
+
// a newly added family silent rather than pointing at a page nobody wrote yet.
|
|
33
|
+
var websiteDocumentedRuleFamilies = map[string]struct{}{
|
|
34
|
+
"boundaries": {},
|
|
35
|
+
"cypress": {},
|
|
36
|
+
"functional": {},
|
|
37
|
+
"jest": {},
|
|
38
|
+
"jsdoc": {},
|
|
39
|
+
"jsx-a11y": {},
|
|
40
|
+
"nextjs": {},
|
|
41
|
+
"playwright": {},
|
|
42
|
+
"promise": {},
|
|
43
|
+
"react": {},
|
|
44
|
+
"react-perf": {},
|
|
45
|
+
"regexp": {},
|
|
46
|
+
"security": {},
|
|
47
|
+
"solid": {},
|
|
48
|
+
"storybook": {},
|
|
49
|
+
"tanstack-query": {},
|
|
50
|
+
"testing-library": {},
|
|
51
|
+
"vitest": {},
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ruleDocumentationURL returns the documentation page for a rule name, or an
|
|
55
|
+
// empty string when no vetted page exists for it.
|
|
56
|
+
//
|
|
57
|
+
// Only active built-in rules resolve. The rule-code ledger is append-only, so a
|
|
58
|
+
// removed built-in name remains reserved and is not enough to establish runtime
|
|
59
|
+
// provenance by itself. The registered rule must also be a native rule rather
|
|
60
|
+
// than a public contributor adapter. That keeps a contributor which reuses a
|
|
61
|
+
// retired ledger name from inheriting a ttsc or upstream URL.
|
|
62
|
+
func ruleDocumentationURL(name string) string {
|
|
63
|
+
candidate := LookupRule(name)
|
|
64
|
+
if candidate == nil {
|
|
65
|
+
return ""
|
|
66
|
+
}
|
|
67
|
+
switch candidate.(type) {
|
|
68
|
+
case contributorAdapter, formatContributorAdapter:
|
|
69
|
+
return ""
|
|
70
|
+
}
|
|
71
|
+
if _, builtIn := builtInRuleCodes[name]; !builtIn {
|
|
72
|
+
return ""
|
|
73
|
+
}
|
|
74
|
+
family, bare, prefixed := strings.Cut(name, "/")
|
|
75
|
+
if !prefixed {
|
|
76
|
+
// An unprefixed built-in name is a core ESLint rule id verbatim.
|
|
77
|
+
return eslintRuleDocsBaseURL + name
|
|
78
|
+
}
|
|
79
|
+
switch family {
|
|
80
|
+
case "unicorn":
|
|
81
|
+
return unicornRuleDocsBaseURL + bare + ".md"
|
|
82
|
+
case "typescript":
|
|
83
|
+
return typescriptRuleDocsBaseURL + bare
|
|
84
|
+
}
|
|
85
|
+
if _, documented := websiteDocumentedRuleFamilies[family]; !documented {
|
|
86
|
+
return ""
|
|
87
|
+
}
|
|
88
|
+
return websiteRuleDocsBaseURL + family + "#" + websiteRuleAnchor(name)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// websiteRuleAnchor reproduces the heading id Nextra assigns to the level-3
|
|
92
|
+
// heading that documents a rule.
|
|
93
|
+
//
|
|
94
|
+
// Nextra's remark-headings plugin slugs the heading's flattened text with
|
|
95
|
+
// github-slugger, which lowercases and drops characters it does not consider
|
|
96
|
+
// word characters — the `/` separator among them. `jsx-a11y/alt-text` therefore
|
|
97
|
+
// renders as `#jsx-a11yalt-text`, not `#jsx-a11y-alt-text`, and the one rule
|
|
98
|
+
// name carrying an uppercase letter (`security/detect-pseudoRandomBytes`)
|
|
99
|
+
// renders fully lowercased. Both fall out of this transform; neither is special
|
|
100
|
+
// cased.
|
|
101
|
+
func websiteRuleAnchor(name string) string {
|
|
102
|
+
return strings.ToLower(strings.ReplaceAll(name, "/", ""))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// lspCodeDescriptionForRule wraps a rule's documentation URL for the wire, or
|
|
106
|
+
// returns nil when the rule has none. A nil result keeps `codeDescription`
|
|
107
|
+
// absent from the marshalled diagnostic rather than emitting an empty object.
|
|
108
|
+
func lspCodeDescriptionForRule(name string) *lspCodeDescription {
|
|
109
|
+
href := ruleDocumentationURL(name)
|
|
110
|
+
if href == "" {
|
|
111
|
+
return nil
|
|
112
|
+
}
|
|
113
|
+
return &lspCodeDescription{Href: href}
|
|
114
|
+
}
|
|
@@ -59,7 +59,10 @@ func (boundariesElementTypes) Check(ctx *Context, node *shimast.Node) {
|
|
|
59
59
|
}
|
|
60
60
|
message := fmt.Sprintf("Import from boundary element %q is not allowed in %q.", target.Type, source.Type)
|
|
61
61
|
if rule != nil && rule.Message != "" {
|
|
62
|
+
// A user wrote this; appending to it would be presumptuous.
|
|
62
63
|
message = rule.Message
|
|
64
|
+
} else if rule != nil {
|
|
65
|
+
message += describeBoundaryAllowed(rule.Allow)
|
|
63
66
|
}
|
|
64
67
|
reportBoundaryDependency(ctx, dep, message)
|
|
65
68
|
}
|
|
@@ -80,6 +83,8 @@ func (boundariesExternal) Check(ctx *Context, node *shimast.Node) {
|
|
|
80
83
|
message := fmt.Sprintf("External dependency %q is not allowed.", dep.specifier)
|
|
81
84
|
if opts.Message != "" {
|
|
82
85
|
message = opts.Message
|
|
86
|
+
} else {
|
|
87
|
+
message += describeBoundaryAllowed(opts.Allow)
|
|
83
88
|
}
|
|
84
89
|
reportBoundaryDependency(ctx, dep, message)
|
|
85
90
|
}
|
|
@@ -109,7 +114,11 @@ func (boundariesEntryPoint) Check(ctx *Context, node *shimast.Node) {
|
|
|
109
114
|
if matchBoundaryElementLocalPattern(target.Entry, target) {
|
|
110
115
|
continue
|
|
111
116
|
}
|
|
112
|
-
reportBoundaryDependency(ctx, dep, fmt.Sprintf(
|
|
117
|
+
reportBoundaryDependency(ctx, dep, fmt.Sprintf(
|
|
118
|
+
"Import %q through an allowed boundary entry point.%s",
|
|
119
|
+
target.RelativePath,
|
|
120
|
+
describeBoundaryAllowed(target.Entry),
|
|
121
|
+
))
|
|
113
122
|
}
|
|
114
123
|
}
|
|
115
124
|
|
|
@@ -157,7 +166,11 @@ func (boundariesNoUnknown) Check(ctx *Context, node *shimast.Node) {
|
|
|
157
166
|
if classifyBoundaryFile(targetPath, opts.Elements) != nil {
|
|
158
167
|
continue
|
|
159
168
|
}
|
|
160
|
-
reportBoundaryDependency(ctx, dep, fmt.Sprintf(
|
|
169
|
+
reportBoundaryDependency(ctx, dep, fmt.Sprintf(
|
|
170
|
+
"Imported file %q does not match any configured boundary element.%s",
|
|
171
|
+
boundaryDisplayPath(targetPath),
|
|
172
|
+
describeBoundaryElements(opts.Elements),
|
|
173
|
+
))
|
|
161
174
|
}
|
|
162
175
|
}
|
|
163
176
|
|
|
@@ -460,6 +473,71 @@ func resolveBoundaryImport(sourceFileName, specifier string) (string, bool) {
|
|
|
460
473
|
return "", false
|
|
461
474
|
}
|
|
462
475
|
|
|
476
|
+
// describeBoundaryAllowed renders a set of allowed patterns as a clause a
|
|
477
|
+
// message can append, or "" when there is nothing to name.
|
|
478
|
+
//
|
|
479
|
+
// A boundary rule computes what would be acceptable in order to decide, then
|
|
480
|
+
// reports only that the code is wrong — leaving the reader to open lint.config
|
|
481
|
+
// to learn what is allowed. Naming the set closes that gap for the cost of a
|
|
482
|
+
// join, and matches what unicorn/import-style and prevent-abbreviations already
|
|
483
|
+
// do. An empty set returns "" rather than an empty list, because a deny-only
|
|
484
|
+
// policy has no allowed set to offer and "allowed here: " with nothing after it
|
|
485
|
+
// is worse than silence.
|
|
486
|
+
func describeBoundaryAllowed(patterns boundaryStringList) string {
|
|
487
|
+
if len(patterns) == 0 {
|
|
488
|
+
return ""
|
|
489
|
+
}
|
|
490
|
+
items := make([]string, 0, len(patterns))
|
|
491
|
+
for _, pattern := range patterns {
|
|
492
|
+
if pattern == "" {
|
|
493
|
+
continue
|
|
494
|
+
}
|
|
495
|
+
items = append(items, pattern)
|
|
496
|
+
}
|
|
497
|
+
if len(items) == 0 {
|
|
498
|
+
return ""
|
|
499
|
+
}
|
|
500
|
+
// Truncate rather than print a wall. A message that scrolls off the line is
|
|
501
|
+
// its own failure, and the first several patterns are enough to orient.
|
|
502
|
+
const limit = 6
|
|
503
|
+
suffix := ""
|
|
504
|
+
if len(items) > limit {
|
|
505
|
+
suffix = ", ..."
|
|
506
|
+
items = items[:limit]
|
|
507
|
+
}
|
|
508
|
+
return " Allowed here: " + strings.Join(items, ", ") + suffix + "."
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// describeBoundaryElements names the configured element types, for a
|
|
512
|
+
// "does not match any element" message that would otherwise make the reader
|
|
513
|
+
// go find the list themselves. Phrased as "Configured elements" rather than
|
|
514
|
+
// "Allowed", because no-unknown is about which elements exist, not which imports
|
|
515
|
+
// a policy permits.
|
|
516
|
+
func describeBoundaryElements(elements []boundaryElement) string {
|
|
517
|
+
types := make([]string, 0, len(elements))
|
|
518
|
+
seen := map[string]struct{}{}
|
|
519
|
+
for _, element := range elements {
|
|
520
|
+
if element.Type == "" {
|
|
521
|
+
continue
|
|
522
|
+
}
|
|
523
|
+
if _, ok := seen[element.Type]; ok {
|
|
524
|
+
continue
|
|
525
|
+
}
|
|
526
|
+
seen[element.Type] = struct{}{}
|
|
527
|
+
types = append(types, element.Type)
|
|
528
|
+
}
|
|
529
|
+
if len(types) == 0 {
|
|
530
|
+
return ""
|
|
531
|
+
}
|
|
532
|
+
const limit = 6
|
|
533
|
+
suffix := ""
|
|
534
|
+
if len(types) > limit {
|
|
535
|
+
suffix = ", ..."
|
|
536
|
+
types = types[:limit]
|
|
537
|
+
}
|
|
538
|
+
return " Configured elements: " + strings.Join(types, ", ") + suffix + "."
|
|
539
|
+
}
|
|
540
|
+
|
|
463
541
|
func reportBoundaryDependency(ctx *Context, dep boundaryDependency, message string) {
|
|
464
542
|
if dep.node != nil {
|
|
465
543
|
ctx.Report(dep.node, message)
|
|
@@ -13,9 +13,10 @@ import (
|
|
|
13
13
|
// - prefer false: no inner space, `{x: 1}`, `{a, b}`, `import {foo}`.
|
|
14
14
|
//
|
|
15
15
|
// It applies to object literals, object binding patterns (destructuring),
|
|
16
|
-
// named imports/exports,
|
|
17
|
-
// bracketSpacing governs. Block, class, interface,
|
|
18
|
-
// are NOT affected (their layout is owned by the
|
|
16
|
+
// named imports/exports, type literals, mapped types, and import attributes,
|
|
17
|
+
// the brace kinds Prettier's bracketSpacing governs. Block, class, interface,
|
|
18
|
+
// enum, and module braces are NOT affected (their layout is owned by the
|
|
19
|
+
// indentation rules).
|
|
19
20
|
//
|
|
20
21
|
// The rule touches only a brace pair that opens and closes on the SAME
|
|
21
22
|
// line: a multi-line container's interior is the indentation rules' surface,
|
|
@@ -38,6 +39,8 @@ func (formatBracketSpacing) Visits() []shimast.Kind {
|
|
|
38
39
|
shimast.KindNamedImports,
|
|
39
40
|
shimast.KindNamedExports,
|
|
40
41
|
shimast.KindTypeLiteral,
|
|
42
|
+
shimast.KindMappedType,
|
|
43
|
+
shimast.KindImportAttributes,
|
|
41
44
|
}
|
|
42
45
|
}
|
|
43
46
|
|
|
@@ -58,11 +61,20 @@ func (formatBracketSpacing) Check(ctx *Context, node *shimast.Node) {
|
|
|
58
61
|
if start < 0 || end <= start || end > len(src) {
|
|
59
62
|
return
|
|
60
63
|
}
|
|
61
|
-
|
|
62
|
-
// otherwise-wrapped form is out of scope.
|
|
63
|
-
if src[start] != '{' || src[end-1] != '}' {
|
|
64
|
+
if src[end-1] != '}' {
|
|
64
65
|
return
|
|
65
66
|
}
|
|
67
|
+
// ImportAttributes begins at `with` or `assert`, not at its opening brace.
|
|
68
|
+
// Every other supported kind begins with `{`. Restrict the fallback search
|
|
69
|
+
// to that syntax node so a brace in an earlier expression cannot be chosen.
|
|
70
|
+
if src[start] != '{' {
|
|
71
|
+
for start < end && src[start] != '{' {
|
|
72
|
+
start++
|
|
73
|
+
}
|
|
74
|
+
if start >= end {
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
}
|
|
66
78
|
inner := src[start+1 : end-1]
|
|
67
79
|
if len(inner) == 0 {
|
|
68
80
|
return // empty `{}`, nothing to pad
|