kimaki 0.4.78 → 0.4.80

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.
Files changed (90) hide show
  1. package/dist/anthropic-auth-plugin.js +628 -0
  2. package/dist/channel-management.js +2 -2
  3. package/dist/cli.js +316 -129
  4. package/dist/commands/action-buttons.js +1 -1
  5. package/dist/commands/login.js +634 -277
  6. package/dist/commands/model.js +91 -6
  7. package/dist/commands/paginated-select.js +57 -0
  8. package/dist/commands/resume.js +2 -2
  9. package/dist/commands/tasks.js +205 -0
  10. package/dist/commands/undo-redo.js +80 -18
  11. package/dist/context-awareness-plugin.js +347 -0
  12. package/dist/database.js +103 -7
  13. package/dist/db.js +39 -1
  14. package/dist/discord-bot.js +42 -19
  15. package/dist/discord-urls.js +11 -0
  16. package/dist/discord-ws-proxy.js +350 -0
  17. package/dist/discord-ws-proxy.test.js +500 -0
  18. package/dist/errors.js +1 -1
  19. package/dist/gateway-session.js +163 -0
  20. package/dist/hrana-server.js +114 -4
  21. package/dist/interaction-handler.js +30 -7
  22. package/dist/ipc-tools-plugin.js +186 -0
  23. package/dist/message-preprocessing.js +56 -11
  24. package/dist/onboarding-welcome.js +1 -1
  25. package/dist/opencode-interrupt-plugin.js +133 -75
  26. package/dist/opencode-plugin.js +12 -389
  27. package/dist/opencode.js +59 -5
  28. package/dist/parse-permission-rules.test.js +117 -0
  29. package/dist/queue-drain-after-interactive-ui.e2e.test.js +119 -0
  30. package/dist/session-handler/thread-session-runtime.js +68 -29
  31. package/dist/startup-time.e2e.test.js +295 -0
  32. package/dist/store.js +1 -0
  33. package/dist/system-message.js +3 -1
  34. package/dist/task-runner.js +7 -3
  35. package/dist/task-schedule.js +12 -0
  36. package/dist/thread-message-queue.e2e.test.js +13 -1
  37. package/dist/undo-redo.e2e.test.js +166 -0
  38. package/dist/utils.js +4 -1
  39. package/dist/voice-attachment.js +34 -0
  40. package/dist/voice-handler.js +11 -9
  41. package/dist/voice-message.e2e.test.js +78 -0
  42. package/dist/voice.test.js +31 -0
  43. package/package.json +12 -7
  44. package/skills/egaki/SKILL.md +80 -15
  45. package/skills/errore/SKILL.md +13 -0
  46. package/skills/lintcn/SKILL.md +749 -0
  47. package/skills/npm-package/SKILL.md +17 -3
  48. package/skills/spiceflow/SKILL.md +14 -0
  49. package/skills/zele/SKILL.md +9 -0
  50. package/src/anthropic-auth-plugin.ts +732 -0
  51. package/src/channel-management.ts +2 -2
  52. package/src/cli.ts +354 -132
  53. package/src/commands/action-buttons.ts +1 -0
  54. package/src/commands/login.ts +836 -337
  55. package/src/commands/model.ts +102 -7
  56. package/src/commands/paginated-select.ts +81 -0
  57. package/src/commands/resume.ts +6 -1
  58. package/src/commands/tasks.ts +293 -0
  59. package/src/commands/undo-redo.ts +87 -20
  60. package/src/context-awareness-plugin.ts +469 -0
  61. package/src/database.ts +138 -7
  62. package/src/db.ts +40 -1
  63. package/src/discord-bot.ts +46 -19
  64. package/src/discord-urls.ts +12 -0
  65. package/src/errors.ts +1 -1
  66. package/src/hrana-server.ts +124 -3
  67. package/src/interaction-handler.ts +41 -9
  68. package/src/ipc-tools-plugin.ts +228 -0
  69. package/src/message-preprocessing.ts +82 -11
  70. package/src/onboarding-welcome.ts +1 -1
  71. package/src/opencode-interrupt-plugin.ts +164 -91
  72. package/src/opencode-plugin.ts +13 -483
  73. package/src/opencode.ts +60 -5
  74. package/src/parse-permission-rules.test.ts +127 -0
  75. package/src/queue-drain-after-interactive-ui.e2e.test.ts +151 -0
  76. package/src/session-handler/thread-runtime-state.ts +4 -1
  77. package/src/session-handler/thread-session-runtime.ts +82 -20
  78. package/src/startup-time.e2e.test.ts +372 -0
  79. package/src/store.ts +8 -0
  80. package/src/system-message.ts +10 -1
  81. package/src/task-runner.ts +9 -22
  82. package/src/task-schedule.ts +15 -0
  83. package/src/thread-message-queue.e2e.test.ts +14 -1
  84. package/src/undo-redo.e2e.test.ts +207 -0
  85. package/src/utils.ts +7 -0
  86. package/src/voice-attachment.ts +51 -0
  87. package/src/voice-handler.ts +15 -7
  88. package/src/voice-message.e2e.test.ts +95 -0
  89. package/src/voice.test.ts +36 -0
  90. package/src/onboarding-tutorial-plugin.ts +0 -93
@@ -0,0 +1,749 @@
1
+ ---
2
+ name: lintcn
3
+ description: >
4
+ Write, add, and update type-aware TypeScript lint rules in .lintcn/ Go files.
5
+ ALWAYS use this skill when creating, editing, or debugging .lintcn/*.go rule files.
6
+ Covers the tsgolint rule API, AST visitors, type checker, reporting, fixes,
7
+ testing, and all patterns from the 50+ built-in rules.
8
+ ---
9
+
10
+ # lintcn — Writing Custom tsgolint Lint Rules
11
+
12
+ tsgolint rules are Go functions that listen for TypeScript AST nodes and use the
13
+ TypeScript type checker for type-aware analysis. Rules live as `.go` files in
14
+ `.lintcn/` and are compiled into a custom tsgolint binary.
15
+
16
+ Always run `go build ./...` inside `.lintcn/` to validate rules compile.
17
+ Always run `go test -v ./...` inside `.lintcn/` to run tests.
18
+
19
+ ## Rule Anatomy
20
+
21
+ Every rule is a `rule.Rule` struct with a `Name` and a `Run` function.
22
+ `Run` receives a `RuleContext` and returns a `RuleListeners` map — a map from
23
+ `ast.Kind` to callback functions. The linter walks the AST and calls your
24
+ callback when it encounters a node of that kind.
25
+
26
+ ```go
27
+ package lintcn
28
+
29
+ import (
30
+ "github.com/microsoft/typescript-go/shim/ast"
31
+ "github.com/typescript-eslint/tsgolint/internal/rule"
32
+ )
33
+
34
+ var MyRule = rule.Rule{
35
+ Name: "my-rule",
36
+ Run: func(ctx rule.RuleContext, options any) rule.RuleListeners {
37
+ return rule.RuleListeners{
38
+ ast.KindCallExpression: func(node *ast.Node) {
39
+ call := node.AsCallExpression()
40
+ // analyze the call...
41
+ ctx.ReportNode(node, rule.RuleMessage{
42
+ Id: "myError",
43
+ Description: "Something is wrong here.",
44
+ })
45
+ },
46
+ }
47
+ },
48
+ }
49
+ ```
50
+
51
+ ### Metadata Comments
52
+
53
+ Add `// lintcn:` comments at the top for CLI metadata:
54
+
55
+ ```go
56
+ // lintcn:name my-rule
57
+ // lintcn:description Disallow doing X without checking Y
58
+ ```
59
+
60
+ ### Package Name
61
+
62
+ All rule files in `.lintcn/` share `package lintcn`. The exported variable name
63
+ must be unique and match the pattern `var XxxRule = rule.Rule{...}`.
64
+
65
+ ## RuleContext
66
+
67
+ `ctx rule.RuleContext` provides:
68
+
69
+ | Field | Type | Description |
70
+ |-------|------|-------------|
71
+ | `SourceFile` | `*ast.SourceFile` | Current file being linted |
72
+ | `Program` | `*compiler.Program` | Full TypeScript program |
73
+ | `TypeChecker` | `*checker.Checker` | TypeScript type checker |
74
+ | `ReportNode` | `func(node, msg)` | Report error on a node |
75
+ | `ReportNodeWithFixes` | `func(node, msg, fixesFn)` | Report with auto-fixes |
76
+ | `ReportNodeWithSuggestions` | `func(node, msg, suggFn)` | Report with suggestions |
77
+ | `ReportRange` | `func(range, msg)` | Report on a text range |
78
+ | `ReportDiagnostic` | `func(diagnostic)` | Report with labeled ranges |
79
+
80
+ ## AST Node Listeners
81
+
82
+ ### Most Useful ast.Kind Values
83
+
84
+ ```go
85
+ // Statements
86
+ ast.KindExpressionStatement // bare expression: `foo();`
87
+ ast.KindReturnStatement // `return x`
88
+ ast.KindThrowStatement // `throw x`
89
+ ast.KindIfStatement // `if (x) { ... }`
90
+ ast.KindVariableDeclaration // `const x = ...`
91
+ ast.KindForInStatement // `for (x in y)`
92
+
93
+ // Expressions
94
+ ast.KindCallExpression // `foo()` — most commonly listened
95
+ ast.KindNewExpression // `new Foo()`
96
+ ast.KindBinaryExpression // `a + b`, `a === b`, `a = b`
97
+ ast.KindPropertyAccessExpression // `obj.prop`
98
+ ast.KindElementAccessExpression // `obj[key]`
99
+ ast.KindAwaitExpression // `await x`
100
+ ast.KindConditionalExpression // `a ? b : c`
101
+ ast.KindPrefixUnaryExpression // `!x`, `-x`, `typeof x`
102
+ ast.KindTemplateExpression // `hello ${name}`
103
+ ast.KindDeleteExpression // `delete obj.x`
104
+ ast.KindVoidExpression // `void x`
105
+
106
+ // Declarations
107
+ ast.KindFunctionDeclaration
108
+ ast.KindArrowFunction
109
+ ast.KindMethodDeclaration
110
+ ast.KindClassDeclaration
111
+ ast.KindEnumDeclaration
112
+
113
+ // Types
114
+ ast.KindUnionType // `A | B`
115
+ ast.KindIntersectionType // `A & B`
116
+ ast.KindAsExpression // `x as T`
117
+ ```
118
+
119
+ ### Enter and Exit Listeners
120
+
121
+ By default, listeners fire when the AST walker **enters** a node.
122
+ Use `rule.ListenerOnExit(kind)` to fire when the walker **exits** — useful
123
+ for scope tracking:
124
+
125
+ ```go
126
+ return rule.RuleListeners{
127
+ // enter function — push scope
128
+ ast.KindFunctionDeclaration: func(node *ast.Node) {
129
+ currentScope = &scopeInfo{upper: currentScope}
130
+ },
131
+ // exit function — pop scope and check
132
+ rule.ListenerOnExit(ast.KindFunctionDeclaration): func(node *ast.Node) {
133
+ if !currentScope.hasAwait {
134
+ ctx.ReportNode(node, msg)
135
+ }
136
+ currentScope = currentScope.upper
137
+ },
138
+ }
139
+ ```
140
+
141
+ Used by require_await, return_await, consistent_return, prefer_readonly for
142
+ tracking state across function bodies with a scope stack.
143
+
144
+ ### Allow/NotAllow Pattern Listeners
145
+
146
+ For destructuring and assignment contexts:
147
+
148
+ ```go
149
+ rule.ListenerOnAllowPattern(ast.KindObjectLiteralExpression) // inside destructuring
150
+ rule.ListenerOnNotAllowPattern(ast.KindArrayLiteralExpression) // outside destructuring
151
+ ```
152
+
153
+ Used by no_unsafe_assignment and unbound_method.
154
+
155
+ ## Type Checker APIs
156
+
157
+ ### Getting Types
158
+
159
+ ```go
160
+ // Get the type of any AST node
161
+ t := ctx.TypeChecker.GetTypeAtLocation(node)
162
+
163
+ // Get type with constraint resolution (unwraps type params)
164
+ t := utils.GetConstrainedTypeAtLocation(ctx.TypeChecker, node)
165
+
166
+ // Get the contextual type (what TypeScript expects at this position)
167
+ t := checker.Checker_getContextualType(ctx.TypeChecker, node, checker.ContextFlagsNone)
168
+
169
+ // Get the apparent type (resolves mapped types, intersections)
170
+ t := checker.Checker_getApparentType(ctx.TypeChecker, t)
171
+
172
+ // Get awaited type (unwraps Promise)
173
+ t := checker.Checker_getAwaitedType(ctx.TypeChecker, t)
174
+
175
+ // Get type from a type annotation node
176
+ t := checker.Checker_getTypeFromTypeNode(ctx.TypeChecker, typeNode)
177
+ ```
178
+
179
+ ### Type Flag Checks
180
+
181
+ TypeFlags are bitmasks — check with `utils.IsTypeFlagSet`:
182
+
183
+ ```go
184
+ // Check specific flags
185
+ if utils.IsTypeFlagSet(t, checker.TypeFlagsVoid) { return }
186
+ if utils.IsTypeFlagSet(t, checker.TypeFlagsUndefined) { return }
187
+ if utils.IsTypeFlagSet(t, checker.TypeFlagsNever) { return }
188
+ if utils.IsTypeFlagSet(t, checker.TypeFlagsAny) { return }
189
+
190
+ // Combine flags with |
191
+ if utils.IsTypeFlagSet(t, checker.TypeFlagsVoid|checker.TypeFlagsUndefined|checker.TypeFlagsNever) {
192
+ return // skip void, undefined, and never
193
+ }
194
+
195
+ // Convenience helpers
196
+ utils.IsTypeAnyType(t)
197
+ utils.IsTypeUnknownType(t)
198
+ utils.IsObjectType(t)
199
+ utils.IsTypeParameter(t)
200
+ ```
201
+
202
+ ### Union and Intersection Types
203
+
204
+ **Decomposing unions is the most common pattern** — 58 uses across all rules:
205
+
206
+ ```go
207
+ // Iterate over union parts: `Error | string` → [Error, string]
208
+ for _, part := range utils.UnionTypeParts(t) {
209
+ if utils.IsErrorLike(ctx.Program, ctx.TypeChecker, part) {
210
+ hasError = true
211
+ break
212
+ }
213
+ }
214
+
215
+ // Check if it's a union type
216
+ if utils.IsUnionType(t) { ... }
217
+ if utils.IsIntersectionType(t) { ... }
218
+
219
+ // Iterate intersection parts
220
+ for _, part := range utils.IntersectionTypeParts(t) { ... }
221
+
222
+ // Recursive predicate check across union/intersection
223
+ result := utils.TypeRecurser(t, func(t *checker.Type) bool {
224
+ return utils.IsTypeAnyType(t)
225
+ })
226
+ ```
227
+
228
+ ### Built-in Type Checks
229
+
230
+ ```go
231
+ // Error types
232
+ utils.IsErrorLike(ctx.Program, ctx.TypeChecker, t)
233
+ utils.IsReadonlyErrorLike(ctx.Program, ctx.TypeChecker, t)
234
+
235
+ // Promise types
236
+ utils.IsPromiseLike(ctx.Program, ctx.TypeChecker, t)
237
+ utils.IsThenableType(ctx.TypeChecker, node, t)
238
+
239
+ // Array types
240
+ checker.Checker_isArrayType(ctx.TypeChecker, t)
241
+ checker.IsTupleType(t)
242
+ checker.Checker_isArrayOrTupleType(ctx.TypeChecker, t)
243
+
244
+ // Generic built-in matching
245
+ utils.IsBuiltinSymbolLike(ctx.Program, ctx.TypeChecker, t, "Function")
246
+ utils.IsBuiltinSymbolLike(ctx.Program, ctx.TypeChecker, t, "RegExp")
247
+ utils.IsBuiltinSymbolLike(ctx.Program, ctx.TypeChecker, t, "ReadonlyArray")
248
+ ```
249
+
250
+ ### Type Properties and Signatures
251
+
252
+ ```go
253
+ // Get a named property from a type
254
+ prop := checker.Checker_getPropertyOfType(ctx.TypeChecker, t, "then")
255
+ if prop != nil {
256
+ propType := ctx.TypeChecker.GetTypeOfSymbolAtLocation(prop, node)
257
+ }
258
+
259
+ // Get all properties
260
+ props := checker.Checker_getPropertiesOfType(ctx.TypeChecker, t)
261
+
262
+ // Get call signatures (for callable types)
263
+ sigs := utils.GetCallSignatures(ctx.TypeChecker, t)
264
+ // or
265
+ sigs := ctx.TypeChecker.GetCallSignatures(t)
266
+
267
+ // Get signature parameters
268
+ params := checker.Signature_parameters(sig)
269
+
270
+ // Get return type of a signature
271
+ returnType := checker.Checker_getReturnTypeOfSignature(ctx.TypeChecker, sig)
272
+
273
+ // Get type arguments (for generics, arrays, tuples)
274
+ typeArgs := checker.Checker_getTypeArguments(ctx.TypeChecker, t)
275
+
276
+ // Get resolved call signature at a call site
277
+ sig := checker.Checker_getResolvedSignature(ctx.TypeChecker, callNode)
278
+ ```
279
+
280
+ ### Type Assignability
281
+
282
+ ```go
283
+ // Check if source is assignable to target
284
+ if checker.Checker_isTypeAssignableTo(ctx.TypeChecker, sourceType, targetType) {
285
+ // source extends target
286
+ }
287
+
288
+ // Get base constraint of a type parameter
289
+ constraint := checker.Checker_getBaseConstraintOfType(ctx.TypeChecker, t)
290
+ ```
291
+
292
+ ### Symbols
293
+
294
+ ```go
295
+ // Get symbol at a location
296
+ symbol := ctx.TypeChecker.GetSymbolAtLocation(node)
297
+
298
+ // Get declaration for a symbol
299
+ decl := utils.GetDeclaration(ctx.TypeChecker, node)
300
+
301
+ // Get type from symbol
302
+ t := checker.Checker_getTypeOfSymbol(ctx.TypeChecker, symbol)
303
+ t := checker.Checker_getDeclaredTypeOfSymbol(ctx.TypeChecker, symbol)
304
+
305
+ // Check if symbol comes from default library
306
+ utils.IsSymbolFromDefaultLibrary(ctx.Program, symbol)
307
+
308
+ // Get the accessed property name (works with computed properties too)
309
+ name, ok := checker.Checker_getAccessedPropertyName(ctx.TypeChecker, node)
310
+ ```
311
+
312
+ ### Formatting Types for Error Messages
313
+
314
+ ```go
315
+ typeName := ctx.TypeChecker.TypeToString(t)
316
+ // → "string", "Error | User", "Promise<number>", etc.
317
+
318
+ // Shorter type name helper
319
+ name := utils.GetTypeName(ctx.TypeChecker, t)
320
+ ```
321
+
322
+ ## AST Navigation
323
+
324
+ ### Node Casting
325
+
326
+ Every AST node is `*ast.Node`. Use `.AsXxx()` to access specific fields:
327
+
328
+ ```go
329
+ call := node.AsCallExpression()
330
+ call.Expression // the callee
331
+ call.Arguments // argument list
332
+
333
+ binary := node.AsBinaryExpression()
334
+ binary.Left
335
+ binary.Right
336
+ binary.OperatorToken.Kind // ast.KindEqualsToken, ast.KindPlusToken, etc.
337
+
338
+ prop := node.AsPropertyAccessExpression()
339
+ prop.Expression // object
340
+ prop.Name() // property name node
341
+ ```
342
+
343
+ ### Type Predicates
344
+
345
+ ```go
346
+ ast.IsCallExpression(node)
347
+ ast.IsPropertyAccessExpression(node)
348
+ ast.IsIdentifier(node)
349
+ ast.IsAccessExpression(node) // property OR element access
350
+ ast.IsBinaryExpression(node)
351
+ ast.IsAssignmentExpression(node, includeCompound) // a = b, a += b
352
+ ast.IsVoidExpression(node)
353
+ ast.IsAwaitExpression(node)
354
+ ast.IsFunctionLike(node)
355
+ ast.IsArrowFunction(node)
356
+ ast.IsStringLiteral(node)
357
+ ```
358
+
359
+ ### Skipping Parentheses
360
+
361
+ Always skip parentheses when analyzing expression content:
362
+
363
+ ```go
364
+ expression := ast.SkipParentheses(node.AsExpressionStatement().Expression)
365
+ ```
366
+
367
+ ### Walking Parents
368
+
369
+ ```go
370
+ parent := node.Parent
371
+ for parent != nil {
372
+ if ast.IsCallExpression(parent) {
373
+ // node is inside a call expression
374
+ break
375
+ }
376
+ parent = parent.Parent
377
+ }
378
+ ```
379
+
380
+ ## Reporting Errors
381
+
382
+ ### Simple Error
383
+
384
+ ```go
385
+ ctx.ReportNode(node, rule.RuleMessage{
386
+ Id: "myErrorId", // unique ID for the error
387
+ Description: "Something is wrong.",
388
+ Help: "Optional longer explanation.", // shown as help text
389
+ })
390
+ ```
391
+
392
+ ### Error with Auto-Fix
393
+
394
+ Fixes are applied automatically by the linter:
395
+
396
+ ```go
397
+ ctx.ReportNodeWithFixes(node, msg, func() []rule.RuleFix {
398
+ return []rule.RuleFix{
399
+ rule.RuleFixInsertBefore(ctx.SourceFile, node, "await "),
400
+ }
401
+ })
402
+ ```
403
+
404
+ ### Error with Suggestions
405
+
406
+ Suggestions require user confirmation:
407
+
408
+ ```go
409
+ ctx.ReportNodeWithSuggestions(node, msg, func() []rule.RuleSuggestion {
410
+ return []rule.RuleSuggestion{{
411
+ Message: rule.RuleMessage{Id: "addAwait", Description: "Add await"},
412
+ FixesArr: []rule.RuleFix{
413
+ rule.RuleFixInsertBefore(ctx.SourceFile, node, "await "),
414
+ },
415
+ }}
416
+ })
417
+ ```
418
+
419
+ ### Error with Multiple Labeled Ranges
420
+
421
+ Highlight multiple code locations:
422
+
423
+ ```go
424
+ ctx.ReportDiagnostic(rule.RuleDiagnostic{
425
+ Range: exprRange,
426
+ Message: rule.RuleMessage{Id: "typeMismatch", Description: "Types are incompatible"},
427
+ LabeledRanges: []rule.RuleLabeledRange{
428
+ {Label: fmt.Sprintf("Type: %v", leftType), Range: leftRange},
429
+ {Label: fmt.Sprintf("Type: %v", rightType), Range: rightRange},
430
+ },
431
+ })
432
+ ```
433
+
434
+ ### Fix Helpers
435
+
436
+ ```go
437
+ // Insert text before a node
438
+ rule.RuleFixInsertBefore(ctx.SourceFile, node, "await ")
439
+
440
+ // Insert text after a node
441
+ rule.RuleFixInsertAfter(node, ")")
442
+
443
+ // Replace a node with text
444
+ rule.RuleFixReplace(ctx.SourceFile, node, "newCode")
445
+
446
+ // Remove a node
447
+ rule.RuleFixRemove(ctx.SourceFile, node)
448
+
449
+ // Replace a specific text range
450
+ rule.RuleFixReplaceRange(textRange, "replacement")
451
+
452
+ // Remove a specific text range
453
+ rule.RuleFixRemoveRange(textRange)
454
+ ```
455
+
456
+ ### Getting Token Ranges for Fixes
457
+
458
+ When you need the exact range of a keyword token (like `void`, `as`, `await`):
459
+
460
+ ```go
461
+ import "github.com/microsoft/typescript-go/shim/scanner"
462
+
463
+ // Get range of token at a position
464
+ voidTokenRange := scanner.GetRangeOfTokenAtPosition(ctx.SourceFile, node.Pos())
465
+
466
+ // Get a scanner to scan forward
467
+ s := scanner.GetScannerForSourceFile(ctx.SourceFile, startPos)
468
+ tokenRange := s.TokenRange()
469
+ ```
470
+
471
+ ## Rule Options
472
+
473
+ Rules can accept configuration via JSON:
474
+
475
+ ```go
476
+ var MyRule = rule.Rule{
477
+ Name: "my-rule",
478
+ Run: func(ctx rule.RuleContext, options any) rule.RuleListeners {
479
+ opts := utils.UnmarshalOptions[MyRuleOptions](options, "my-rule")
480
+ // opts is now typed
481
+ },
482
+ }
483
+
484
+ type MyRuleOptions struct {
485
+ IgnoreVoid bool `json:"ignoreVoid"`
486
+ AllowedTypes []string `json:"allowedTypes"`
487
+ }
488
+ ```
489
+
490
+ For lintcn rules, define the options struct directly in your rule file.
491
+ Built-in tsgolint rules use `schema.json` + codegen, but for custom rules
492
+ a manual struct is simpler.
493
+
494
+ ## State Tracking (Scope Stacks)
495
+
496
+ When you need to track state across function boundaries (like "does this
497
+ function contain an await?"), use enter/exit listener pairs with a linked
498
+ list as a stack:
499
+
500
+ ```go
501
+ type scopeInfo struct {
502
+ hasAwait bool
503
+ upper *scopeInfo
504
+ }
505
+ var currentScope *scopeInfo
506
+
507
+ enterFunc := func(node *ast.Node) {
508
+ currentScope = &scopeInfo{upper: currentScope}
509
+ }
510
+
511
+ exitFunc := func(node *ast.Node) {
512
+ if !currentScope.hasAwait {
513
+ ctx.ReportNode(node, msg)
514
+ }
515
+ currentScope = currentScope.upper
516
+ }
517
+
518
+ return rule.RuleListeners{
519
+ ast.KindFunctionDeclaration: enterFunc,
520
+ rule.ListenerOnExit(ast.KindFunctionDeclaration): exitFunc,
521
+ ast.KindArrowFunction: enterFunc,
522
+ rule.ListenerOnExit(ast.KindArrowFunction): exitFunc,
523
+ ast.KindAwaitExpression: func(node *ast.Node) {
524
+ currentScope.hasAwait = true
525
+ },
526
+ }
527
+ ```
528
+
529
+ ## Testing
530
+
531
+ Tests use `rule_tester.RunRuleTester` which creates a TypeScript program from
532
+ inline code and runs the rule against it.
533
+
534
+ ```go
535
+ package lintcn
536
+
537
+ import (
538
+ "testing"
539
+ "github.com/typescript-eslint/tsgolint/internal/rule_tester"
540
+ "github.com/typescript-eslint/tsgolint/internal/rules/fixtures"
541
+ )
542
+
543
+ func TestMyRule(t *testing.T) {
544
+ t.Parallel()
545
+ rule_tester.RunRuleTester(
546
+ fixtures.GetRootDir(),
547
+ "tsconfig.minimal.json",
548
+ t,
549
+ &MyRule,
550
+ validCases,
551
+ invalidCases,
552
+ )
553
+ }
554
+ ```
555
+
556
+ ### Valid Test Cases (should NOT trigger)
557
+
558
+ ```go
559
+ var validCases = []rule_tester.ValidTestCase{
560
+ {Code: `const x = getUser("id");`},
561
+ {Code: `void dangerousCall();`},
562
+ // tsx support
563
+ {Code: `<div onClick={() => {}} />`, Tsx: true},
564
+ // custom filename
565
+ {Code: `import x from './foo'`, FileName: "index.ts"},
566
+ // with rule options
567
+ {Code: `getUser("id");`, Options: MyRuleOptions{IgnoreVoid: true}},
568
+ // with extra files for multi-file tests
569
+ {
570
+ Code: `import { x } from './helper';`,
571
+ Files: map[string]string{
572
+ "helper.ts": `export const x = 1;`,
573
+ },
574
+ },
575
+ }
576
+ ```
577
+
578
+ ### Invalid Test Cases (SHOULD trigger)
579
+
580
+ ```go
581
+ var invalidCases = []rule_tester.InvalidTestCase{
582
+ // Basic — just check the error fires
583
+ {
584
+ Code: `
585
+ declare function getUser(id: string): Error | { name: string };
586
+ getUser("id");
587
+ `,
588
+ Errors: []rule_tester.InvalidTestCaseError{
589
+ {MessageId: "noUnhandledError"},
590
+ },
591
+ },
592
+ // With exact position
593
+ {
594
+ Code: `getUser("id");`,
595
+ Errors: []rule_tester.InvalidTestCaseError{
596
+ {MessageId: "noUnhandledError", Line: 1, Column: 1, EndColumn: 15},
597
+ },
598
+ },
599
+ // With suggestions
600
+ {
601
+ Code: `
602
+ declare const arr: number[];
603
+ delete arr[0];
604
+ `,
605
+ Errors: []rule_tester.InvalidTestCaseError{
606
+ {
607
+ MessageId: "noArrayDelete",
608
+ Suggestions: []rule_tester.InvalidTestCaseSuggestion{
609
+ {
610
+ MessageId: "useSplice",
611
+ Output: `
612
+ declare const arr: number[];
613
+ arr.splice(0, 1);
614
+ `,
615
+ },
616
+ },
617
+ },
618
+ },
619
+ },
620
+ // With auto-fix output (code after fix applied)
621
+ {
622
+ Code: `const x = foo as any;`,
623
+ Output: []string{`const x = foo;`},
624
+ Errors: []rule_tester.InvalidTestCaseError{
625
+ {MessageId: "unsafeAssertion"},
626
+ },
627
+ },
628
+ }
629
+ ```
630
+
631
+ ### Important Test Details
632
+
633
+ - **MessageId** must match the `Id` field in your `rule.RuleMessage`
634
+ - **Line/Column** are 1-indexed, optional (omit for flexibility)
635
+ - **Output** is the code after ALL auto-fixes are applied (iterates up to 10 times)
636
+ - **Suggestions** check the output of each individual suggestion fix
637
+ - Tests run in parallel by default (`t.Parallel()`)
638
+ - Use `Only: true` on a test case to run only that test (like `.only` in vitest)
639
+ - Use `Skip: true` to skip a test case
640
+
641
+ ### Running Tests
642
+
643
+ ```bash
644
+ cd .lintcn
645
+ go test -v ./... # all tests
646
+ go test -v -run TestMyRule # specific test
647
+ go test -count=1 ./... # bypass test cache
648
+ ```
649
+
650
+ ## Complete Rule Example: no-unhandled-error
651
+
652
+ A real rule that enforces the errore pattern — errors when a call expression
653
+ returns a type containing `Error` and the result is discarded:
654
+
655
+ ```go
656
+ // lintcn:name no-unhandled-error
657
+ // lintcn:description Disallow discarding expressions that are subtypes of Error
658
+
659
+ package lintcn
660
+
661
+ import (
662
+ "github.com/microsoft/typescript-go/shim/ast"
663
+ "github.com/microsoft/typescript-go/shim/checker"
664
+ "github.com/typescript-eslint/tsgolint/internal/rule"
665
+ "github.com/typescript-eslint/tsgolint/internal/utils"
666
+ )
667
+
668
+ var NoUnhandledErrorRule = rule.Rule{
669
+ Name: "no-unhandled-error",
670
+ Run: func(ctx rule.RuleContext, options any) rule.RuleListeners {
671
+ return rule.RuleListeners{
672
+ ast.KindExpressionStatement: func(node *ast.Node) {
673
+ exprStatement := node.AsExpressionStatement()
674
+ expression := ast.SkipParentheses(exprStatement.Expression)
675
+
676
+ // void expressions are intentional discards
677
+ if ast.IsVoidExpression(expression) {
678
+ return
679
+ }
680
+
681
+ // only check call expressions and await expressions wrapping calls
682
+ innerExpr := expression
683
+ if ast.IsAwaitExpression(innerExpr) {
684
+ innerExpr = ast.SkipParentheses(innerExpr.Expression())
685
+ }
686
+ if !ast.IsCallExpression(innerExpr) {
687
+ return
688
+ }
689
+
690
+ t := ctx.TypeChecker.GetTypeAtLocation(expression)
691
+
692
+ // skip void, undefined, never
693
+ if utils.IsTypeFlagSet(t,
694
+ checker.TypeFlagsVoid|checker.TypeFlagsVoidLike|
695
+ checker.TypeFlagsUndefined|checker.TypeFlagsNever) {
696
+ return
697
+ }
698
+
699
+ // check if any union part is Error-like
700
+ for _, part := range utils.UnionTypeParts(t) {
701
+ if utils.IsErrorLike(ctx.Program, ctx.TypeChecker, part) {
702
+ ctx.ReportNode(node, rule.RuleMessage{
703
+ Id: "noUnhandledError",
704
+ Description: "Error-typed return value is not handled.",
705
+ })
706
+ return
707
+ }
708
+ }
709
+ },
710
+ }
711
+ },
712
+ }
713
+ ```
714
+
715
+ ## Go Workspace Setup
716
+
717
+ `.lintcn/` needs these generated files (created by `lintcn add` or manually):
718
+
719
+ **go.mod** — module name MUST be a child path of tsgolint for `internal/`
720
+ package access:
721
+
722
+ ```
723
+ module github.com/typescript-eslint/tsgolint/lintcn-rules
724
+
725
+ go 1.26
726
+ ```
727
+
728
+ **go.work** — workspace linking to cached tsgolint source:
729
+
730
+ ```
731
+ go 1.26
732
+
733
+ use (
734
+ .
735
+ ./.tsgolint
736
+ ./.tsgolint/typescript-go
737
+ )
738
+
739
+ replace (
740
+ github.com/microsoft/typescript-go/shim/ast => ./.tsgolint/shim/ast
741
+ github.com/microsoft/typescript-go/shim/checker => ./.tsgolint/shim/checker
742
+ // ... all 14 shim modules
743
+ )
744
+ ```
745
+
746
+ **.tsgolint/** — symlink to cached tsgolint clone (gitignored).
747
+
748
+ With this setup, gopls provides full autocomplete and go-to-definition on all
749
+ tsgolint and typescript-go APIs.