Skip to content

Commit 6aeff6a

Browse files
feat: port TypeScript-ESLint rules with LSP improvements and complete safety integration (#154)
1 parent 25a31f5 commit 6aeff6a

37 files changed

+14804
-41
lines changed

CLAUDE.md

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
# Rslint Development Guide for AI Agents
2+
3+
This guide helps AI agents understand and work with the Rslint codebase effectively.
4+
5+
## Core Concepts
6+
7+
Rslint is a TypeScript/JavaScript linter written in Go that implements TypeScript-ESLint rules. It uses the TypeScript compiler API through a Go shim and provides diagnostics via CLI and Language Server Protocol (LSP).
8+
9+
## Rule Implementation Guide
10+
11+
### Creating a New TypeScript-ESLint Rule
12+
13+
1. **Create the rule file**: `internal/rules/<rule_name>/<rule_name>.go`
14+
2. **Define the rule structure**:
15+
16+
```go
17+
package rule_name
18+
19+
import (
20+
"github.com/microsoft/typescript-go/shim/ast"
21+
"github.com/web-infra-dev/rslint/internal/rule"
22+
)
23+
24+
var RuleNameRule = rule.Rule{
25+
Name: "rule-name", // Use short name WITHOUT @typescript-eslint/ prefix
26+
Run: func(ctx rule.RuleContext, options any) rule.RuleListeners {
27+
return rule.RuleListeners{
28+
ast.KindSomeNode: func(node *ast.Node) {
29+
// Rule logic here
30+
},
31+
}
32+
},
33+
}
34+
```
35+
36+
3. **Register the rule in `internal/config/config.go`**:
37+
38+
- Add import: `"github.com/web-infra-dev/rslint/internal/rules/rule_name"`
39+
- In `RegisterAllTypeSriptEslintPluginRules()`, add:
40+
```go
41+
GlobalRuleRegistry.Register("@typescript-eslint/rule-name", rule_name.RuleNameRule)
42+
```
43+
44+
4. **Add the rule to the API hardcoded list in `cmd/rslint/api.go`**:
45+
46+
- Add import: `"github.com/web-infra-dev/rslint/internal/rules/rule_name"`
47+
- In the `origin_rules` slice (around line 100), add:
48+
```go
49+
rule_name.RuleNameRule,
50+
```
51+
- **IMPORTANT**: The API uses a hardcoded list for the test runner. If you don't add your rule here, tests will fail with "Expected diagnostics for invalid case" errors.
52+
53+
5. **Add struct field to TypedRules if the rule needs configuration**
54+
55+
### Critical Safety Requirements
56+
57+
**Always check for nil pointers** when working with AST nodes:
58+
59+
```go
60+
// ALWAYS do this:
61+
typeRef := node.AsTypeReference()
62+
if typeRef == nil {
63+
return
64+
}
65+
66+
// Check nested properties:
67+
if typeRef.TypeArguments != nil && len(typeRef.TypeArguments.Nodes) > 0 {
68+
// safe to access
69+
}
70+
```
71+
72+
Common nil-check patterns:
73+
74+
- `node.AsXXX()` methods can return nil
75+
- `node.Parent` can be nil for root nodes
76+
- `nodeList.Nodes` - check nodeList isn't nil first
77+
- Check each level when accessing nested properties
78+
79+
### Reporting Diagnostics
80+
81+
Use `ctx.ReportNode()` to emit diagnostics:
82+
83+
```go
84+
ctx.ReportNode(node, rule.RuleMessage{
85+
Id: "messageId",
86+
Description: "Clear description of the violation",
87+
})
88+
89+
// With auto-fix:
90+
ctx.ReportNodeWithFixes(node, message,
91+
rule.RuleFixReplace(ctx.SourceFile, node, "replacement text"))
92+
```
93+
94+
## Testing Rules
95+
96+
### Unit Tests
97+
98+
Create test file: `packages/rslint-test-tools/tests/typescript-eslint/rules/<rule-name>.test.ts`
99+
100+
```typescript
101+
import { RuleTester } from '@typescript-eslint/rule-tester';
102+
import { getFixturesRootDir } from '../RuleTester.ts';
103+
104+
const rootDir = getFixturesRootDir();
105+
106+
const ruleTester = new RuleTester({
107+
languageOptions: {
108+
parserOptions: {
109+
project: './tsconfig.json',
110+
tsconfigRootDir: rootDir,
111+
},
112+
},
113+
});
114+
115+
// Use the rule name WITHOUT the @typescript-eslint/ prefix
116+
ruleTester.run('rule-name', {
117+
valid: ['valid code examples'],
118+
invalid: [
119+
{
120+
code: 'invalid code',
121+
errors: [
122+
{
123+
messageId: 'messageId',
124+
line: 1,
125+
column: 1,
126+
endLine: 1,
127+
endColumn: 10,
128+
},
129+
],
130+
},
131+
],
132+
});
133+
```
134+
135+
**Important**: The test runner expects exact error positions. Always include line/column information in error expectations.
136+
137+
### Manual Testing
138+
139+
```bash
140+
# Build the project
141+
pnpm build
142+
143+
# Test directly
144+
cd packages/rslint/fixtures
145+
../bin/rslint src/test.ts
146+
147+
# Add rule to fixtures/rslint.json to enable it
148+
```
149+
150+
## Important Implementation Details
151+
152+
### Rule Naming Convention
153+
154+
- **Rule implementation**: Use short name (e.g., `"array-type"`)
155+
- **Registration**: Use full name (e.g., `"@typescript-eslint/array-type"`)
156+
- **Configuration files**: Use full name with prefix
157+
158+
### AST Navigation
159+
160+
Use the TypeScript AST through the Go shim:
161+
162+
- `ast.KindXXX` constants for node types
163+
- `node.AsXXX()` methods for type assertions (always check for nil)
164+
- `ast.IsXXX(node)` helper functions for type checking
165+
166+
### Running Tests
167+
168+
```bash
169+
# All tests
170+
pnpm test
171+
172+
# Just Go tests
173+
go test ./...
174+
175+
# Specific rule tests
176+
cd packages/rslint-test-tools
177+
pnpm test <rule-name>
178+
```
179+
180+
### CI Requirements
181+
182+
Your changes must pass:
183+
184+
- `golangci-lint` - Go code quality
185+
- `go fmt` - Go formatting
186+
- `go vet` - Go static analysis
187+
- `go test -parallel 4 ./internal/...` - Go unit tests
188+
- `pnpm tsc -b tsconfig.json` - TypeScript type checking
189+
- `pnpm test` - TypeScript/JavaScript unit tests
190+
- `pnpm run lint` - ESLint and other linting checks
191+
- VSCode extension tests (may have timing issues, focus on Go tests)
192+
193+
## Debugging Tips
194+
195+
1. **Check rule registration**: Ensure the rule is in `RegisterAllTypeSriptEslintPluginRules()`
196+
2. **Verify configuration**: Rule must be in `rslint.json` to generate diagnostics
197+
3. **Test CLI directly**: Use `rslint` binary to verify rule works
198+
4. **VSCode extension tests**: If failing with "Expected diagnostics but got 0", this is usually due to missing rule registration (not LSP issues)
199+
5. **Add logging**: Use `log.Printf()` in LSP code (outputs to stderr)
200+
201+
## Common Pitfalls to Avoid
202+
203+
1. **Don't modify** `getAllTypeScriptEslintPluginRules()` - it must match main branch
204+
2. **Don't change** core infrastructure without understanding impacts
205+
3. **Always handle nil** from type assertions
206+
4. **Test with real TypeScript code** to ensure rule behaves correctly
207+
5. **Missing API registration** - Always add new rules to the hardcoded list in `cmd/rslint/api.go`
208+
6. **Test failures** - "Expected diagnostics for invalid case" usually means the rule isn't registered in the API
209+
7. **Wrong rule name in tests** - Use the short name without @typescript-eslint/ prefix in test files
210+
8. **VSCode test failures** - Diagnostic tests may fail initially due to LSP timing, but should pass consistently after proper rule registration
211+
212+
## Complete Checklist for Adding a New Rule
213+
214+
1. [ ] Create rule implementation in `internal/rules/<rule_name>/<rule_name>.go`
215+
2. [ ] Add nil checks for all AST node type assertions
216+
3. [ ] Register in `internal/config/config.go` with full @typescript-eslint/ prefix
217+
4. [ ] Add to hardcoded list in `cmd/rslint/api.go`
218+
5. [ ] Create test file in `packages/rslint-test-tools/tests/typescript-eslint/rules/`
219+
6. [ ] Run `pnpm build` to compile everything
220+
7. [ ] Run `pnpm test` to verify tests pass
221+
8. [ ] Test manually with CLI: `cd packages/rslint/fixtures && ../bin/rslint src/test.ts`
222+
9. [ ] Update test snapshots if needed: `pnpm test -u <rule-name>`
223+
10. [ ] Run Go quality checks: `go vet ./cmd/... ./internal/...` and `go fmt ./cmd/... ./internal/...`
224+
11. [ ] Run Go unit tests: `go test -parallel 4 ./internal/...`
225+
12. [ ] Run TypeScript type checking: `pnpm tsc -b tsconfig.json`
226+
13. [ ] Run all tests: `pnpm test`
227+
14. [ ] Run linting checks: `pnpm run lint`
228+
15. [ ] Ensure CI passes (golangci-lint, all tests)
229+
230+
## When You're Done
231+
232+
1. Ensure all tests pass: `pnpm test`
233+
2. Verify no linting errors: `pnpm build`
234+
3. Run Go quality checks: `go vet ./cmd/... ./internal/...` and `go fmt ./cmd/... ./internal/...`
235+
4. Run Go unit tests: `go test -parallel 4 ./internal/...`
236+
5. Run TypeScript type checking: `pnpm tsc -b tsconfig.json`
237+
6. Run linting checks: `pnpm run lint`
238+
7. Test your rule manually with the CLI
239+
8. Document any special behavior or options in the rule implementation
240+
241+
## Go Test Infrastructure Notes
242+
243+
### AST Node Kinds
244+
245+
- CallExpression nodes use `ast.KindCallExpression` (value: 213)
246+
- Node kind definitions are in `typescript-go/internal/ast/kind.go`
247+
- The TypeScript Go shim properly maps all AST node types from the TypeScript compiler
248+
249+
### Common Go Test Issues
250+
251+
1. **File Path Comparison**: The test infrastructure uses `sourceFile.FileName()` for file paths. Ensure consistency between test and production code.
252+
2. **Options Handling**: Go tests may pass options as `map[string]interface{}` while TypeScript tests use arrays. Handle both formats:
253+
```go
254+
// Handle array format: [{ option: value }]
255+
if optArray, isArray := options.([]interface{}); isArray && len(optArray) > 0 {
256+
optsMap, ok = optArray[0].(map[string]interface{})
257+
}
258+
```
259+
3. **Regex Patterns**: Go regex patterns differ from JavaScript. In Go, `/foo/` is literal, use `"foo"` for matching substrings.
260+
261+
### AST Traversal
262+
263+
- The linter uses a dual-visitor pattern: `childVisitor` for main traversal and `patternVisitor` for destructuring patterns
264+
- All nodes are visited via `ForEachChild` which respects TypeScript's AST structure
265+
- Listeners are registered by node kind and executed during traversal
266+
- Use `isInVariableDeclaration` helper to check if a node is within any variable declaration ancestor

cmd/rslint/api.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ import (
1616
rslintconfig "github.com/web-infra-dev/rslint/internal/config"
1717
"github.com/web-infra-dev/rslint/internal/linter"
1818
"github.com/web-infra-dev/rslint/internal/rule"
19+
"github.com/web-infra-dev/rslint/internal/rules/adjacent_overload_signatures"
20+
"github.com/web-infra-dev/rslint/internal/rules/array_type"
1921
"github.com/web-infra-dev/rslint/internal/rules/await_thenable"
22+
"github.com/web-infra-dev/rslint/internal/rules/class_literal_property_style"
2023
"github.com/web-infra-dev/rslint/internal/rules/no_array_delete"
2124
"github.com/web-infra-dev/rslint/internal/rules/no_base_to_string"
2225
"github.com/web-infra-dev/rslint/internal/rules/no_confusing_void_expression"
@@ -41,8 +44,12 @@ import (
4144
"github.com/web-infra-dev/rslint/internal/rules/no_unsafe_return"
4245
"github.com/web-infra-dev/rslint/internal/rules/no_unsafe_type_assertion"
4346
"github.com/web-infra-dev/rslint/internal/rules/no_unsafe_unary_minus"
47+
"github.com/web-infra-dev/rslint/internal/rules/no_unused_vars"
48+
"github.com/web-infra-dev/rslint/internal/rules/no_useless_empty_export"
49+
"github.com/web-infra-dev/rslint/internal/rules/no_var_requires"
4450
"github.com/web-infra-dev/rslint/internal/rules/non_nullable_type_assertion_style"
4551
"github.com/web-infra-dev/rslint/internal/rules/only_throw_error"
52+
"github.com/web-infra-dev/rslint/internal/rules/prefer_as_const"
4653
"github.com/web-infra-dev/rslint/internal/rules/prefer_promise_reject_errors"
4754
"github.com/web-infra-dev/rslint/internal/rules/prefer_reduce_type_parameter"
4855
"github.com/web-infra-dev/rslint/internal/rules/prefer_return_this_type"
@@ -97,7 +104,10 @@ func (h *IPCHandler) HandleLint(req api.LintRequest) (*api.LintResponse, error)
97104

98105
// Create rules
99106
var origin_rules = []rule.Rule{
107+
adjacent_overload_signatures.AdjacentOverloadSignaturesRule,
108+
array_type.ArrayTypeRule,
100109
await_thenable.AwaitThenableRule,
110+
class_literal_property_style.ClassLiteralPropertyStyleRule,
101111
no_array_delete.NoArrayDeleteRule,
102112
no_base_to_string.NoBaseToStringRule,
103113
no_confusing_void_expression.NoConfusingVoidExpressionRule,
@@ -122,8 +132,12 @@ func (h *IPCHandler) HandleLint(req api.LintRequest) (*api.LintResponse, error)
122132
no_unsafe_return.NoUnsafeReturnRule,
123133
no_unsafe_type_assertion.NoUnsafeTypeAssertionRule,
124134
no_unsafe_unary_minus.NoUnsafeUnaryMinusRule,
135+
no_unused_vars.NoUnusedVarsRule,
136+
no_useless_empty_export.NoUselessEmptyExportRule,
137+
no_var_requires.NoVarRequiresRule,
125138
non_nullable_type_assertion_style.NonNullableTypeAssertionStyleRule,
126139
only_throw_error.OnlyThrowErrorRule,
140+
prefer_as_const.PreferAsConstRule,
127141
prefer_promise_reject_errors.PreferPromiseRejectErrorsRule,
128142
prefer_reduce_type_parameter.PreferReduceTypeParameterRule,
129143
prefer_return_this_type.PreferReturnThisTypeRule,

0 commit comments

Comments
 (0)