Skip to content

Commit 7202e7d

Browse files
committed
chore(no-large-snapshots): convert to typescript
1 parent 392db88 commit 7202e7d

File tree

4 files changed

+122
-37
lines changed

4 files changed

+122
-37
lines changed

src/rules/__tests__/no-large-snapshots.test.js renamed to src/rules/__tests__/no-large-snapshots.test.ts

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,30 @@
1-
import { RuleTester } from 'eslint';
1+
import {
2+
AST_NODE_TYPES,
3+
TSESLint,
4+
TSESTree,
5+
} from '@typescript-eslint/experimental-utils';
26
import rule from '../no-large-snapshots';
37

48
const noLargeSnapshots = rule.create.bind(rule);
59

6-
const ruleTester = new RuleTester({
10+
const ruleTester = new TSESLint.RuleTester({
711
parserOptions: {
812
ecmaVersion: 2015,
913
},
1014
});
1115

12-
const generateSnapshotLines = lines => `\`\n${'line\n'.repeat(lines)}\``;
16+
const generateSnapshotLines = (lines: number) =>
17+
`\`\n${'line\n'.repeat(lines)}\``;
1318

14-
const generateExportsSnapshotString = (lines, title = 'a big component 1') =>
15-
`exports[\`${title}\`] = ${generateSnapshotLines(lines - 1)};`;
19+
const generateExportsSnapshotString = (
20+
lines: number,
21+
title: string = 'a big component 1',
22+
) => `exports[\`${title}\`] = ${generateSnapshotLines(lines - 1)};`;
1623

17-
const generateExpectInlineSnapsCode = (lines, matcher) =>
18-
`expect(something).${matcher}(${generateSnapshotLines(lines)});`;
24+
const generateExpectInlineSnapsCode = (
25+
lines: number,
26+
matcher: 'toMatchInlineSnapshot' | 'toThrowErrorMatchingInlineSnapshot',
27+
) => `expect(something).${matcher}(${generateSnapshotLines(lines)});`;
1928

2029
ruleTester.run('no-large-snapshots', rule, {
2130
valid: [
@@ -156,7 +165,9 @@ ruleTester.run('no-large-snapshots', rule, {
156165
});
157166

158167
describe('no-large-snapshots', () => {
159-
const buildBaseNode = type => ({
168+
const buildBaseNode = <Type extends AST_NODE_TYPES>(
169+
type: Type,
170+
): TSESTree.BaseNode & { type: Type } => ({
160171
type,
161172
range: [0, 1],
162173
loc: {
@@ -190,8 +201,8 @@ describe('no-large-snapshots', () => {
190201

191202
expect(() =>
192203
ExpressionStatement({
193-
...buildBaseNode('ExpressionStatement'),
194-
expression: buildBaseNode('JSXClosingFragment'),
204+
...buildBaseNode(AST_NODE_TYPES.ExpressionStatement),
205+
expression: buildBaseNode(AST_NODE_TYPES.JSXClosingFragment),
195206
}),
196207
).toThrow(
197208
'All paths for whitelistedSnapshots must be absolute. You can use JS config and `path.resolve`',
Lines changed: 56 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,35 @@
1+
import {
2+
AST_NODE_TYPES,
3+
TSESLint,
4+
TSESTree,
5+
} from '@typescript-eslint/experimental-utils';
16
import { isAbsolute } from 'path';
2-
import { getDocsUrl, getStringValue } from './util';
7+
import {
8+
createRule,
9+
getAccessorValue,
10+
isExpectMember,
11+
isSupportedAccessor,
12+
} from './tsUtils';
313

4-
const reportOnViolation = (context, node) => {
5-
const lineLimit =
6-
context.options[0] && Number.isFinite(context.options[0].maxSize)
7-
? context.options[0].maxSize
8-
: 50;
14+
interface RuleOptions {
15+
maxSize?: number;
16+
whitelistedSnapshots?: Record<string, Array<string | RegExp>>;
17+
}
18+
19+
type MessageId = 'noSnapshot' | 'tooLongSnapshots';
20+
21+
type RuleContext = TSESLint.RuleContext<MessageId, [RuleOptions]>;
22+
23+
const reportOnViolation = (
24+
context: RuleContext,
25+
node: TSESTree.CallExpression | TSESTree.ExpressionStatement,
26+
{ maxSize: lineLimit = 50, whitelistedSnapshots = {} }: RuleOptions,
27+
) => {
928
const startLine = node.loc.start.line;
1029
const endLine = node.loc.end.line;
1130
const lineCount = endLine - startLine;
12-
const whitelistedSnapshots =
13-
context.options &&
14-
context.options[0] &&
15-
context.options[0].whitelistedSnapshots;
1631

17-
const allPathsAreAbsolute = Object.keys(whitelistedSnapshots || {}).every(
32+
const allPathsAreAbsolute = Object.keys(whitelistedSnapshots).every(
1833
isAbsolute,
1934
);
2035

@@ -26,17 +41,23 @@ const reportOnViolation = (context, node) => {
2641

2742
let isWhitelisted = false;
2843

29-
if (whitelistedSnapshots) {
44+
if (
45+
whitelistedSnapshots &&
46+
node.type === AST_NODE_TYPES.ExpressionStatement &&
47+
'left' in node.expression &&
48+
isExpectMember(node.expression.left)
49+
) {
3050
const fileName = context.getFilename();
3151
const whitelistedSnapshotsInFile = whitelistedSnapshots[fileName];
3252

3353
if (whitelistedSnapshotsInFile) {
34-
const snapshotName = getStringValue(node.expression.left.property);
54+
const snapshotName = getAccessorValue(node.expression.left.property);
3555
isWhitelisted = whitelistedSnapshotsInFile.some(name => {
36-
if (name.test && typeof name.test === 'function') {
56+
if (name instanceof RegExp) {
3757
return name.test(snapshotName);
3858
}
39-
return name === snapshotName;
59+
60+
return snapshotName;
4061
});
4162
}
4263
}
@@ -50,16 +71,20 @@ const reportOnViolation = (context, node) => {
5071
}
5172
};
5273

53-
export default {
74+
export default createRule<[RuleOptions], MessageId>({
75+
name: __filename,
5476
meta: {
5577
docs: {
56-
url: getDocsUrl(__filename),
78+
category: 'Best Practices',
79+
description: 'disallow large snapshots',
80+
recommended: false,
5781
},
5882
messages: {
5983
noSnapshot: '`{{ lineCount }}`s should begin with lowercase',
6084
tooLongSnapshots:
6185
'Expected Jest snapshot to be smaller than {{ lineLimit }} lines but was {{ lineCount }} lines long',
6286
},
87+
type: 'suggestion',
6388
schema: [
6489
{
6590
type: 'object',
@@ -78,28 +103,34 @@ export default {
78103
},
79104
],
80105
},
81-
create(context) {
106+
defaultOptions: [{}],
107+
create(context, [options]) {
82108
if (context.getFilename().endsWith('.snap')) {
83109
return {
84110
ExpressionStatement(node) {
85-
reportOnViolation(context, node);
111+
reportOnViolation(context, node, options);
86112
},
87113
};
88114
} else if (context.getFilename().endsWith('.js')) {
89115
return {
90116
CallExpression(node) {
91-
const propertyName =
92-
node.callee.property && node.callee.property.name;
93117
if (
94-
propertyName === 'toMatchInlineSnapshot' ||
95-
propertyName === 'toThrowErrorMatchingInlineSnapshot'
118+
'property' in node.callee &&
119+
(isSupportedAccessor(
120+
node.callee.property,
121+
'toMatchInlineSnapshot',
122+
) ||
123+
isSupportedAccessor(
124+
node.callee.property,
125+
'toThrowErrorMatchingInlineSnapshot',
126+
))
96127
) {
97-
reportOnViolation(context, node);
128+
reportOnViolation(context, node, options);
98129
}
99130
},
100131
};
101132
}
102133

103134
return {};
104135
},
105-
};
136+
});

src/rules/tsUtils.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,14 @@ export const isStringNode = <V extends string>(
108108
export const getStringValue = <S extends string>(node: StringNode<S>): S =>
109109
isTemplateLiteral(node) ? node.quasis[0].value.raw : node.value;
110110

111+
/**
112+
* Represents a `MemberExpression` with a "known" `property`.
113+
*/
114+
interface KnownMemberExpression<Name extends string = string>
115+
extends TSESTree.MemberExpression {
116+
property: AccessorNode<Name>;
117+
}
118+
111119
/**
112120
* An `Identifier` with a known `name` value - i.e `expect`.
113121
*/
@@ -213,6 +221,43 @@ type AccessorNode<Specifics extends string = string> =
213221
| StringNode<Specifics>
214222
| KnownIdentifier<Specifics>;
215223

224+
interface ExpectCall extends TSESTree.CallExpression {
225+
callee: AccessorNode<'expect'>;
226+
parent: TSESTree.Node;
227+
}
228+
229+
/**
230+
* Represents a `MemberExpression` that comes after an `ExpectCall`.
231+
*/
232+
interface ExpectMember<
233+
PropertyName extends ExpectPropertyName = ExpectPropertyName,
234+
Parent extends TSESTree.Node | undefined = TSESTree.Node | undefined
235+
> extends KnownMemberExpression<PropertyName> {
236+
object: ExpectCall | ExpectMember;
237+
parent: Parent;
238+
}
239+
240+
export const isExpectMember = <
241+
Name extends ExpectPropertyName = ExpectPropertyName
242+
>(
243+
node: TSESTree.Node,
244+
name?: Name,
245+
): node is ExpectMember<Name> =>
246+
node.type === AST_NODE_TYPES.MemberExpression &&
247+
isSupportedAccessor(node.property, name);
248+
249+
/**
250+
* Represents all the jest matchers.
251+
*/
252+
type MatcherName = string /* & not ModifierName */;
253+
type ExpectPropertyName = ModifierName | MatcherName;
254+
255+
export enum ModifierName {
256+
not = 'not',
257+
rejects = 'rejects',
258+
resolves = 'resolves',
259+
}
260+
216261
interface JestExpectIdentifier extends TSESTree.Identifier {
217262
name: 'expect';
218263
}

src/rules/util.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,6 @@ export const isFunction = node =>
102102
(node.type === 'FunctionExpression' ||
103103
node.type === 'ArrowFunctionExpression');
104104

105-
export const getStringValue = arg => arg.quasis[0].value.raw;
106-
107105
/**
108106
* Generates the URL to documentation for the given rule name. It uses the
109107
* package version to build the link to a tagged version of the

0 commit comments

Comments
 (0)