Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: ts-migration/valid-expect #333

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -1,7 +1,7 @@
import { RuleTester } from 'eslint';
import { TSESLint } from '@typescript-eslint/experimental-utils';
import rule from '../valid-expect';

const ruleTester = new RuleTester({
const ruleTester = new TSESLint.RuleTester({
parserOptions: {
ecmaVersion: 8,
},
Expand Down
76 changes: 75 additions & 1 deletion src/rules/tsUtils.ts
Expand Up @@ -59,7 +59,9 @@ interface JestExpectNamespaceMemberExpression
*
* @return {node is JestExpectCallExpression}
*/
const isExpectCall = (node: TSESTree.Node): node is JestExpectCallExpression =>
export const isExpectCall = (
node: TSESTree.Node,
): node is JestExpectCallExpression =>
node.type === AST_NODE_TYPES.CallExpression &&
isExpectIdentifier(node.callee);

Expand All @@ -75,6 +77,78 @@ export const isExpectCallWithParent = (
node.parent.type === AST_NODE_TYPES.MemberExpression &&
node.parent.property.type === AST_NODE_TYPES.Identifier;

export const methodName = (node: TSESTree.Node): string | undefined => {
if (
node.parent &&
node.parent.type === AST_NODE_TYPES.MemberExpression &&
node.parent.property &&
node.parent.property.type === AST_NODE_TYPES.Identifier
) {
return node.parent.property.name;
}
return;
};

interface JestExpectNotCall extends JestExpectCallExpression {
parent: JestExpectCallMemberExpression;
}
export const isExpectNotCall = (
node: TSESTree.Node,
): node is JestExpectNotCall =>
isExpectCall(node) &&
!!node.parent &&
!!node.parent.parent &&
node.parent.parent.type === 'MemberExpression' &&
methodName(node) === 'not';

interface JestExpectResolvesCall extends JestExpectCallExpression {
parent: JestExpectCallMemberExpression;
}
export const isExpectResolvesCall = (
node: TSESTree.Node,
): node is JestExpectResolvesCall =>
isExpectCall(node) &&
!!node.parent &&
!!node.parent.parent &&
node.parent.parent.type === 'MemberExpression' &&
methodName(node) === 'resolves';

interface JestExpectNotResolvesCall extends JestExpectCallExpression {
parent: JestExpectCallMemberExpression;
}
export const isExpectNotResolvesCall = (
node: TSESTree.Node,
): node is JestExpectNotResolvesCall =>
isExpectNotCall(node) &&
!!node.parent &&
!!node.parent.parent &&
node.parent.parent.type === 'MemberExpression' &&
methodName(node) === 'resolves';

interface JestExpectRejectsCall extends JestExpectCallExpression {
parent: JestExpectCallMemberExpression;
}
export const isExpectRejectsCall = (
node: TSESTree.Node,
): node is JestExpectRejectsCall =>
isExpectCall(node) &&
!!node.parent &&
!!node.parent.parent &&
node.parent.parent.type === 'MemberExpression' &&
methodName(node) === 'rejects';

interface JestExpectNotRejectsCall extends JestExpectCallExpression {
parent: JestExpectCallMemberExpression;
}
export const isExpectNotRejectsCall = (
node: TSESTree.Node,
): node is JestExpectNotRejectsCall =>
isExpectNotCall(node) &&
!!node.parent &&
!!node.parent.parent &&
node.parent.parent.type === 'MemberExpression' &&
methodName(node) === 'rejects';

export enum DescribeAlias {
'describe' = 'describe',
'fdescribe' = 'fdescribe',
Expand Down
96 changes: 63 additions & 33 deletions src/rules/valid-expect.js → src/rules/valid-expect.ts
Expand Up @@ -2,15 +2,19 @@
* This implementation is ported from from eslint-plugin-jasmine.
* MIT license, Tom Vincent.
*/
import {
AST_NODE_TYPES,
TSESTree,
} from '@typescript-eslint/experimental-utils';

import {
expectCase,
expectNotRejectsCase,
expectNotResolvesCase,
expectRejectsCase,
expectResolvesCase,
getDocsUrl,
} from './util';
createRule,
isExpectCall,
isExpectNotRejectsCall,
isExpectNotResolvesCall,
isExpectRejectsCall,
isExpectResolvesCall,
} from './tsUtils';

const expectProperties = ['not', 'resolves', 'rejects'];
const promiseArgumentTypes = ['CallExpression', 'ArrayExpression'];
Expand All @@ -22,7 +26,9 @@ const promiseArgumentTypes = ['CallExpression', 'ArrayExpression'];
*
* @Returns CallExpressionNode
*/
const getClosestParentCallExpressionNode = node => {
const getClosestParentCallExpressionNode = (
node: TSESTree.Node,
): TSESTree.Node | null => {
if (!node || !node.parent || !node.parent.parent) {
return null;
}
Expand All @@ -40,7 +46,9 @@ const getClosestParentCallExpressionNode = node => {
*
* @Returns CallExpressionNode
*/
const getPromiseCallExpressionNode = node => {
const getPromiseCallExpressionNode = (
node: TSESTree.Node,
): TSESTree.Node | null => {
if (
node &&
node.type === 'ArrayExpression' &&
Expand All @@ -54,6 +62,7 @@ const getPromiseCallExpressionNode = node => {
node.type === 'CallExpression' &&
node.callee &&
node.callee.type === 'MemberExpression' &&
node.callee.object.type === AST_NODE_TYPES.Identifier &&
node.callee.object.name === 'Promise' &&
node.parent
) {
Expand All @@ -63,22 +72,33 @@ const getPromiseCallExpressionNode = node => {
return null;
};

const checkIfValidReturn = (parentCallExpressionNode, allowReturn) => {
const validParentNodeTypes = ['ArrowFunctionExpression', 'AwaitExpression'];
const checkIfValidReturn = (
parentCallExpressionNode: TSESTree.Node,
allowReturn: boolean,
) => {
const validParentNodeTypes = [
AST_NODE_TYPES.ArrowFunctionExpression,
AST_NODE_TYPES.AwaitExpression,
];
if (allowReturn) {
validParentNodeTypes.push('ReturnStatement');
validParentNodeTypes.push(AST_NODE_TYPES.ReturnStatement);
}

return validParentNodeTypes.includes(parentCallExpressionNode.type);
};

const promiseArrayExceptionKey = ({ start, end }) =>
const promiseArrayExceptionKey = ({ start, end }: TSESTree.SourceLocation) =>
`${start.line}:${start.column}-${end.line}:${end.column}`;

export default {
export default createRule({
name: __filename,
meta: {
type: 'problem',
docs: {
url: getDocsUrl(__filename),
description:
'Ensure expect() is called with a single argument and there is an actual expectation made.',
category: 'Possible Errors',
recommended: 'error',
},
messages: {
multipleArgs: 'More than one argument was passed to expect().',
Expand All @@ -104,25 +124,30 @@ export default {
additionalProperties: false,
},
],
},
} as const,
defaultOptions: [
{
alwaysAwait: false,
},
],
create(context) {
// Context state
const arrayExceptions = {};
const arrayExceptions: { [key: string]: boolean } = {};

const pushPromiseArrayException = loc => {
const pushPromiseArrayException = (loc: TSESTree.SourceLocation) => {
const key = promiseArrayExceptionKey(loc);
arrayExceptions[key] = true;
};

const promiseArrayExceptionExists = loc => {
const promiseArrayExceptionExists = (loc: TSESTree.SourceLocation) => {
const key = promiseArrayExceptionKey(loc);
return !!arrayExceptions[key];
};

return {
CallExpression(node) {
// checking "expect()" arguments
if (expectCase(node)) {
if (isExpectCall(node)) {
if (node.arguments.length > 1) {
const secondArgumentLocStart = node.arguments[1].loc.start;
const lastArgumentLocEnd =
Expand Down Expand Up @@ -164,12 +189,12 @@ export default {
node.parent.parent
) {
let parentNode = node.parent;
let parentProperty = parentNode.property;
let parentProperty = parentNode.property as TSESTree.Identifier;
let propertyName = parentProperty.name;
let grandParent = parentNode.parent;

// a property is accessed, get the next node
if (grandParent.type === 'MemberExpression') {
if (grandParent && grandParent.type === 'MemberExpression') {
// a modifier is used, just get the next one
if (expectProperties.indexOf(propertyName) > -1) {
grandParent = grandParent.parent;
Expand All @@ -186,13 +211,13 @@ export default {
}

// this next one should be the matcher
parentNode = parentNode.parent;
parentProperty = parentNode.property;
parentNode = parentNode.parent as TSESTree.MemberExpression;
parentProperty = parentNode.property as TSESTree.Identifier;
propertyName = parentProperty.name;
}

// matcher was not called
if (grandParent.type === 'ExpressionStatement') {
if (grandParent && grandParent.type === 'ExpressionStatement') {
context.report({
// For some reason `endColumn` isn't set in tests if `loc` is not
// added
Expand All @@ -209,13 +234,13 @@ export default {
}

if (
expectResolvesCase(node) ||
expectRejectsCase(node) ||
expectNotResolvesCase(node) ||
expectNotRejectsCase(node)
isExpectResolvesCall(node) ||
isExpectRejectsCall(node) ||
isExpectNotResolvesCall(node) ||
isExpectNotRejectsCall(node)
) {
let parentNode = getClosestParentCallExpressionNode(node);
if (parentNode) {
if (parentNode && parentNode.parent) {
const { options } = context;
const allowReturn = !options[0] || !options[0].alwaysAwait;
const isParentArrayExpression =
Expand All @@ -236,7 +261,10 @@ export default {
}

if (
!checkIfValidReturn(parentNode.parent, allowReturn) &&
!checkIfValidReturn(
parentNode.parent as TSESTree.Node,
allowReturn,
) &&
!promiseArrayExceptionExists(parentNode.loc)
) {
context.report({
Expand All @@ -257,9 +285,11 @@ export default {
},

// nothing called on "expect()"
'CallExpression:exit'(node) {
'CallExpression:exit'(node: TSESTree.CallExpression) {
if (
node.callee.type === AST_NODE_TYPES.Identifier &&
node.callee.name === 'expect' &&
node.parent &&
node.parent.type === 'ExpressionStatement'
) {
context.report({
Expand All @@ -273,4 +303,4 @@ export default {
},
};
},
};
});