Skip to content

Commit

Permalink
chore(no-alias-methods): convert to typescript (#383)
Browse files Browse the repository at this point in the history
  • Loading branch information
G-Rath authored and SimenB committed Aug 12, 2019
1 parent 6a75f24 commit 9465e57
Show file tree
Hide file tree
Showing 3 changed files with 189 additions and 21 deletions.
@@ -1,7 +1,7 @@
import { RuleTester } from 'eslint';
import { TSESLint } from '@typescript-eslint/experimental-utils';
import rule from '../no-alias-methods';

const ruleTester = new RuleTester();
const ruleTester = new TSESLint.RuleTester();

ruleTester.run('no-alias-methods', rule, {
valid: [
Expand Down
37 changes: 18 additions & 19 deletions src/rules/no-alias-methods.js → src/rules/no-alias-methods.ts
@@ -1,19 +1,25 @@
import { expectCaseWithParent, getDocsUrl, method } from './util';
import { createRule, isExpectCall, parseExpectCall } from './tsUtils';

export default {
export default createRule({
name: __filename,
meta: {
docs: {
url: getDocsUrl(__filename),
category: 'Best Practices',
description: 'Disallow alias methods',
recommended: 'warn',
},
messages: {
replaceAlias: `Replace {{ replace }}() with its canonical name of {{ canonical }}()`,
},
fixable: 'code',
type: 'suggestion',
schema: [],
},
defaultOptions: [],
create(context) {
// The Jest methods which have aliases. The canonical name is the first
// index of each item.
// todo: replace w/ Map
const methodNames = [
['toHaveBeenCalled', 'toBeCalled'],
['toHaveBeenCalledTimes', 'toBeCalledTimes'],
Expand All @@ -30,27 +36,18 @@ export default {

return {
CallExpression(node) {
if (!expectCaseWithParent(node)) {
if (!isExpectCall(node)) {
return;
}

let targetNode = method(node);
if (
targetNode.name === 'resolves' ||
targetNode.name === 'rejects' ||
targetNode.name === 'not'
) {
targetNode = method(node.parent);
}
const { matcher } = parseExpectCall(node);

if (!targetNode) {
if (!matcher) {
return;
}

// Check if the method used matches any of ours
const methodItem = methodNames.find(
item => item[1] === targetNode.name,
);
const methodItem = methodNames.find(item => item[1] === matcher.name);

if (methodItem) {
context.report({
Expand All @@ -59,11 +56,13 @@ export default {
replace: methodItem[1],
canonical: methodItem[0],
},
node: targetNode,
fix: fixer => [fixer.replaceText(targetNode, methodItem[0])],
node: matcher.node.property,
fix: fixer => [
fixer.replaceText(matcher.node.property, methodItem[0]),
],
});
}
},
};
},
};
});
169 changes: 169 additions & 0 deletions src/rules/tsUtils.ts
Expand Up @@ -320,6 +320,175 @@ export const isExpectCallWithParent = (
node.parent.type === AST_NODE_TYPES.MemberExpression &&
node.parent.property.type === AST_NODE_TYPES.Identifier;

interface ParsedExpectMember<
Name extends ExpectPropertyName = ExpectPropertyName,
Node extends ExpectMember<Name> = ExpectMember<Name>
> {
name: Name;
node: Node;
}

/**
* Represents a parsed expect matcher, such as `toBe`, `toContain`, and so on.
*/
export interface ParsedExpectMatcher<
Matcher extends MatcherName = MatcherName,
Node extends ExpectMember<Matcher> = ExpectMember<Matcher>
> extends ParsedExpectMember<Matcher, Node> {
arguments: TSESTree.CallExpression['arguments'] | null;
}

type BaseParsedModifier<
Modifier extends ModifierName = ModifierName
> = ParsedExpectMember<Modifier>;

type NegatableModifierName = ModifierName.rejects | ModifierName.resolves;
type NotNegatableModifierName = ModifierName.not;

/**
* Represents a parsed modifier that can be followed by a `not` negation modifier.
*/
interface NegatableParsedModifier<
Modifier extends NegatableModifierName = NegatableModifierName
> extends BaseParsedModifier<Modifier> {
negation?: ExpectMember<ModifierName.not>;
}

/**
* Represents a parsed modifier that cannot be followed by a `not` negation modifier.
*/
export interface NotNegatableParsedModifier<
Modifier extends NotNegatableModifierName = NotNegatableModifierName
> extends BaseParsedModifier<Modifier> {
negation?: never;
}

type ParsedExpectModifier =
| NotNegatableParsedModifier<NotNegatableModifierName>
| NegatableParsedModifier<NegatableModifierName>;

interface Expectation<ExpectNode extends ExpectCall = ExpectCall> {
expect: ExpectNode;
modifier?: ParsedExpectModifier;
matcher?: ParsedExpectMatcher;
}

const parseExpectMember = <S extends ExpectPropertyName>(
expectMember: ExpectMember<S>,
): ParsedExpectMember<S> => ({
name: getAccessorValue<S>(expectMember.property),
node: expectMember,
});

const reparseAsMatcher = (
parsedMember: ParsedExpectMember,
): ParsedExpectMatcher => ({
...parsedMember,
/**
* The arguments being passed to this `Matcher`, if any.
*
* If this matcher isn't called, this will be `null`.
*/
arguments:
/* istanbul ignore next */
parsedMember.node.parent &&
parsedMember.node.parent.type === AST_NODE_TYPES.CallExpression
? parsedMember.node.parent.arguments
: null,
});

/**
* Re-parses the given `parsedMember` as a `ParsedExpectModifier`.
*
* If the given `parsedMember` does not have a `name` of a valid `Modifier`,
* an exception will be thrown.
*
* @param {ParsedExpectMember<ModifierName>} parsedMember
*
* @return {ParsedExpectModifier}
*/
const reparseMemberAsModifier = (
parsedMember: ParsedExpectMember<ModifierName>,
): ParsedExpectModifier => {
if (isSpecificMember(parsedMember, ModifierName.not)) {
return parsedMember;
}

/* istanbul ignore if */
if (
!isSpecificMember(parsedMember, ModifierName.resolves) &&
!isSpecificMember(parsedMember, ModifierName.rejects)
) {
// ts doesn't think that the ModifierName.not check is the direct inverse as the above two checks
// todo: impossible at runtime, but can't be typed w/o negation support
throw new Error(
`modifier name must be either "${ModifierName.resolves}" or "${ModifierName.rejects}" (got "${parsedMember.name}")`,
);
}

/* istanbul ignore next */
const negation =
parsedMember.node.parent &&
isExpectMember(parsedMember.node.parent, ModifierName.not)
? parsedMember.node.parent
: undefined;

return {
...parsedMember,
negation,
};
};

const isSpecificMember = <Name extends ExpectPropertyName>(
member: ParsedExpectMember,
specific: Name,
): member is ParsedExpectMember<Name> => member.name === specific;

/**
* Checks if the given `ParsedExpectMember` should be re-parsed as an `ParsedExpectModifier`.
*
* @param {ParsedExpectMember} member
*
* @return {member is ParsedExpectMember<ModifierName>}
*/
const shouldBeParsedExpectModifier = (
member: ParsedExpectMember,
): member is ParsedExpectMember<ModifierName> =>
ModifierName.hasOwnProperty(member.name);

export const parseExpectCall = <ExpectNode extends ExpectCall>(
expect: ExpectNode,
): Expectation<ExpectNode> => {
const expectation: Expectation<ExpectNode> = {
expect,
};

if (!isExpectMember(expect.parent)) {
return expectation;
}

const parsedMember = parseExpectMember(expect.parent);
if (!shouldBeParsedExpectModifier(parsedMember)) {
expectation.matcher = reparseAsMatcher(parsedMember);

return expectation;
}

const modifier = (expectation.modifier = reparseMemberAsModifier(
parsedMember,
));

const memberNode = modifier.negation || modifier.node;

if (!memberNode.parent || !isExpectMember(memberNode.parent)) {
return expectation;
}

expectation.matcher = reparseAsMatcher(parseExpectMember(memberNode.parent));

return expectation;
};

export enum DescribeAlias {
'describe' = 'describe',
'fdescribe' = 'fdescribe',
Expand Down

0 comments on commit 9465e57

Please sign in to comment.