-
-
Notifications
You must be signed in to change notification settings - Fork 31.4k
/
Copy pathprefer-optional-chaining.js
93 lines (80 loc) Β· 2.89 KB
/
prefer-optional-chaining.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
'use strict';
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Prefer optional chaining',
category: 'suggestion',
},
fixable: 'code',
schema: [],
},
create(context) {
const sourceCode = context.getSourceCode();
// Helper function: Checks if two nodes have identical tokens
function equalTokens(left, right) {
const leftTokens = sourceCode.getTokens(left);
const rightTokens = sourceCode.getTokens(right);
return (
leftTokens.length === rightTokens.length &&
leftTokens.every((tokenL, i) => tokenL.type === rightTokens[i].type && tokenL.value === rightTokens[i].value)
);
}
// Check if a sequence of two nodes forms a valid member expression chain
function isValidMemberExpressionPair(left, right) {
return (
right.type === 'MemberExpression' &&
equalTokens(left, right.object)
);
}
// Generate the optional chaining expression
function generateOptionalChaining(ops, first, last) {
return ops.slice(first, last + 1).reduce((chain, node, i) => {
const property = node.computed ?
`[${sourceCode.getText(node.property)}]` :
sourceCode.getText(node.property);
return i === 0 ? sourceCode.getText(node) : `${chain}?.${property}`;
}, '');
}
return {
'LogicalExpression[operator=&&]:exit'(node) {
// Early return if part of a larger `&&` chain
if (node.parent.type === 'LogicalExpression' && node.parent.operator === '&&') {
return;
}
const ops = [];
let current = node;
// Collect `&&` expressions into the ops array
while (current.type === 'LogicalExpression' && current.operator === '&&') {
ops.unshift(current.right); // Add right operand
current = current.left;
}
ops.unshift(current); // Add the leftmost operand
// Find the first valid member expression sequence
let first = 0;
while (first < ops.length - 1 && !isValidMemberExpressionPair(ops[first], ops[first + 1])) {
first++;
}
// No valid sequence found
if (first === ops.length - 1) return;
context.report({
node,
message: 'Prefer optional chaining.',
fix(fixer) {
// Find the last valid member expression sequence
let last = first;
while (last < ops.length - 1 && isValidMemberExpressionPair(ops[last], ops[last + 1])) {
last++;
}
return fixer.replaceTextRange(
[ops[first].range[0], ops[last].range[1]],
generateOptionalChaining(ops, first, last),
);
},
});
},
};
},
};