Skip to content

Commit

Permalink
feat: switch to new scope-based jest fn call parser to support `@jest…
Browse files Browse the repository at this point in the history
…/globals` (#20)

* feat: switch to new scope-based jest fn call parser to support `@jest/globals`

* chore: remove unneeded utils

* test: add some extra cases for coverage

* ci: only collect coverage on runs using ESLint v8+

* chore: add back test project
  • Loading branch information
G-Rath authored Aug 20, 2022
1 parent 1208b32 commit 35ddfed
Show file tree
Hide file tree
Showing 17 changed files with 1,996 additions and 653 deletions.
10 changes: 6 additions & 4 deletions .github/workflows/nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@ name: Unit tests & Release
on:
push:
branches:
- master
- main
- next
pull_request:
branches:
- master
- main
- next

Expand Down Expand Up @@ -87,10 +85,12 @@ jobs:
yarn
yarn add --dev eslint@${{ matrix.eslint-version }}
- name: run tests
run: yarn test --coverage
# only collect coverage on eslint versions that support dynamic import
run: yarn test --coverage ${{ matrix.eslint-version >= 8 }}
env:
CI: true
- uses: codecov/codecov-action@v3
if: ${{ matrix.eslint-version >= 8 }}
test-os:
name: Test on ${{ matrix.os }} using Node.js LTS
needs: prepare-yarn-cache
Expand All @@ -109,10 +109,12 @@ jobs:
- name: install
run: yarn
- name: run tests
run: yarn test --coverage
# only collect coverage on eslint versions that support dynamic import
run: yarn test --coverage ${{ matrix.eslint-version >= 8 }}
env:
CI: true
- uses: codecov/codecov-action@v3
if: ${{ matrix.eslint-version >= 8 }}

docs:
if: ${{ github.event_name == 'pull_request' }}
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -112,16 +112,19 @@
"@babel/preset-typescript": "^7.3.3",
"@commitlint/cli": "^16.0.0",
"@commitlint/config-conventional": "^16.0.0",
"@schemastore/package": "^0.0.6",
"@semantic-release/changelog": "^6.0.0",
"@semantic-release/git": "^10.0.0",
"@tsconfig/node12": "^1.0.11",
"@types/dedent": "^0.7.0",
"@types/jest": "^28.0.0",
"@types/node": "^16.0.0",
"@types/prettier": "^2.7.0",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"babel-jest": "^28.0.0",
"babel-plugin-replace-ts-export-assignment": "^0.0.2",
"dedent": "^0.7.0",
"eslint": "^6.0.0 || ^7.0.0 || ^8.0.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-eslint-comments": "^3.1.2",
Expand Down
2 changes: 2 additions & 0 deletions src/rules/__tests__/prefer-to-be-array.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ const createTestsForEqualityMatchers = (): Array<

ruleTester.run('prefer-to-be-array', rule, {
valid: [
'expect.hasAssertions',
'expect.hasAssertions()',
'expect',
'expect()',
'expect().toBe(true)',
Expand Down
1 change: 1 addition & 0 deletions src/rules/__tests__/prefer-to-be-object.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const createTestsForEqualityMatchers = (): Array<
ruleTester.run('prefer-to-be-object', rule, {
valid: [
'expect.hasAssertions',
'expect.hasAssertions()',
'expect',
'expect().not',
'expect().toBe',
Expand Down
52 changes: 29 additions & 23 deletions src/rules/prefer-to-be-array.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils';
import {
ModifierName,
createRule,
followTypeAssertionChain,
getAccessorValue,
isBooleanEqualityMatcher,
isExpectCall,
isInstanceOfBinaryExpression,
isParsedInstanceOfMatcherCall,
isSupportedAccessor,
parseExpectCall,
parseJestFnCall,
} from './utils';

const isArrayIsArrayCall = (
Expand Down Expand Up @@ -42,25 +41,21 @@ export default createRule<Options, MessageIds>({
create(context) {
return {
CallExpression(node) {
if (!isExpectCall(node)) {
return;
}

const { expect, modifier, matcher } = parseExpectCall(node);
const jestFnCall = parseJestFnCall(node, context);

if (!matcher) {
if (jestFnCall?.type !== 'expect') {
return;
}

if (isParsedInstanceOfMatcherCall(matcher, 'Array')) {
if (isParsedInstanceOfMatcherCall(jestFnCall, 'Array')) {
context.report({
node: matcher.node.property,
node: jestFnCall.matcher,
messageId: 'preferToBeArray',
fix: fixer => [
fixer.replaceTextRange(
[
matcher.node.property.range[0],
matcher.node.property.range[1] + '(Array)'.length,
jestFnCall.matcher.range[0],
jestFnCall.matcher.range[1] + '(Array)'.length,
],
'toBeArray()',
),
Expand All @@ -70,11 +65,17 @@ export default createRule<Options, MessageIds>({
return;
}

const { parent: expect } = jestFnCall.head.node;

if (expect?.type !== AST_NODE_TYPES.CallExpression) {
return;
}

const [expectArg] = expect.arguments;

if (
!expectArg ||
!isBooleanEqualityMatcher(matcher) ||
!isBooleanEqualityMatcher(jestFnCall) ||
!(
isArrayIsArrayCall(expectArg) ||
isInstanceOfBinaryExpression(expectArg, 'Array')
Expand All @@ -84,11 +85,11 @@ export default createRule<Options, MessageIds>({
}

context.report({
node: matcher.node.property,
node: jestFnCall.matcher,
messageId: 'preferToBeArray',
fix(fixer) {
const fixes = [
fixer.replaceText(matcher.node.property, 'toBeArray'),
fixer.replaceText(jestFnCall.matcher, 'toBeArray'),
expectArg.type === AST_NODE_TYPES.CallExpression
? fixer.remove(expectArg.callee)
: fixer.removeRange([
Expand All @@ -97,10 +98,11 @@ export default createRule<Options, MessageIds>({
]),
];

let invertCondition = matcher.name === 'toBeFalse';
let invertCondition =
getAccessorValue(jestFnCall.matcher) === 'toBeFalse';

if (matcher.arguments?.length) {
const [matcherArg] = matcher.arguments;
if (jestFnCall.args.length) {
const [matcherArg] = jestFnCall.args;

fixes.push(fixer.remove(matcherArg));

Expand All @@ -111,13 +113,17 @@ export default createRule<Options, MessageIds>({
}

if (invertCondition) {
const notModifier = jestFnCall.modifiers.find(
nod => getAccessorValue(nod) === 'not',
);

fixes.push(
modifier && modifier.name === ModifierName.not
notModifier
? fixer.removeRange([
modifier.node.property.range[0] - 1,
modifier.node.property.range[1],
notModifier.range[0] - 1,
notModifier.range[1],
])
: fixer.insertTextBefore(matcher.node.property, 'not.'),
: fixer.insertTextBefore(jestFnCall.matcher, 'not.'),
);
}

Expand Down
43 changes: 15 additions & 28 deletions src/rules/prefer-to-be-false.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils';
import {
MaybeTypeCast,
ParsedEqualityMatcherCall,
ParsedExpectMatcher,
EqualityMatcher,
createRule,
followTypeAssertionChain,
isExpectCall,
isParsedEqualityMatcherCall,
parseExpectCall,
getAccessorValue,
getFirstMatcherArg,
parseJestFnCall,
} from './utils';

interface FalseLiteral extends TSESTree.BooleanLiteral {
Expand All @@ -17,20 +14,6 @@ interface FalseLiteral extends TSESTree.BooleanLiteral {
const isFalseLiteral = (node: TSESTree.Node): node is FalseLiteral =>
node.type === AST_NODE_TYPES.Literal && node.value === false;

/**
* Checks if the given `ParsedExpectMatcher` is a call to one of the equality matchers,
* with a `false` literal as the sole argument.
*
* @param {ParsedExpectMatcher} matcher
*
* @return {matcher is ParsedEqualityMatcherCall<MaybeTypeCast<FalseLiteral>>}
*/
const isFalseEqualityMatcher = (
matcher: ParsedExpectMatcher,
): matcher is ParsedEqualityMatcherCall<MaybeTypeCast<FalseLiteral>> =>
isParsedEqualityMatcherCall(matcher) &&
isFalseLiteral(followTypeAssertionChain(matcher.arguments[0]));

export default createRule({
name: __filename,
meta: {
Expand All @@ -50,19 +33,23 @@ export default createRule({
create(context) {
return {
CallExpression(node) {
if (!isExpectCall(node)) {
const jestFnCall = parseJestFnCall(node, context);

if (jestFnCall?.type !== 'expect') {
return;
}

const { matcher } = parseExpectCall(node);

if (matcher && isFalseEqualityMatcher(matcher)) {
if (
jestFnCall.args.length === 1 &&
isFalseLiteral(getFirstMatcherArg(jestFnCall)) &&
EqualityMatcher.hasOwnProperty(getAccessorValue(jestFnCall.matcher))
) {
context.report({
node: matcher.node.property,
node: jestFnCall.matcher,
messageId: 'preferToBeFalse',
fix: fixer => [
fixer.replaceText(matcher.node.property, 'toBeFalse'),
fixer.remove(matcher.arguments[0]),
fixer.replaceText(jestFnCall.matcher, 'toBeFalse'),
fixer.remove(jestFnCall.args[0]),
],
});
}
Expand Down
52 changes: 29 additions & 23 deletions src/rules/prefer-to-be-object.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
import {
ModifierName,
createRule,
followTypeAssertionChain,
getAccessorValue,
isBooleanEqualityMatcher,
isExpectCall,
isInstanceOfBinaryExpression,
isParsedInstanceOfMatcherCall,
parseExpectCall,
parseJestFnCall,
} from './utils';

export type MessageIds = 'preferToBeObject';
Expand All @@ -33,25 +32,21 @@ export default createRule<Options, MessageIds>({
create(context) {
return {
CallExpression(node) {
if (!isExpectCall(node)) {
return;
}

const { expect, modifier, matcher } = parseExpectCall(node);
const jestFnCall = parseJestFnCall(node, context);

if (!matcher) {
if (jestFnCall?.type !== 'expect') {
return;
}

if (isParsedInstanceOfMatcherCall(matcher, 'Object')) {
if (isParsedInstanceOfMatcherCall(jestFnCall, 'Object')) {
context.report({
node: matcher.node.property,
node: jestFnCall.matcher,
messageId: 'preferToBeObject',
fix: fixer => [
fixer.replaceTextRange(
[
matcher.node.property.range[0],
matcher.node.property.range[1] + '(Object)'.length,
jestFnCall.matcher.range[0],
jestFnCall.matcher.range[1] + '(Object)'.length,
],
'toBeObject()',
),
Expand All @@ -61,29 +56,36 @@ export default createRule<Options, MessageIds>({
return;
}

const { parent: expect } = jestFnCall.head.node;

if (expect?.type !== AST_NODE_TYPES.CallExpression) {
return;
}

const [expectArg] = expect.arguments;

if (
!expectArg ||
!isBooleanEqualityMatcher(matcher) ||
!isBooleanEqualityMatcher(jestFnCall) ||
!isInstanceOfBinaryExpression(expectArg, 'Object')
) {
return;
}

context.report({
node: matcher.node.property,
node: jestFnCall.matcher,
messageId: 'preferToBeObject',
fix(fixer) {
const fixes = [
fixer.replaceText(matcher.node.property, 'toBeObject'),
fixer.replaceText(jestFnCall.matcher, 'toBeObject'),
fixer.removeRange([expectArg.left.range[1], expectArg.range[1]]),
];

let invertCondition = matcher.name === 'toBeFalse';
let invertCondition =
getAccessorValue(jestFnCall.matcher) === 'toBeFalse';

if (matcher.arguments?.length) {
const [matcherArg] = matcher.arguments;
if (jestFnCall.args?.length) {
const [matcherArg] = jestFnCall.args;

fixes.push(fixer.remove(matcherArg));

Expand All @@ -94,13 +96,17 @@ export default createRule<Options, MessageIds>({
}

if (invertCondition) {
const notModifier = jestFnCall.modifiers.find(
nod => getAccessorValue(nod) === 'not',
);

fixes.push(
modifier && modifier.name === ModifierName.not
notModifier
? fixer.removeRange([
modifier.node.property.range[0] - 1,
modifier.node.property.range[1],
notModifier.range[0] - 1,
notModifier.range[1],
])
: fixer.insertTextBefore(matcher.node.property, 'not.'),
: fixer.insertTextBefore(jestFnCall.matcher, 'not.'),
);
}

Expand Down
Loading

0 comments on commit 35ddfed

Please sign in to comment.