Skip to content

Commit 7e8270c

Browse files
delino[bot]github-actions[bot]claude
authored
feat: Port ESLint core rule no-const-assign (#73)
## Summary This PR ports the ESLint core rule `no-const-assign` to rslint, disallowing reassignment of const variables. Follow-up task of PR #72. ## Implementation Details - ✅ Created new rule in `internal/rules/no_const_assign/` - ✅ Detects reassignment of const variables via assignment operators - ✅ Detects increment/decrement operators (++, --) - ✅ Handles all compound assignment operators (+=, -=, *=, /=, etc.) - ✅ Supports destructured const bindings - ✅ Properly excludes initializer assignments - ✅ Registered in global rule registry ## Rule Behavior The rule prevents reassignment of variables declared with `const` keywords. This helps prevent runtime errors and maintains code integrity. ### Invalid Patterns ```javascript // Direct reassignment const x = 0; x = 1; // Compound assignment const x = 0; x += 1; // Increment/decrement operators const x = 0; ++x; // Destructuring reassignment const {a: x} = {a: 0}; x = 1; ``` ### Valid Patterns ```javascript // Reading constant values const x = 0; foo(x); // Modifying properties (not reassigning the constant itself) const x = {key: 0}; x.key = 1; // For-in/for-of loops (x is redeclared on each iteration) for (const x in [1,2,3]) { foo(x); } // Different scope const x = 0; { let x; x = 1; } ``` ## Test Coverage - ✅ Ported comprehensive test cases from ESLint's test suite - ✅ **17 valid test cases** covering various scenarios - ✅ **31 invalid test cases** with expected error detection - ✅ Tests include: - Direct reassignment and compound assignments - All assignment operators (=, +=, -=, *=, /=, %=, **=, <<=, >>=, >>>=, &=, |=, ^=, ??=, &&=, ||=) - Increment and decrement operators (prefix and postfix) - Destructuring patterns (object and array) - Scope shadowing scenarios - Property modification vs reassignment ## Test Plan - [x] Rule implementation follows rslint patterns - [x] All test cases ported from ESLint - [ ] Tests pass (requires full build environment with submodules) - [ ] Manual testing with example code - [ ] Integration with existing linter setup ## References - ESLint Rule: https://eslint.org/docs/latest/rules/no-const-assign - ESLint Source: https://github.com/eslint/eslint/blob/main/lib/rules/no-const-assign.js - ESLint Tests: https://github.com/eslint/eslint/blob/main/tests/lib/rules/no-const-assign.js - Related PR #72: #72 ## Files Changed - `internal/config/config.go` - Added rule registration (2 lines) - `internal/rules/no_const_assign/no_const_assign.go` - Complete rule implementation (365 lines) - `internal/rules/no_const_assign/no_const_assign_test.go` - Comprehensive test suite (230 lines) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent d9be5fc commit 7e8270c

File tree

3 files changed

+738
-0
lines changed

3 files changed

+738
-0
lines changed

internal/config/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ import (
8181
"github.com/web-infra-dev/rslint/internal/rules/no_class_assign"
8282
"github.com/web-infra-dev/rslint/internal/rules/no_compare_neg_zero"
8383
"github.com/web-infra-dev/rslint/internal/rules/no_cond_assign"
84+
"github.com/web-infra-dev/rslint/internal/rules/no_const_assign"
8485
)
8586

8687
// RslintConfig represents the top-level configuration array
@@ -431,6 +432,7 @@ func registerAllCoreEslintRules() {
431432
GlobalRuleRegistry.Register("no-class-assign", no_class_assign.NoClassAssignRule)
432433
GlobalRuleRegistry.Register("no-compare-neg-zero", no_compare_neg_zero.NoCompareNegZeroRule)
433434
GlobalRuleRegistry.Register("no-cond-assign", no_cond_assign.NoCondAssignRule)
435+
GlobalRuleRegistry.Register("no-const-assign", no_const_assign.NoConstAssignRule)
434436
}
435437

436438
// getAllTypeScriptEslintPluginRules returns all registered rules (for backward compatibility when no config is provided)
Lines changed: 372 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,372 @@
1+
package no_const_assign
2+
3+
import (
4+
"github.com/microsoft/typescript-go/shim/ast"
5+
"github.com/web-infra-dev/rslint/internal/rule"
6+
)
7+
8+
// Message builder
9+
func buildConstMessage(name string) rule.RuleMessage {
10+
return rule.RuleMessage{
11+
Id: "const",
12+
Description: "'" + name + "' is constant.",
13+
}
14+
}
15+
16+
// isConstBinding checks if a variable declaration is a const binding
17+
func isConstBinding(node *ast.Node) bool {
18+
if node == nil || node.Kind != ast.KindVariableDeclarationList {
19+
return false
20+
}
21+
22+
varDeclList := node.AsVariableDeclarationList()
23+
if varDeclList == nil {
24+
return false
25+
}
26+
27+
// Check if the declaration is const (or using/await using in the future)
28+
// In TypeScript AST, const declarations have flags
29+
return (varDeclList.Flags & ast.NodeFlagsConst) != 0
30+
}
31+
32+
// getIdentifierName gets the name of an identifier node
33+
func getIdentifierName(node *ast.Node) string {
34+
if node == nil || node.Kind != ast.KindIdentifier {
35+
return ""
36+
}
37+
38+
return node.Text()
39+
}
40+
41+
// isWriteReference checks if a reference is a write operation (assignment, increment, decrement, etc.)
42+
func isWriteReference(node *ast.Node) bool {
43+
if node == nil {
44+
return false
45+
}
46+
47+
parent := node.Parent
48+
if parent == nil {
49+
return false
50+
}
51+
52+
switch parent.Kind {
53+
case ast.KindBinaryExpression:
54+
// Check if this is an assignment operation
55+
binary := parent.AsBinaryExpression()
56+
if binary == nil {
57+
return false
58+
}
59+
60+
// Check if the node is on the left side of an assignment
61+
if binary.Left != node {
62+
return false
63+
}
64+
65+
// Check for all assignment operators
66+
switch binary.OperatorToken.Kind {
67+
case ast.KindEqualsToken, // =
68+
ast.KindPlusEqualsToken, // +=
69+
ast.KindMinusEqualsToken, // -=
70+
ast.KindAsteriskEqualsToken, // *=
71+
ast.KindSlashEqualsToken, // /=
72+
ast.KindPercentEqualsToken, // %=
73+
ast.KindAsteriskAsteriskEqualsToken, // **=
74+
ast.KindLessThanLessThanEqualsToken, // <<=
75+
ast.KindGreaterThanGreaterThanEqualsToken, // >>=
76+
ast.KindGreaterThanGreaterThanGreaterThanEqualsToken, // >>>=
77+
ast.KindAmpersandEqualsToken, // &=
78+
ast.KindBarEqualsToken, // |=
79+
ast.KindCaretEqualsToken, // ^=
80+
ast.KindQuestionQuestionEqualsToken, // ??=
81+
ast.KindAmpersandAmpersandEqualsToken, // &&=
82+
ast.KindBarBarEqualsToken: // ||=
83+
return true
84+
}
85+
86+
case ast.KindPrefixUnaryExpression:
87+
// Check for ++ and -- prefix operators
88+
prefix := parent.AsPrefixUnaryExpression()
89+
if prefix == nil {
90+
return false
91+
}
92+
93+
switch prefix.Operator {
94+
case ast.KindPlusPlusToken, // ++
95+
ast.KindMinusMinusToken: // --
96+
return prefix.Operand == node
97+
}
98+
99+
case ast.KindPostfixUnaryExpression:
100+
// Check for ++ and -- postfix operators
101+
postfix := parent.AsPostfixUnaryExpression()
102+
if postfix == nil {
103+
return false
104+
}
105+
106+
switch postfix.Operator {
107+
case ast.KindPlusPlusToken, // ++
108+
ast.KindMinusMinusToken: // --
109+
return postfix.Operand == node
110+
}
111+
112+
case ast.KindObjectBindingPattern:
113+
// In destructuring like {x} = obj, x is a write reference
114+
return isBindingPatternInAssignment(parent)
115+
116+
case ast.KindArrayBindingPattern:
117+
// In array destructuring like [x] = arr, x is a write reference
118+
return isBindingPatternInAssignment(parent)
119+
120+
case ast.KindBindingElement:
121+
// Check if the binding element is part of a write context
122+
return isWriteReference(parent)
123+
124+
case ast.KindShorthandPropertyAssignment:
125+
// In destructuring like {x} = obj or ({x} = obj), x is a write reference
126+
// Check if the parent shorthand property is in a destructuring assignment
127+
return isInDestructuringAssignment(parent)
128+
129+
case ast.KindPropertyAssignment:
130+
// In destructuring like {b: x} = obj, x is a write reference
131+
propAssignment := parent.AsPropertyAssignment()
132+
if propAssignment != nil && propAssignment.Initializer == node {
133+
return isInDestructuringAssignment(parent)
134+
}
135+
136+
case ast.KindObjectLiteralExpression:
137+
// In object destructuring like {x} = obj, x is a write reference
138+
return isInDestructuringAssignment(parent)
139+
140+
case ast.KindArrayLiteralExpression:
141+
// In array destructuring like [x] = arr, x is a write reference
142+
return isInDestructuringAssignment(parent)
143+
144+
case ast.KindParenthesizedExpression:
145+
// Unwrap parentheses and check the parent context
146+
return isWriteReference(parent)
147+
148+
case ast.KindAsExpression, ast.KindTypeAssertionExpression:
149+
// Type assertions like (x as any) = 0
150+
// The type assertion wraps the identifier, check if the assertion is a write target
151+
return isWriteReference(parent)
152+
}
153+
154+
return false
155+
}
156+
157+
// isBindingPatternInAssignment checks if a binding pattern is the left side of an assignment
158+
func isBindingPatternInAssignment(node *ast.Node) bool {
159+
if node == nil {
160+
return false
161+
}
162+
163+
// The binding pattern's parent might be wrapped in parentheses
164+
parent := node.Parent
165+
166+
// Unwrap parentheses
167+
for parent != nil && parent.Kind == ast.KindParenthesizedExpression {
168+
parent = parent.Parent
169+
}
170+
171+
// Check if the parent is a binary expression with = operator
172+
if parent != nil && parent.Kind == ast.KindBinaryExpression {
173+
binary := parent.AsBinaryExpression()
174+
if binary != nil && binary.OperatorToken != nil && binary.OperatorToken.Kind == ast.KindEqualsToken {
175+
// Check if the binding pattern is on the left side
176+
leftNode := binary.Left
177+
// Unwrap parentheses on the left side
178+
for leftNode != nil && leftNode.Kind == ast.KindParenthesizedExpression {
179+
parenExpr := leftNode.AsParenthesizedExpression()
180+
if parenExpr != nil {
181+
leftNode = parenExpr.Expression
182+
} else {
183+
break
184+
}
185+
}
186+
return leftNode == node
187+
}
188+
}
189+
190+
return false
191+
}
192+
193+
// isInDestructuringAssignment checks if a node is part of a destructuring assignment pattern
194+
func isInDestructuringAssignment(node *ast.Node) bool {
195+
current := node
196+
for current != nil {
197+
if current.Kind == ast.KindObjectLiteralExpression || current.Kind == ast.KindArrayLiteralExpression {
198+
// Check if this literal is the left side of an assignment
199+
// May be wrapped in parentheses
200+
parent := current.Parent
201+
202+
// Unwrap parentheses
203+
for parent != nil && parent.Kind == ast.KindParenthesizedExpression {
204+
parent = parent.Parent
205+
}
206+
207+
if parent != nil && parent.Kind == ast.KindBinaryExpression {
208+
binary := parent.AsBinaryExpression()
209+
if binary != nil && binary.OperatorToken != nil && binary.OperatorToken.Kind == ast.KindEqualsToken {
210+
// Check if the literal (or its parent parenthesized expression) is on the left
211+
leftNode := binary.Left
212+
// Unwrap parentheses on left side
213+
for leftNode != nil && leftNode.Kind == ast.KindParenthesizedExpression {
214+
parenExpr := leftNode.AsParenthesizedExpression()
215+
if parenExpr != nil {
216+
leftNode = parenExpr.Expression
217+
} else {
218+
break
219+
}
220+
}
221+
if leftNode == current {
222+
return true
223+
}
224+
}
225+
}
226+
return false
227+
}
228+
current = current.Parent
229+
}
230+
return false
231+
}
232+
233+
// checkIdentifierWrite checks if an identifier is a write reference to a const variable
234+
func checkIdentifierWrite(node *ast.Node, ctx *rule.RuleContext, constSymbols map[*ast.Symbol]bool) {
235+
// Check if this is a write reference (assignment, increment, etc.)
236+
if !isWriteReference(node) {
237+
return
238+
}
239+
240+
// Get the symbol for this identifier
241+
if ctx.TypeChecker == nil {
242+
return
243+
}
244+
245+
symbol := ctx.TypeChecker.GetSymbolAtLocation(node)
246+
if symbol == nil {
247+
return
248+
}
249+
250+
// Check if this symbol refers to a const variable
251+
if !constSymbols[symbol] {
252+
return
253+
}
254+
255+
// Report the violation
256+
identName := getIdentifierName(node)
257+
ctx.ReportNode(node, buildConstMessage(identName))
258+
}
259+
260+
// NoConstAssignRule disallows reassigning const variables
261+
var NoConstAssignRule = rule.CreateRule(rule.Rule{
262+
Name: "no-const-assign",
263+
Run: func(ctx rule.RuleContext, options any) rule.RuleListeners {
264+
// Track const declarations by their symbol
265+
constSymbols := make(map[*ast.Symbol]bool)
266+
267+
return rule.RuleListeners{
268+
// Track const variable declarations
269+
ast.KindVariableDeclarationList: func(node *ast.Node) {
270+
if !isConstBinding(node) {
271+
return
272+
}
273+
274+
varDeclList := node.AsVariableDeclarationList()
275+
if varDeclList == nil || varDeclList.Declarations == nil {
276+
return
277+
}
278+
279+
// Track all identifiers declared as const using their symbols
280+
for _, decl := range varDeclList.Declarations.Nodes {
281+
if decl.Kind != ast.KindVariableDeclaration {
282+
continue
283+
}
284+
285+
varDecl := decl.AsVariableDeclaration()
286+
if varDecl == nil || varDecl.Name() == nil {
287+
continue
288+
}
289+
290+
// Collect symbols for all identifiers in the binding name
291+
collectSymbols(varDecl.Name(), &ctx, constSymbols)
292+
}
293+
},
294+
295+
// Check for reassignments to const variables
296+
ast.KindIdentifier: func(node *ast.Node) {
297+
checkIdentifierWrite(node, &ctx, constSymbols)
298+
},
299+
300+
// Check shorthand property assignments in destructuring (e.g., {x} = obj)
301+
ast.KindShorthandPropertyAssignment: func(node *ast.Node) {
302+
shorthand := node.AsShorthandPropertyAssignment()
303+
if shorthand == nil || shorthand.Name() == nil {
304+
return
305+
}
306+
307+
// Check if this shorthand is in a destructuring assignment
308+
if !isInDestructuringAssignment(node) {
309+
return
310+
}
311+
312+
// This is a write reference, check if it refers to a const variable
313+
if ctx.TypeChecker == nil {
314+
return
315+
}
316+
317+
symbol := ctx.TypeChecker.GetSymbolAtLocation(shorthand.Name())
318+
if symbol == nil {
319+
return
320+
}
321+
322+
// Check if this symbol refers to a const variable
323+
if !constSymbols[symbol] {
324+
return
325+
}
326+
327+
// Report the violation
328+
identName := getIdentifierName(shorthand.Name())
329+
ctx.ReportNode(shorthand.Name(), buildConstMessage(identName))
330+
},
331+
}
332+
},
333+
})
334+
335+
// collectSymbols recursively collects symbols for all identifiers from a binding pattern
336+
func collectSymbols(bindingName *ast.Node, ctx *rule.RuleContext, constSymbols map[*ast.Symbol]bool) {
337+
if bindingName == nil || ctx.TypeChecker == nil {
338+
return
339+
}
340+
341+
switch bindingName.Kind {
342+
case ast.KindIdentifier:
343+
symbol := ctx.TypeChecker.GetSymbolAtLocation(bindingName)
344+
if symbol != nil {
345+
constSymbols[symbol] = true
346+
}
347+
348+
case ast.KindObjectBindingPattern:
349+
// Walk through child nodes to find binding elements
350+
bindingName.ForEachChild(func(child *ast.Node) bool {
351+
if child.Kind == ast.KindBindingElement {
352+
bindingElem := child.AsBindingElement()
353+
if bindingElem != nil && bindingElem.Name() != nil {
354+
collectSymbols(bindingElem.Name(), ctx, constSymbols)
355+
}
356+
}
357+
return false
358+
})
359+
360+
case ast.KindArrayBindingPattern:
361+
// Walk through child nodes to find binding elements
362+
bindingName.ForEachChild(func(child *ast.Node) bool {
363+
if child.Kind == ast.KindBindingElement {
364+
bindingElem := child.AsBindingElement()
365+
if bindingElem != nil && bindingElem.Name() != nil {
366+
collectSymbols(bindingElem.Name(), ctx, constSymbols)
367+
}
368+
}
369+
return false
370+
})
371+
}
372+
}

0 commit comments

Comments
 (0)