Skip to content

Commit 63b88d7

Browse files
committed
feat(rules): add prefer-optional-chain rule ❓
- Add complete test coverage with 35 test cases - Add prefer-optional-chain lint rule implementation - Add detailed documentation and examples for the new rule - Add new AST node types for conditional and member expressions - Add new type guards for improved type safety - Reorganize type definitions and utilities alphabetically - Refactor existing code for better maintainability - Update module exports to include new rule
1 parent efcd1bf commit 63b88d7

8 files changed

Lines changed: 720 additions & 60 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Deno lint plugin collection for identifying and reporting on patterns found in c
88
- [`explicit-parameter-types`](./examples/explicit-parameter-types.md) - Requires explicit type annotations on parameters
99
- [`explicit-return-types`](./examples/explicit-return-types.md) - Requires explicit return type annotations
1010
- [`prefer-nullish-coalescing`](./examples/prefer-nullish-coalescing.md) - Prefers nullish coalescing (??) over logical OR (||) for null/undefined checks
11+
- [`prefer-optional-chain`](./examples/prefer-optional-chain.md) - Prefers optional chaining (?.) over logical AND (&&) for property access
1112
- [`require-error-handling`](./examples/require-error-handling.md) - Ensures Deno file operations are properly awaited
1213

1314
## Installation

examples/prefer-optional-chain.md

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
# Expected Behavior: `prefer-optional-chain` Rule
2+
3+
This rule enforces the use of optional chaining (`?.`) over logical AND (`&&`) for property access when the same object is being checked and accessed.
4+
5+
## ❌ Invalid Examples
6+
7+
```typescript
8+
// Logical AND with property access (same object)
9+
const name = user && user.name
10+
const email = user && user.profile && user.profile.email
11+
const count = data && data.items && data.items.length
12+
13+
// Logical AND with method calls (same object)
14+
const result = api && api.getData()
15+
const user = service && service.getUser(id)
16+
const processed = data && data.filter().map()
17+
18+
// Logical AND with computed property access (same object)
19+
const value = obj && obj[key]
20+
const item = array && array[index]
21+
const prop = config && config[setting]
22+
23+
// Complex nested access patterns
24+
const fullName = user && user.profile && user.profile.name
25+
const settings = config && config.user && config.user.preferences
26+
const data = response && response.data && response.data.results
27+
28+
// In function parameters and returns
29+
function getUserName(user) {
30+
return user && user.name
31+
}
32+
33+
const getUserEmail = user => user && user.profile && user.profile.email
34+
35+
// In object properties
36+
const userData = {
37+
name: user && user.name,
38+
email: user && user.profile && user.profile.email
39+
}
40+
41+
// In template literals
42+
const greeting = `Hello ${user && user.name}!`
43+
const status = `Count: ${data && data.items && data.items.length}`
44+
45+
// In class methods
46+
class UserService {
47+
getName(user) {
48+
return user && user.name
49+
}
50+
51+
getProfile(user) {
52+
return user && user.profile && user.profile.details
53+
}
54+
}
55+
56+
// In async functions
57+
async function fetchUserData(id) {
58+
const user = await api.getUser(id)
59+
return user && user.profile && user.profile.email
60+
}
61+
```
62+
63+
## ✅ Valid Examples
64+
65+
```typescript
66+
// Optional chaining with property access (correct)
67+
const name = user?.name
68+
const email = user?.profile?.email
69+
const count = data?.items?.length
70+
71+
// Optional chaining with method calls (correct)
72+
const result = api?.getData()
73+
const user = service?.getUser(id)
74+
const processed = data?.filter()?.map()
75+
76+
// Optional chaining with computed property access (correct)
77+
const value = obj?.[key]
78+
const item = array?.[index]
79+
const prop = config?.[setting]
80+
81+
// Complex nested access patterns with optional chaining
82+
const fullName = user?.profile?.name
83+
const settings = config?.user?.preferences
84+
const data = response?.data?.results
85+
86+
// In function parameters and returns
87+
function getUserName(user) {
88+
return user?.name
89+
}
90+
91+
const getUserEmail = user => user?.profile?.email
92+
93+
// In object properties
94+
const userData = {
95+
name: user?.name,
96+
email: user?.profile?.email
97+
}
98+
99+
// In template literals
100+
const greeting = `Hello ${user?.name}!`
101+
const status = `Count: ${data?.items?.length}`
102+
103+
// In class methods
104+
class UserService {
105+
getName(user) {
106+
return user?.name
107+
}
108+
109+
getProfile(user) {
110+
return user?.profile?.details
111+
}
112+
}
113+
114+
// In async functions
115+
async function fetchUserData(id) {
116+
const user = await api.getUser(id)
117+
return user?.profile?.email
118+
}
119+
120+
// Logical AND with different objects (not affected by this rule)
121+
const result = user && profile.name
122+
const data = api && otherService.getData()
123+
124+
// Logical AND with literals (not affected by this rule)
125+
const flag = user && 'active'
126+
const message = data && 'loaded'
127+
128+
// Logical AND with function calls (not affected by this rule)
129+
const result = user && getName()
130+
const data = api && processData()
131+
132+
// Already using optional chaining (not affected by this rule)
133+
const name = user?.name
134+
const email = user?.profile?.email
135+
136+
// Logical OR (not affected by this rule)
137+
const name = user || user.name
138+
const email = user || user.profile?.email
139+
```
140+
141+
## Rule Scope
142+
143+
This rule **only applies to**:
144+
145+
- ✅ Logical AND (`&&`) operators where the same object is being checked and accessed:
146+
- `object && object.property``object?.property`
147+
- `object && object.method()``object?.method()`
148+
- `object && object[key]``object?.[key]`
149+
150+
This rule **does NOT apply to**:
151+
152+
- ❌ Logical AND (`&&`) with different objects (`user && profile.name`)
153+
- ❌ Logical AND (`&&`) with literals (`user && 'active'`)
154+
- ❌ Logical AND (`&&`) with function calls (`user && getName()`)
155+
- ❌ Logical OR (`||`) operators
156+
- ❌ Already using optional chaining (`user?.name`)
157+
- ❌ Other logical operators (`!`, `??`)
158+
159+
## Why This Rule Matters
160+
161+
The logical AND (`&&`) pattern for property access is verbose and error-prone:
162+
163+
```typescript
164+
// ❌ Verbose and repetitive
165+
const name = user && user.name
166+
const email = user && user.profile && user.profile.email
167+
const count = data && data.items && data.items.length
168+
```
169+
170+
Optional chaining (`?.`) is more concise and readable:
171+
172+
```typescript
173+
// ✅ Concise and clear
174+
const name = user?.name
175+
const email = user?.profile?.email
176+
const count = data?.items?.length
177+
```
178+
179+
## Auto-fix Behavior
180+
181+
The rule provides auto-fix suggestions that replace `&&` with `?.`:
182+
183+
- `user && user.name``user?.name`
184+
- `user && user.getName()``user?.getName()`
185+
- `obj && obj[key]``obj?.[key]`
186+
- `api && api.getData(id)``api?.getData(id)`
187+
- `user && user.profile && user.profile.email``user?.profile?.email`

src/mod.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { asyncFunctionNamingRule } from '@rules/async-function-naming.ts'
33
import { explicitParameterTypesRule } from '@rules/explicit-parameter-types.ts'
44
import { explicitReturnTypesRule } from '@rules/explicit-return-types.ts'
55
import { preferNullishCoalescingRule } from '@rules/prefer-nullish-coalescing.ts'
6+
import { preferOptionalChainRule } from '@rules/prefer-optional-chain.ts'
67
import { requireErrorHandlingRule } from '@rules/require-error-handling.ts'
78

89
/**
@@ -15,6 +16,7 @@ const plugin: LintPlugin = {
1516
'explicit-parameter-types': explicitParameterTypesRule,
1617
'explicit-return-types': explicitReturnTypesRule,
1718
'prefer-nullish-coalescing': preferNullishCoalescingRule,
19+
'prefer-optional-chain': preferOptionalChainRule,
1820
'require-error-handling': requireErrorHandlingRule
1921
}
2022
}

src/rules/prefer-nullish-coalescing.ts

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,6 @@
11
import type { ASTNode, LintContext, LintFixer, LogicalExpressionNode } from '@app/types.ts'
22
import { isLiteral, isLogicalExpression } from '@shared/expression.ts'
33

4-
/**
5-
* Checks if a logical expression should use nullish coalescing instead of logical OR.
6-
* @param node - The logical expression node to check
7-
* @returns True if the expression should use nullish coalescing, false otherwise
8-
*/
9-
function shouldUseNullishCoalescing(node: LogicalExpressionNode): boolean {
10-
if (node.operator !== '||') {
11-
return false
12-
}
13-
const rightSide = node.right
14-
if (isLiteral(rightSide)) {
15-
const value = rightSide.value
16-
if (value === '' || value === 0 || value === false) {
17-
return true
18-
}
19-
}
20-
return false
21-
}
22-
234
/**
245
* Lint rule for preferring nullish coalescing over logical OR.
256
*/
@@ -55,3 +36,22 @@ export const preferNullishCoalescingRule = {
5536
}
5637
}
5738
}
39+
40+
/**
41+
* Checks if a logical expression should use nullish coalescing instead of logical OR.
42+
* @param node - The logical expression node to check
43+
* @returns True if the expression should use nullish coalescing, false otherwise
44+
*/
45+
function shouldUseNullishCoalescing(node: LogicalExpressionNode): boolean {
46+
if (node.operator !== '||') {
47+
return false
48+
}
49+
const rightSide = node.right
50+
if (isLiteral(rightSide)) {
51+
const value = rightSide.value
52+
if (value === '' || value === 0 || value === false) {
53+
return true
54+
}
55+
}
56+
return false
57+
}

0 commit comments

Comments
 (0)