Skip to content

Commit c1ed28f

Browse files
committed
feat(rules): add prefer-const-assertions rule 🍰
- Add PreferConstAssertions rule implementation with tests and documentation - Add createConstAssertionFix utility function for const assertion fixes - Add hasConstAssertion and isTopLevelExpression validator utilities - Register prefer-const-assertions rule in main plugin index - Remove redundant comments from existing rule files
1 parent ec9cfc3 commit c1ed28f

13 files changed

Lines changed: 613 additions & 8 deletions
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
# Expected Behavior: `prefer-const-assertions` Rule
2+
3+
This rule enforces the use of const assertions (`as const`) on array and object literals for better type inference, but only when they are used in top-level contexts like variable declarations, return statements, and assignments.
4+
5+
## ❌ Invalid Examples
6+
7+
```typescript
8+
// Array literals without const assertion
9+
const colors = ['red', 'green', 'blue']
10+
const numbers = [1, 2, 3, 4, 5]
11+
const flags = [true, false, true]
12+
13+
// Object literals without const assertion
14+
const config = { apiUrl: 'https://api.example.com', timeout: 5000 }
15+
const user = { name: 'John', age: 30, active: true }
16+
const settings = { debug: false, version: '1.0.0' }
17+
18+
// Mixed type arrays without const assertion
19+
const mixed = [1, 'hello', true, { id: 1 }]
20+
const data = ['string', 42, null, undefined]
21+
22+
// Nested structures without const assertion
23+
const nested = {
24+
user: { name: 'John', age: 30 },
25+
settings: { theme: 'dark', language: 'en' }
26+
}
27+
28+
// Arrays with objects without const assertion
29+
const users = [
30+
{ id: 1, name: 'Alice' },
31+
{ id: 2, name: 'Bob' }
32+
]
33+
34+
// Complex nested arrays without const assertion
35+
const matrix = [
36+
[1, 2, 3],
37+
[4, 5, 6],
38+
[7, 8, 9]
39+
]
40+
41+
// Object with arrays without const assertion
42+
const state = {
43+
items: ['item1', 'item2'],
44+
counts: [1, 2, 3],
45+
flags: [true, false]
46+
}
47+
48+
// In return statements without const assertion
49+
function getColors() {
50+
return ['red', 'green', 'blue']
51+
}
52+
53+
function getConfig() {
54+
return { apiUrl: 'https://api.example.com', timeout: 5000 }
55+
}
56+
57+
// In variable declarations with let/var without const assertion
58+
let status = ['pending', 'completed', 'failed']
59+
var options = { retries: 3, timeout: 1000 }
60+
61+
// In assignment expressions without const assertion
62+
let data
63+
data = { name: 'test', value: 42 }
64+
65+
// In function expressions without const assertion
66+
const getData = () => ({ id: 1, name: 'test' })
67+
const getItems = () => ['item1', 'item2', 'item3']
68+
```
69+
70+
## ✅ Valid Examples
71+
72+
```typescript
73+
// Array literals with const assertion
74+
const colors = ['red', 'green', 'blue'] as const
75+
const numbers = [1, 2, 3, 4, 5] as const
76+
const flags = [true, false, true] as const
77+
78+
// Object literals with const assertion
79+
const config = { apiUrl: 'https://api.example.com', timeout: 5000 } as const
80+
const user = { name: 'John', age: 30, active: true } as const
81+
const settings = { debug: false, version: '1.0.0' } as const
82+
83+
// Mixed type arrays with const assertion
84+
const mixed = [1, 'hello', true, { id: 1 }] as const
85+
const data = ['string', 42, null, undefined] as const
86+
87+
// Nested structures with const assertion
88+
const nested = {
89+
user: { name: 'John', age: 30 },
90+
settings: { theme: 'dark', language: 'en' }
91+
} as const
92+
93+
// Arrays with objects with const assertion
94+
const users = [
95+
{ id: 1, name: 'Alice' },
96+
{ id: 2, name: 'Bob' }
97+
] as const
98+
99+
// Complex nested arrays with const assertion
100+
const matrix = [
101+
[1, 2, 3],
102+
[4, 5, 6],
103+
[7, 8, 9]
104+
] as const
105+
106+
// Object with arrays with const assertion
107+
const state = {
108+
items: ['item1', 'item2'],
109+
counts: [1, 2, 3],
110+
flags: [true, false]
111+
} as const
112+
113+
// In return statements with const assertion
114+
function getColors() {
115+
return ['red', 'green', 'blue'] as const
116+
}
117+
118+
function getConfig() {
119+
return { apiUrl: 'https://api.example.com', timeout: 5000 } as const
120+
}
121+
122+
// In variable declarations with let/var with const assertion
123+
let status = ['pending', 'completed', 'failed'] as const
124+
var options = { retries: 3, timeout: 1000 } as const
125+
126+
// In assignment expressions with const assertion
127+
let data
128+
data = { name: 'test', value: 42 } as const
129+
130+
// In function expressions with const assertion
131+
const getData = () => ({ id: 1, name: 'test' } as const)
132+
const getItems = () => ['item1', 'item2', 'item3'] as const
133+
134+
// Empty arrays and objects (not affected by this rule)
135+
const emptyArray = []
136+
const emptyObject = {}
137+
138+
// Arrays and objects in function parameters (not affected by this rule)
139+
function processData(items = [1, 2, 3]) {
140+
return items.map(x => x * 2)
141+
}
142+
143+
function createUser(user = { name: 'Guest' }) {
144+
return user
145+
}
146+
147+
// Arrays and objects in arrow function parameters (not affected by this rule)
148+
const processItems = (items = ['a', 'b', 'c']) => items.length
149+
const createConfig = (config = { debug: false }) => config
150+
151+
// Already using const assertion (not affected by this rule)
152+
const colors = ['red', 'green', 'blue'] as const
153+
const config = { apiUrl: 'https://api.example.com' } as const
154+
155+
// Arrays and objects in nested contexts (not affected by this rule)
156+
const complex = {
157+
data: [1, 2, 3], // This won't trigger the rule
158+
nested: {
159+
items: ['a', 'b'] // This won't trigger the rule
160+
}
161+
}
162+
```
163+
164+
## Rule Scope
165+
166+
This rule **only applies to**:
167+
168+
- ✅ Array literals (`[]`) in top-level contexts:
169+
170+
- Variable declarations (`const arr = [...]`)
171+
- Return statements (`return [...]`)
172+
- Assignment expressions (`arr = [...]`)
173+
- Function expressions (`() => [...]`)
174+
175+
- ✅ Object literals (`{}`) in top-level contexts:
176+
- Variable declarations (`const obj = {...}`)
177+
- Return statements (`return {...}`)
178+
- Assignment expressions (`obj = {...}`)
179+
- Function expressions (`() => ({...})`)
180+
181+
This rule **does NOT apply to**:
182+
183+
- ❌ Empty arrays (`[]`) or objects (`{}`)
184+
- ❌ Arrays/objects in function parameters (`function test(arr = [...])`)
185+
- ❌ Arrays/objects in arrow function parameters (`const test = (arr = [...]) => ...`)
186+
- ❌ Arrays/objects in nested contexts (inside other arrays/objects)
187+
- ❌ Arrays/objects that already have const assertion (`[...] as const`)
188+
189+
## Why This Rule Matters
190+
191+
Const assertions provide better type inference by making TypeScript treat the literal as a readonly tuple or readonly object with exact literal types:
192+
193+
```typescript
194+
// Without const assertion - TypeScript infers general types
195+
const colors = ['red', 'green', 'blue']
196+
// Type: string[]
197+
198+
// With const assertion - TypeScript infers exact literal types
199+
const colors = ['red', 'green', 'blue'] as const
200+
// Type: readonly ['red', 'green', 'blue']
201+
```
202+
203+
This enables:
204+
205+
- **Exact literal types** instead of general types
206+
- **Readonly properties** preventing accidental mutations
207+
- **Better IntelliSense** with exact autocomplete options
208+
- **Type-safe operations** with precise type checking
209+
210+
## Auto-fix Behavior
211+
212+
The rule provides auto-fix suggestions that add `as const` to array and object literals:
213+
214+
- `[1, 2, 3]``[1, 2, 3] as const`
215+
- `{ name: 'test' }``{ name: 'test' } as const`
216+
- `['a', 'b', 'c']``['a', 'b', 'c'] as const`
217+
- `{ id: 1, active: true }``{ id: 1, active: true } as const`

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { LintPlugin } from '@interfaces/index.ts'
22
import { asyncFunctionNamingRule } from '@rules/AsyncFunctionNaming.ts'
33
import { explicitParameterTypesRule } from '@rules/ExplicitParameterTypes.ts'
44
import { explicitReturnTypesRule } from '@rules/ExplicitReturnTypes.ts'
5+
import { preferConstAssertionsRule } from '@rules/PreferConstAssertions.ts'
56
import { preferNullishCoalescingRule } from '@rules/PreferNullishCoalescing.ts'
67
import { preferOptionalChainRule } from '@rules/PreferOptionalChain.ts'
78
import { preferPromiseRejectErrorsRule } from '@rules/PreferPromiseRejectErrors.ts'
@@ -16,6 +17,7 @@ const plugin: LintPlugin = {
1617
'async-function-naming': asyncFunctionNamingRule,
1718
'explicit-parameter-types': explicitParameterTypesRule,
1819
'explicit-return-types': explicitReturnTypesRule,
20+
'prefer-const-assertions': preferConstAssertionsRule,
1921
'prefer-nullish-coalescing': preferNullishCoalescingRule,
2022
'prefer-optional-chain': preferOptionalChainRule,
2123
'prefer-promise-reject-errors': preferPromiseRejectErrorsRule,

src/rules/AsyncFunctionNaming.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88

99
/**
1010
* Lint rule for enforcing async function naming conventions.
11-
* Refactored to use centralized utilities.
1211
*/
1312
export const asyncFunctionNamingRule = {
1413
/**

src/rules/ExplicitParameterTypes.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,6 @@ function getFixFunction(
6767

6868
/**
6969
* Lint rule for enforcing explicit parameter type annotations.
70-
* Refactored to use centralized utilities.
7170
*/
7271
export const explicitParameterTypesRule = {
7372
/**

src/rules/ExplicitReturnTypes.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88

99
/**
1010
* Lint rule for enforcing explicit return type annotations.
11-
* Refactored to use centralized utilities.
1211
*/
1312
export const explicitReturnTypesRule = {
1413
/**

src/rules/PreferConstAssertions.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import type { DenoASTNode, LintContext } from '@interfaces/index.ts'
2+
import {
3+
createConstAssertionFix,
4+
hasConstAssertion,
5+
isArrayExpression,
6+
isObjectExpression,
7+
isTopLevelExpression
8+
} from '@utils/index.ts'
9+
10+
/**
11+
* Lint rule for enforcing const assertions on array and object literals.
12+
*/
13+
export const preferConstAssertionsRule = {
14+
/**
15+
* Creates the lint rule implementation.
16+
* @param context - The Deno lint context for reporting issues and fixes
17+
* @returns Object containing visitor functions for AST node types
18+
*/
19+
create(context: LintContext): Record<string, (node: DenoASTNode) => void> {
20+
return {
21+
/**
22+
* Visitor function for array expressions.
23+
* @param node - The AST node representing an array expression
24+
*/
25+
ArrayExpression(node: DenoASTNode): void {
26+
if (!isArrayExpression(node)) {
27+
return
28+
}
29+
if (node.elements.length === 0) {
30+
return
31+
}
32+
if (hasConstAssertion(node)) {
33+
return
34+
}
35+
if (!isTopLevelExpression(node)) {
36+
return
37+
}
38+
context.report({
39+
node,
40+
message: 'Array literal should use const assertion for better type inference',
41+
fix: createConstAssertionFix(context, node)
42+
})
43+
},
44+
/**
45+
* Visitor function for object expressions.
46+
* @param node - The AST node representing an object expression
47+
*/
48+
ObjectExpression(node: DenoASTNode): void {
49+
if (!isObjectExpression(node)) {
50+
return
51+
}
52+
if (node.properties.length === 0) {
53+
return
54+
}
55+
if (hasConstAssertion(node)) {
56+
return
57+
}
58+
if (!isTopLevelExpression(node)) {
59+
return
60+
}
61+
context.report({
62+
node,
63+
message: 'Object literal should use const assertion for better type inference',
64+
fix: createConstAssertionFix(context, node)
65+
})
66+
}
67+
}
68+
}
69+
}

src/rules/PreferNullishCoalescing.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77

88
/**
99
* Lint rule for preferring nullish coalescing over logical OR.
10-
* Refactored to use centralized utilities.
1110
*/
1211
export const preferNullishCoalescingRule = {
1312
/**

src/rules/PreferOptionalChain.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88

99
/**
1010
* Lint rule for enforcing the use of optional chaining (?.) over logical AND (&&) for property access.
11-
* Refactored to use centralized utilities.
1211
*/
1312
export const preferOptionalChainRule = {
1413
/**

src/rules/PreferPromiseRejectErrors.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88

99
/**
1010
* Lint rule for preferring Error objects in Promise.reject() calls.
11-
* Refactored to use centralized utilities.
1211
*/
1312
export const preferPromiseRejectErrorsRule = {
1413
/**

src/rules/RequireErrorHandling.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { isAwaited, isCallExpression, isDenoApiCall, requiresErrorHandling } fro
33

44
/**
55
* Lint rule for enforcing error handling on Deno file operations.
6-
* Refactored to use centralized utilities.
76
*/
87
export const requireErrorHandlingRule = {
98
/**

0 commit comments

Comments
 (0)