UNPKG

14.7 kBJavaScriptView Raw
1"use strict";
2var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3 if (k2 === undefined) k2 = k;
4 Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
5}) : (function(o, m, k, k2) {
6 if (k2 === undefined) k2 = k;
7 o[k2] = m[k];
8}));
9var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
10 Object.defineProperty(o, "default", { enumerable: true, value: v });
11}) : function(o, v) {
12 o["default"] = v;
13});
14var __importStar = (this && this.__importStar) || function (mod) {
15 if (mod && mod.__esModule) return mod;
16 var result = {};
17 if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
18 __setModuleDefault(result, mod);
19 return result;
20};
21Object.defineProperty(exports, "__esModule", { value: true });
22const utils_1 = require("@typescript-eslint/utils");
23const util = __importStar(require("../util"));
24/*
25The AST is always constructed such the first element is always the deepest element.
26
27I.e. for this code: `foo && foo.bar && foo.bar.baz && foo.bar.baz.buzz`
28The AST will look like this:
29{
30 left: {
31 left: {
32 left: foo
33 right: foo.bar
34 }
35 right: foo.bar.baz
36 }
37 right: foo.bar.baz.buzz
38}
39*/
40exports.default = util.createRule({
41 name: 'prefer-optional-chain',
42 meta: {
43 type: 'suggestion',
44 docs: {
45 description: 'Prefer using concise optional chain expressions instead of chained logical ands',
46 recommended: false,
47 suggestion: true,
48 },
49 hasSuggestions: true,
50 messages: {
51 preferOptionalChain: "Prefer using an optional chain expression instead, as it's more concise and easier to read.",
52 optionalChainSuggest: 'Change to an optional chain.',
53 },
54 schema: [],
55 },
56 defaultOptions: [],
57 create(context) {
58 const sourceCode = context.getSourceCode();
59 return {
60 [[
61 'LogicalExpression[operator="&&"] > Identifier',
62 'LogicalExpression[operator="&&"] > MemberExpression',
63 'LogicalExpression[operator="&&"] > ChainExpression > MemberExpression',
64 'LogicalExpression[operator="&&"] > BinaryExpression[operator="!=="]',
65 'LogicalExpression[operator="&&"] > BinaryExpression[operator="!="]',
66 ].join(',')](initialIdentifierOrNotEqualsExpr) {
67 var _a;
68 // selector guarantees this cast
69 const initialExpression = (((_a = initialIdentifierOrNotEqualsExpr.parent) === null || _a === void 0 ? void 0 : _a.type) ===
70 utils_1.AST_NODE_TYPES.ChainExpression
71 ? initialIdentifierOrNotEqualsExpr.parent.parent
72 : initialIdentifierOrNotEqualsExpr.parent);
73 if (initialExpression.left !== initialIdentifierOrNotEqualsExpr) {
74 // the node(identifier or member expression) is not the deepest left node
75 return;
76 }
77 if (!isValidChainTarget(initialIdentifierOrNotEqualsExpr, true)) {
78 return;
79 }
80 // walk up the tree to figure out how many logical expressions we can include
81 let previous = initialExpression;
82 let current = initialExpression;
83 let previousLeftText = getText(initialIdentifierOrNotEqualsExpr);
84 let optionallyChainedCode = previousLeftText;
85 let expressionCount = 1;
86 while (current.type === utils_1.AST_NODE_TYPES.LogicalExpression) {
87 if (!isValidChainTarget(current.right,
88 // only allow identifiers for the first chain - foo && foo()
89 expressionCount === 1)) {
90 break;
91 }
92 const leftText = previousLeftText;
93 const rightText = getText(current.right);
94 // can't just use startsWith because of cases like foo && fooBar.baz;
95 const matchRegex = new RegExp(`^${
96 // escape regex characters
97 leftText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[^a-zA-Z0-9_$]`);
98 if (!matchRegex.test(rightText) &&
99 // handle redundant cases like foo.bar && foo.bar
100 leftText !== rightText) {
101 break;
102 }
103 // omit weird doubled up expression that make no sense like foo.bar && foo.bar
104 if (rightText !== leftText) {
105 expressionCount += 1;
106 previousLeftText = rightText;
107 /*
108 Diff the left and right text to construct the fix string
109 There are the following cases:
110
111 1)
112 rightText === 'foo.bar.baz.buzz'
113 leftText === 'foo.bar.baz'
114 diff === '.buzz'
115
116 2)
117 rightText === 'foo.bar.baz.buzz()'
118 leftText === 'foo.bar.baz'
119 diff === '.buzz()'
120
121 3)
122 rightText === 'foo.bar.baz.buzz()'
123 leftText === 'foo.bar.baz.buzz'
124 diff === '()'
125
126 4)
127 rightText === 'foo.bar.baz[buzz]'
128 leftText === 'foo.bar.baz'
129 diff === '[buzz]'
130
131 5)
132 rightText === 'foo.bar.baz?.buzz'
133 leftText === 'foo.bar.baz'
134 diff === '?.buzz'
135 */
136 const diff = rightText.replace(leftText, '');
137 if (diff.startsWith('?')) {
138 // item was "pre optional chained"
139 optionallyChainedCode += diff;
140 }
141 else {
142 const needsDot = diff.startsWith('(') || diff.startsWith('[');
143 optionallyChainedCode += `?${needsDot ? '.' : ''}${diff}`;
144 }
145 }
146 previous = current;
147 current = util.nullThrows(current.parent, util.NullThrowsReasons.MissingParent);
148 }
149 if (expressionCount > 1) {
150 if (previous.right.type === utils_1.AST_NODE_TYPES.BinaryExpression) {
151 // case like foo && foo.bar !== someValue
152 optionallyChainedCode += ` ${previous.right.operator} ${sourceCode.getText(previous.right.right)}`;
153 }
154 context.report({
155 node: previous,
156 messageId: 'preferOptionalChain',
157 suggest: [
158 {
159 messageId: 'optionalChainSuggest',
160 fix: (fixer) => [
161 fixer.replaceText(previous, optionallyChainedCode),
162 ],
163 },
164 ],
165 });
166 }
167 },
168 };
169 function getText(node) {
170 if (node.type === utils_1.AST_NODE_TYPES.BinaryExpression) {
171 return getText(
172 // isValidChainTarget ensures this is type safe
173 node.left);
174 }
175 if (node.type === utils_1.AST_NODE_TYPES.CallExpression) {
176 const calleeText = getText(
177 // isValidChainTarget ensures this is type safe
178 node.callee);
179 // ensure that the call arguments are left untouched, or else we can break cases that _need_ whitespace:
180 // - JSX: <Foo Needs Space Between Attrs />
181 // - Unary Operators: typeof foo, await bar, delete baz
182 const closingParenToken = util.nullThrows(sourceCode.getLastToken(node), util.NullThrowsReasons.MissingToken('closing parenthesis', node.type));
183 const openingParenToken = util.nullThrows(sourceCode.getFirstTokenBetween(node.callee, closingParenToken, util.isOpeningParenToken), util.NullThrowsReasons.MissingToken('opening parenthesis', node.type));
184 const argumentsText = sourceCode.text.substring(openingParenToken.range[0], closingParenToken.range[1]);
185 return `${calleeText}${argumentsText}`;
186 }
187 if (node.type === utils_1.AST_NODE_TYPES.Identifier) {
188 return node.name;
189 }
190 if (node.type === utils_1.AST_NODE_TYPES.ThisExpression) {
191 return 'this';
192 }
193 if (node.type === utils_1.AST_NODE_TYPES.ChainExpression) {
194 /* istanbul ignore if */ if (node.expression.type === utils_1.AST_NODE_TYPES.TSNonNullExpression) {
195 // this shouldn't happen
196 return '';
197 }
198 return getText(node.expression);
199 }
200 return getMemberExpressionText(node);
201 }
202 /**
203 * Gets a normalized representation of the given MemberExpression
204 */
205 function getMemberExpressionText(node) {
206 let objectText;
207 // cases should match the list in ALLOWED_MEMBER_OBJECT_TYPES
208 switch (node.object.type) {
209 case utils_1.AST_NODE_TYPES.CallExpression:
210 case utils_1.AST_NODE_TYPES.Identifier:
211 objectText = getText(node.object);
212 break;
213 case utils_1.AST_NODE_TYPES.MemberExpression:
214 objectText = getMemberExpressionText(node.object);
215 break;
216 case utils_1.AST_NODE_TYPES.ThisExpression:
217 objectText = getText(node.object);
218 break;
219 /* istanbul ignore next */
220 default:
221 throw new Error(`Unexpected member object type: ${node.object.type}`);
222 }
223 let propertyText;
224 if (node.computed) {
225 // cases should match the list in ALLOWED_COMPUTED_PROP_TYPES
226 switch (node.property.type) {
227 case utils_1.AST_NODE_TYPES.Identifier:
228 propertyText = getText(node.property);
229 break;
230 case utils_1.AST_NODE_TYPES.Literal:
231 case utils_1.AST_NODE_TYPES.TemplateLiteral:
232 propertyText = sourceCode.getText(node.property);
233 break;
234 case utils_1.AST_NODE_TYPES.MemberExpression:
235 propertyText = getMemberExpressionText(node.property);
236 break;
237 /* istanbul ignore next */
238 default:
239 throw new Error(`Unexpected member property type: ${node.object.type}`);
240 }
241 return `${objectText}${node.optional ? '?.' : ''}[${propertyText}]`;
242 }
243 else {
244 // cases should match the list in ALLOWED_NON_COMPUTED_PROP_TYPES
245 switch (node.property.type) {
246 case utils_1.AST_NODE_TYPES.Identifier:
247 propertyText = getText(node.property);
248 break;
249 /* istanbul ignore next */
250 default:
251 throw new Error(`Unexpected member property type: ${node.object.type}`);
252 }
253 return `${objectText}${node.optional ? '?.' : '.'}${propertyText}`;
254 }
255 }
256 },
257});
258const ALLOWED_MEMBER_OBJECT_TYPES = new Set([
259 utils_1.AST_NODE_TYPES.CallExpression,
260 utils_1.AST_NODE_TYPES.Identifier,
261 utils_1.AST_NODE_TYPES.MemberExpression,
262 utils_1.AST_NODE_TYPES.ThisExpression,
263]);
264const ALLOWED_COMPUTED_PROP_TYPES = new Set([
265 utils_1.AST_NODE_TYPES.Identifier,
266 utils_1.AST_NODE_TYPES.Literal,
267 utils_1.AST_NODE_TYPES.MemberExpression,
268 utils_1.AST_NODE_TYPES.TemplateLiteral,
269]);
270const ALLOWED_NON_COMPUTED_PROP_TYPES = new Set([
271 utils_1.AST_NODE_TYPES.Identifier,
272]);
273function isValidChainTarget(node, allowIdentifier) {
274 if (node.type === utils_1.AST_NODE_TYPES.ChainExpression) {
275 return isValidChainTarget(node.expression, allowIdentifier);
276 }
277 if (node.type === utils_1.AST_NODE_TYPES.MemberExpression) {
278 const isObjectValid = ALLOWED_MEMBER_OBJECT_TYPES.has(node.object.type) &&
279 // make sure to validate the expression is of our expected structure
280 isValidChainTarget(node.object, true);
281 const isPropertyValid = node.computed
282 ? ALLOWED_COMPUTED_PROP_TYPES.has(node.property.type) &&
283 // make sure to validate the member expression is of our expected structure
284 (node.property.type === utils_1.AST_NODE_TYPES.MemberExpression
285 ? isValidChainTarget(node.property, allowIdentifier)
286 : true)
287 : ALLOWED_NON_COMPUTED_PROP_TYPES.has(node.property.type);
288 return isObjectValid && isPropertyValid;
289 }
290 if (node.type === utils_1.AST_NODE_TYPES.CallExpression) {
291 return isValidChainTarget(node.callee, allowIdentifier);
292 }
293 if (allowIdentifier &&
294 (node.type === utils_1.AST_NODE_TYPES.Identifier ||
295 node.type === utils_1.AST_NODE_TYPES.ThisExpression)) {
296 return true;
297 }
298 /*
299 special case for the following, where we only want the left
300 - foo !== null
301 - foo != null
302 - foo !== undefined
303 - foo != undefined
304 */
305 if (node.type === utils_1.AST_NODE_TYPES.BinaryExpression &&
306 ['!==', '!='].includes(node.operator) &&
307 isValidChainTarget(node.left, allowIdentifier)) {
308 if (node.right.type === utils_1.AST_NODE_TYPES.Identifier &&
309 node.right.name === 'undefined') {
310 return true;
311 }
312 if (node.right.type === utils_1.AST_NODE_TYPES.Literal &&
313 node.right.value === null) {
314 return true;
315 }
316 }
317 return false;
318}
319//# sourceMappingURL=prefer-optional-chain.js.map
\No newline at end of file