Skip to content

Commit

Permalink
chore(prefer-to-contain): convert to typescript (#393)
Browse files Browse the repository at this point in the history
  • Loading branch information
G-Rath committed Aug 15, 2019
1 parent 425cc69 commit c914f1b
Show file tree
Hide file tree
Showing 4 changed files with 294 additions and 160 deletions.
@@ -1,7 +1,7 @@
import { RuleTester } from 'eslint';
import { TSESLint } from '@typescript-eslint/experimental-utils';
import rule from '../prefer-to-contain';

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

ruleTester.run('prefer-to-contain', rule, {
valid: [
Expand Down Expand Up @@ -31,6 +31,12 @@ ruleTester.run('prefer-to-contain', rule, {
errors: [{ messageId: 'useToContain', column: 23, line: 1 }],
output: 'expect(a).toContain(b);',
},
// todo: support this, as it's counted by isSupportedAccessor
// {
// code: "expect(a['includes'](b)).toEqual(true);",
// errors: [{ messageId: 'useToContain', column: 23, line: 1 }],
// output: 'expect(a).toContain(b);',
// },
{
code: 'expect(a.includes(b)).toEqual(false);',
errors: [{ messageId: 'useToContain', column: 23, line: 1 }],
Expand Down
131 changes: 0 additions & 131 deletions src/rules/prefer-to-contain.js

This file was deleted.

242 changes: 242 additions & 0 deletions src/rules/prefer-to-contain.ts
@@ -0,0 +1,242 @@
import {
AST_NODE_TYPES,
TSESLint,
TSESTree,
} from '@typescript-eslint/experimental-utils';
import {
CallExpressionWithSingleArgument,
KnownCallExpression,
ModifierName,
NotNegatableParsedModifier,
ParsedEqualityMatcherCall,
ParsedExpectMatcher,
createRule,
hasOnlyOneArgument,
isExpectCall,
isParsedEqualityMatcherCall,
isSupportedAccessor,
parseExpectCall,
} from './tsUtils';

interface BooleanLiteral extends TSESTree.Literal {
value: boolean;
}

const isBooleanLiteral = (node: TSESTree.Node): node is BooleanLiteral =>
node.type === AST_NODE_TYPES.Literal && typeof node.value === 'boolean';

type ParsedBooleanEqualityMatcherCall = ParsedEqualityMatcherCall<
BooleanLiteral
>;

/**
* Checks if the given `ParsedExpectMatcher` is a call to one of the equality matchers,
* with a boolean literal as the sole argument.
*
* @example javascript
* toBe(true);
* toEqual(false);
*
* @param {ParsedExpectMatcher} matcher
*
* @return {matcher is ParsedBooleanEqualityMatcher}
*/
const isBooleanEqualityMatcher = (
matcher: ParsedExpectMatcher,
): matcher is ParsedBooleanEqualityMatcherCall =>
isParsedEqualityMatcherCall(matcher) &&
isBooleanLiteral(matcher.arguments[0]);

type FixableIncludesCallExpression = KnownCallExpression<'includes'> &
CallExpressionWithSingleArgument;

/**
* Checks if the given `node` is a `CallExpression` representing the calling
* of an `includes`-like method that can be 'fixed' (using `toContain`).
*
* @param {CallExpression} node
*
* @return {node is FixableIncludesCallExpression}
*
* @todo support `['includes']()` syntax (remove last property.type check to begin)
* @todo break out into `isMethodCall<Name extends string>(node: TSESTree.Node, method: Name)` util-fn
*/
const isFixableIncludesCallExpression = (
node: TSESTree.Node,
): node is FixableIncludesCallExpression =>
node.type === AST_NODE_TYPES.CallExpression &&
node.callee.type === AST_NODE_TYPES.MemberExpression &&
isSupportedAccessor(node.callee.property, 'includes') &&
node.callee.property.type === AST_NODE_TYPES.Identifier &&
hasOnlyOneArgument(node);

const buildToContainFuncExpectation = (negated: boolean) =>
negated ? `${ModifierName.not}.toContain` : 'toContain';

/**
* Finds the first `.` character token between the `object` & `property` of the given `member` expression.
*
* @param {TSESTree.MemberExpression} member
* @param {SourceCode} sourceCode
*
* @return {Token | null}
*/
const findPropertyDotToken = (
member: TSESTree.MemberExpression,
sourceCode: TSESLint.SourceCode,
) =>
sourceCode.getFirstTokenBetween(
member.object,
member.property,
token => token.value === '.',
);

const getNegationFixes = (
node: FixableIncludesCallExpression,
modifier: NotNegatableParsedModifier,
matcher: ParsedBooleanEqualityMatcherCall,
sourceCode: TSESLint.SourceCode,
fixer: TSESLint.RuleFixer,
fileName: string,
) => {
const [containArg] = node.arguments;
const negationPropertyDot = findPropertyDotToken(modifier.node, sourceCode);

const toContainFunc = buildToContainFuncExpectation(
matcher.arguments[0].value,
);

/* istanbul ignore if */
if (negationPropertyDot === null) {
throw new Error(
`Unexpected null when attempting to fix ${fileName} - please file a github issue at https://github.com/jest-community/eslint-plugin-jest`,
);
}

return [
fixer.remove(negationPropertyDot),
fixer.remove(modifier.node.property),
fixer.replaceText(matcher.node.property, toContainFunc),
fixer.replaceText(matcher.arguments[0], sourceCode.getText(containArg)),
];
};

const getCommonFixes = (
node: FixableIncludesCallExpression,
sourceCode: TSESLint.SourceCode,
fileName: string,
): Array<TSESTree.Node | TSESTree.Token> => {
const [containArg] = node.arguments;
const includesCallee = node.callee;

const propertyDot = findPropertyDotToken(includesCallee, sourceCode);

const closingParenthesis = sourceCode.getTokenAfter(containArg);
const openParenthesis = sourceCode.getTokenBefore(containArg);

/* istanbul ignore if */
if (
propertyDot === null ||
closingParenthesis === null ||
openParenthesis === null
) {
throw new Error(
`Unexpected null when attempting to fix ${fileName} - please file a github issue at https://github.com/jest-community/eslint-plugin-jest`,
);
}

return [
containArg,
includesCallee.property,
propertyDot,
closingParenthesis,
openParenthesis,
];
};
// expect(array.includes(<value>)[not.]{toBe,toEqual}(<boolean>)
export default createRule({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Suggest using `toContain()`',
recommended: false,
},
messages: {
useToContain: 'Use toContain() instead',
},
fixable: 'code',
type: 'suggestion',
schema: [],
},
defaultOptions: [],
create(context) {
return {
CallExpression(node) {
if (!isExpectCall(node)) {
return;
}

const {
expect: {
arguments: [includesCall],
},
matcher,
modifier,
} = parseExpectCall(node);

if (
!matcher ||
(modifier && modifier.name !== ModifierName.not) ||
!isBooleanEqualityMatcher(matcher) ||
!isFixableIncludesCallExpression(includesCall)
) {
return;
}

context.report({
fix(fixer) {
const sourceCode = context.getSourceCode();
const fileName = context.getFilename();

const fixArr = getCommonFixes(
includesCall,
sourceCode,
fileName,
).map(target => fixer.remove(target));

if (modifier && modifier.name === ModifierName.not) {
return getNegationFixes(
includesCall,
modifier,
matcher,
sourceCode,
fixer,
fileName,
).concat(fixArr);
}

const toContainFunc = buildToContainFuncExpectation(
!matcher.arguments[0].value,
);

const [containArg] = includesCall.arguments;

fixArr.push(
fixer.replaceText(matcher.node.property, toContainFunc),
);
fixArr.push(
fixer.replaceText(
matcher.arguments[0],
sourceCode.getText(containArg),
),
);
return fixArr;
},
messageId: 'useToContain',
node: (modifier || matcher).node.property,
});
},
};
},
});

0 comments on commit c914f1b

Please sign in to comment.