Skip to content

Commit 1234e06

Browse files
authored
Improved allowTestedProperty option to allow more tested properties (#257)
1 parent 55963af commit 1234e06

File tree

3 files changed

+90
-29
lines changed

3 files changed

+90
-29
lines changed

lib/util/type-checker/property-guards.js

Lines changed: 59 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const TS_NODE_TYPES = [
2424
* @typedef {import("estree").ReturnStatement} ReturnStatement
2525
* @typedef {import("estree").ContinueStatement} ContinueStatement
2626
* @typedef {import("estree").BreakStatement} BreakStatement
27+
* @typedef {import("estree").IfStatement} IfStatement
2728
* @typedef {import("estree").Node} Node
2829
* @typedef {import("eslint").SourceCode} SourceCode
2930
* @typedef {import("eslint").Rule.RuleContext} RuleContext
@@ -50,7 +51,7 @@ module.exports = { createPropertyGuardsContext }
5051
/**
5152
* @typedef {object} GuardChecker
5253
* @property {(node: MemberExpression|Property)=>boolean} test
53-
* @property {"instanceof"|"definedValue"|"definedType"|"hasValue"|"unknown"} kind
54+
* @property {"instanceof"|"definedValue"|"definedType"|"hasValue"|"optional"|"unknown"} kind
5455
*/
5556
/**
5657
* @typedef {object} MaybeGuard
@@ -251,6 +252,15 @@ function createPropertyGuardsContext(options) {
251252
}
252253
return null
253254
}
255+
if (
256+
((parent.type === "CallExpression" && parent.callee === node) ||
257+
(parent.type === "MemberExpression" &&
258+
parent.object === node)) &&
259+
parent.optional
260+
) {
261+
// e.g. x.property?.()
262+
return { test: (n) => n === node, kind: "optional" }
263+
}
254264

255265
if (
256266
propertyTypes.every(
@@ -289,36 +299,54 @@ function createPropertyGuardsContext(options) {
289299
return getGuardCheckerForExpression(parent, { not: !not })
290300
}
291301
if (parent.type === "IfStatement" && parent.test === node) {
292-
if (!not) {
293-
const block = parent.consequent
294-
return (n) =>
295-
block.range[0] <= n.range[0] && n.range[1] <= block.range[1]
296-
}
297-
// e.g. if (typeof x.property === 'undefined')
298-
if (parent.alternate) {
299-
const block = parent.alternate
300-
return (n) =>
301-
block.range[0] <= n.range[0] && n.range[1] <= block.range[1]
302-
}
303-
if (!hasJumpStatementInAllPath(parent.consequent)) {
304-
return null
305-
}
306-
/** @type {Node|null} */
307-
const pp = getParent(parent)
308-
if (
309-
!pp ||
310-
(pp.type !== "BlockStatement" && pp.type !== "Program")
311-
) {
312-
return null
313-
}
314-
const start = parent.range[1]
315-
const end = pp.range[1]
316-
317-
return (n) => start <= n.range[0] && n.range[1] <= end
302+
return getGuardCheckerForIfStatement(parent, { not })
303+
}
304+
if (
305+
!not &&
306+
parent.type === "LogicalExpression" &&
307+
parent.operator === "&&" &&
308+
parent.left === node
309+
) {
310+
// e.g. typeof x.property !== 'undefined' && x.property
311+
const block = parent.right
312+
return (n) =>
313+
block.range[0] <= n.range[0] && n.range[1] <= block.range[1]
318314
}
319315
return null
320316
}
321317

318+
/**
319+
* @param {IfStatement} node
320+
* @returns {((node: MemberExpression|Property)=>boolean)|null} The guard tester.
321+
*/
322+
function getGuardCheckerForIfStatement(node, { not = false } = {}) {
323+
if (!not) {
324+
const block = node.consequent
325+
return (n) =>
326+
block.range[0] <= n.range[0] && n.range[1] <= block.range[1]
327+
}
328+
if (node.alternate) {
329+
const block = node.alternate
330+
return (n) =>
331+
block.range[0] <= n.range[0] && n.range[1] <= block.range[1]
332+
}
333+
if (!hasJumpStatementInAllPath(node.consequent)) {
334+
return null
335+
}
336+
/** @type {Node|null} */
337+
const parent = getParent(node)
338+
if (
339+
!parent ||
340+
(parent.type !== "BlockStatement" && parent.type !== "Program")
341+
) {
342+
return null
343+
}
344+
const start = node.range[1]
345+
const end = parent.range[1]
346+
347+
return (n) => start <= n.range[0] && n.range[1] <= end
348+
}
349+
322350
/**
323351
* @param {MemberExpression|Property} node
324352
* @returns {GuardChecker|null} The guard checker.
@@ -421,7 +449,10 @@ function createPropertyGuardsContext(options) {
421449
const guard = {
422450
...params,
423451
prototypeGuard: isPrototypePropertyAccess(params.node),
424-
used: false,
452+
used:
453+
// A optional chain allows the property access expression of its own.
454+
// but we mark expressions that are candidates for guarding as used here because we won't check them later.
455+
checker.kind === "optional",
425456
isAvailableLocation: checker.test,
426457
}
427458
let classGuards = maybeGuards.get(params.className)

tests/lib/rules/no-array-from.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,18 @@ new RuleTester().run("no-array-from", rule, {
2424
code: "if (Array.from) { const {from} = Array }",
2525
options: [{ allowTestedProperty: true }],
2626
},
27+
{
28+
code: "typeof Array.from !== 'undefined' && Array.from",
29+
options: [{ allowTestedProperty: true }],
30+
},
31+
{
32+
code: "Array.from && Array.from(it)",
33+
options: [{ allowTestedProperty: true }],
34+
},
35+
{
36+
code: "Array.from?.(it)",
37+
options: [{ allowTestedProperty: true }],
38+
},
2739
],
2840
invalid: [
2941
{
@@ -47,5 +59,10 @@ new RuleTester().run("no-array-from", rule, {
4759
options: [{ allowTestedProperty: true }],
4860
errors: ["ES2015 'Array.from' method is forbidden."],
4961
},
62+
{
63+
code: "Array?.from(it)",
64+
options: [{ allowTestedProperty: true }],
65+
errors: ["ES2015 'Array.from' method is forbidden."],
66+
},
5067
],
5168
})

tests/lib/rules/no-number-epsilon.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,24 @@ const RuleTester = require("../../tester")
88
const rule = require("../../../lib/rules/no-number-epsilon.js")
99

1010
new RuleTester().run("no-number-epsilon", rule, {
11-
valid: ["Number", "Number.xyz", "let Number = 0; Number.EPSILON"],
11+
valid: [
12+
"Number",
13+
"Number.xyz",
14+
"let Number = 0; Number.EPSILON",
15+
{
16+
code: "Number.EPSILON?.toString()",
17+
options: [{ allowTestedProperty: true }],
18+
},
19+
],
1220
invalid: [
1321
{
1422
code: "Number.EPSILON",
1523
errors: ["ES2015 'Number.EPSILON' property is forbidden."],
1624
},
25+
{
26+
code: "Number.EPSILON.toString?.()",
27+
options: [{ allowTestedProperty: true }],
28+
errors: ["ES2015 'Number.EPSILON' property is forbidden."],
29+
},
1730
],
1831
})

0 commit comments

Comments
 (0)