Skip to content

Commit 34d7f08

Browse files
committed
implement is % deepEqual checks to prefer-t-regex
1 parent d9f9a49 commit 34d7f08

File tree

3 files changed

+146
-55
lines changed

3 files changed

+146
-55
lines changed

docs/rules/prefer-t-regex.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Translations: [Français](https://github.com/avajs/ava-docs/blob/master/fr_FR/re
44

55
The AVA [`t.regex()` assertion](https://github.com/avajs/ava/blob/master/docs/03-assertions.md#regexcontents-regex-message) can test a string against a regular expression.
66

7-
This rule will enforce the use of `t.regex()` instead of manually using `RegExp#test()`, which will make your code look clearer and produce better failure output.
7+
This rule will enforce the use of `t.regex()` instead of manually using `RegExp#test()`, which will make your code look clearer and produce better failure output. The rule will also prevent equality assertions with a regex and a non-regex, which is almost always an error.
88

99
This rule is fixable. It will replace the use of `RegExp#test()`, `String#match()`, or `String#search()` with `t.regex()`.
1010

@@ -27,6 +27,14 @@ test('main', t => {
2727
});
2828
```
2929

30+
```js
31+
const test = require('ava');
32+
33+
test('main', t => {
34+
t.is('foo', /\w+/);
35+
});
36+
```
37+
3038

3139
## Pass
3240

rules/prefer-t-regex.js

Lines changed: 116 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -13,75 +13,138 @@ const create = context => {
1313
'falsy'
1414
];
1515

16+
const equalityTests = ['is', 'deepEqual'];
17+
1618
const findReference = name => {
1719
const reference = context.getScope().references.find(reference => reference.identifier.name === name);
1820
const definitions = reference.resolved.defs;
21+
22+
// Many integration tests have identifiers that match zero definitions
23+
if (definitions.length === 0) {
24+
return undefined;
25+
}
26+
1927
return definitions[definitions.length - 1].node;
2028
};
2129

30+
const isRegExp = lookup => {
31+
let isRegex = lookup.regex;
32+
33+
// It's not a regexp but an identifier
34+
if (!isRegex && lookup.type === 'Identifier') {
35+
const reference = findReference(lookup.name);
36+
37+
// Not all possible references have an init field§
38+
if (reference && reference.init) {
39+
isRegex = reference.init.regex;
40+
}
41+
}
42+
43+
return isRegex;
44+
};
45+
46+
const booleanHandler = node => {
47+
const firstArg = node.arguments[0];
48+
49+
// First argument is a call expression
50+
const isFunctionCall = firstArg.type === 'CallExpression';
51+
if (!isFunctionCall || !firstArg.callee.property) {
52+
return;
53+
}
54+
55+
const {name} = firstArg.callee.property;
56+
let lookup = {};
57+
let variable = {};
58+
59+
if (name === 'test') {
60+
// `lookup.test(variable)`
61+
lookup = firstArg.callee.object;
62+
variable = firstArg.arguments[0];
63+
} else if (['search', 'match'].includes(name)) {
64+
// `variable.match(lookup)`
65+
lookup = firstArg.arguments[0];
66+
variable = firstArg.callee.object;
67+
}
68+
69+
if (!isRegExp(lookup)) {
70+
return;
71+
}
72+
73+
const assertion = ['true', 'truthy'].includes(node.callee.property.name) ? 'regex' : 'notRegex';
74+
75+
const fix = fixer => {
76+
const source = context.getSourceCode();
77+
return [
78+
fixer.replaceText(node.callee.property, assertion),
79+
fixer.replaceText(firstArg, `${source.getText(variable)}, ${source.getText(lookup)}`)
80+
];
81+
};
82+
83+
context.report({
84+
node,
85+
message: `Prefer using the \`t.${assertion}()\` assertion.`,
86+
fix
87+
});
88+
};
89+
90+
const equalityHandler = node => {
91+
const firstArg = node.arguments[0];
92+
const secondArg = node.arguments[1];
93+
94+
if (!firstArg || !secondArg) {
95+
return;
96+
}
97+
98+
const firstIsRx = isRegExp(firstArg);
99+
const secondIsRx = isRegExp(secondArg);
100+
101+
// If both are regex, or neither are, the expression is ok
102+
if (firstIsRx === secondIsRx) {
103+
return;
104+
}
105+
106+
const matchee = secondIsRx ? firstArg : secondArg;
107+
const regex = secondIsRx ? secondArg : firstArg;
108+
109+
const assertion = 'regex';
110+
111+
const fix = fixer => {
112+
const source = context.getSourceCode();
113+
return [
114+
fixer.replaceText(node.callee.property, 'regex'),
115+
fixer.replaceText(firstArg, `${source.getText(matchee)}`),
116+
fixer.replaceText(secondArg, `${source.getText(regex)}`)
117+
];
118+
};
119+
120+
context.report({
121+
node,
122+
message: `Prefer using the \`t.${assertion}()\` assertion.`,
123+
fix
124+
});
125+
};
126+
22127
return ava.merge({
23128
CallExpression: visitIf([
24129
ava.isInTestFile,
25130
ava.isInTestNode
26131
])(node => {
27-
// Call a boolean assertion, for example, `t.true`, `t.false`, …
28-
const isBooleanAssertion = node.callee.type === 'MemberExpression' &&
29-
booleanTests.includes(node.callee.property.name) &&
132+
const isAssertion = node.callee.type === 'MemberExpression' &&
30133
util.getNameOfRootNodeObject(node.callee) === 't';
31134

32-
if (!isBooleanAssertion) {
33-
return;
34-
}
35-
36-
const firstArg = node.arguments[0];
37-
38-
// First argument is a call expression
39-
const isFunctionCall = firstArg.type === 'CallExpression';
40-
if (!isFunctionCall || !firstArg.callee.property) {
41-
return;
42-
}
43-
44-
const {name} = firstArg.callee.property;
45-
let lookup = {};
46-
let variable = {};
47-
48-
if (name === 'test') {
49-
// `lookup.test(variable)`
50-
lookup = firstArg.callee.object;
51-
variable = firstArg.arguments[0];
52-
} else if (['search', 'match'].includes(name)) {
53-
// `variable.match(lookup)`
54-
lookup = firstArg.arguments[0];
55-
variable = firstArg.callee.object;
56-
}
57-
58-
let isRegExp = lookup.regex;
135+
// Call a boolean assertion, for example, `t.true`, `t.false`, …
136+
const isBooleanAssertion = isAssertion &&
137+
booleanTests.includes(node.callee.property.name);
59138

60-
// It's not a regexp but an identifier
61-
if (!isRegExp && lookup.type === 'Identifier') {
62-
const reference = findReference(lookup.name);
63-
isRegExp = reference.init.regex;
64-
}
139+
// Call an equality assertion, ie. 't.is', 't.deepEqual'
140+
const isEqualityAssertion = isAssertion &&
141+
equalityTests.includes(node.callee.property.name);
65142

66-
if (!isRegExp) {
67-
return;
143+
if (isBooleanAssertion) {
144+
booleanHandler(node);
145+
} else if (isEqualityAssertion) {
146+
equalityHandler(node);
68147
}
69-
70-
const assertion = ['true', 'truthy'].includes(node.callee.property.name) ? 'regex' : 'notRegex';
71-
72-
const fix = fixer => {
73-
const source = context.getSourceCode();
74-
return [
75-
fixer.replaceText(node.callee.property, assertion),
76-
fixer.replaceText(firstArg, `${source.getText(variable)}, ${source.getText(lookup)}`)
77-
];
78-
};
79-
80-
context.report({
81-
node,
82-
message: `Prefer using the \`t.${assertion}()\` assertion.`,
83-
fix
84-
});
85148
})
86149
});
87150
};

test/prefer-t-regex.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ ruleTester.run('prefer-t-regex', rule, {
1818
valid: [
1919
header + 'test(t => t.regex("foo", /\\d+/));',
2020
header + 'test(t => t.regex(foo(), /\\d+/));',
21-
header + 'test(t => t.is(/\\d+/.test("foo")), true);',
21+
header + 'test(t => t.is(/\\d+/.test("foo"), true));',
2222
header + 'test(t => t.true(1 === 1));',
2323
header + 'test(t => t.true(foo.bar()));',
2424
header + 'const a = /\\d+/;\ntest(t => t.truthy(a));',
@@ -62,6 +62,26 @@ ruleTester.run('prefer-t-regex', rule, {
6262
code: header + 'const reg = /\\d+/;\ntest(t => t.true(reg.test(foo.bar())));',
6363
output: header + 'const reg = /\\d+/;\ntest(t => t.regex(foo.bar(), reg));',
6464
errors: errors('regex')
65+
},
66+
{
67+
code: header + 'test(t => t.is(foo(), /\\d+/));',
68+
output: header + 'test(t => t.regex(foo(), /\\d+/));',
69+
errors: errors('regex')
70+
},
71+
{
72+
code: header + 'test(t => t.is(/\\d+/, foo()));',
73+
output: header + 'test(t => t.regex(foo(), /\\d+/));',
74+
errors: errors('regex')
75+
},
76+
{
77+
code: header + 'test(t => t.deepEqual(foo(), /\\d+/));',
78+
output: header + 'test(t => t.regex(foo(), /\\d+/));',
79+
errors: errors('regex')
80+
},
81+
{
82+
code: header + 'test(t => t.deepEqual(/\\d+/, foo()));',
83+
output: header + 'test(t => t.regex(foo(), /\\d+/));',
84+
errors: errors('regex')
6585
}
6686
]
6787
});

0 commit comments

Comments
 (0)