|
| 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 |
0 commit comments