Skip to content

Commit ce4770f

Browse files
authored
refactor(no-node-access): use new testing library rule maker (#237)
* build: add npmrc file Adding .npmrc file to indicate we don't want to generate package-lock properly. * refactor: first approach for testing library detection * refactor: move testing library detection to high-order function * refactor: include create-testing-library-rule * refactor(no-node-access): use create-testing-library-rule * test: decrease coverage threshold for utils detection * test: decrease coverage threshold for utils detection branches * style: add missing return type on function * style: format with prettier properly Apparently the regexp for formatting the files within npm command must be passed with double quotes. More details here: https://dev.to/gruckion/comment/b665 * docs: copied types clarification
1 parent 1dbb513 commit ce4770f

11 files changed

+157
-55
lines changed

.npmrc

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
package-lock=false

jest.config.js

+8-2
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,17 @@ module.exports = {
1111
statements: 100,
1212
},
1313
// TODO drop this custom threshold in v4
14-
"./lib/node-utils.ts": {
14+
'./lib/detect-testing-library-utils.ts': {
15+
branches: 50,
16+
functions: 90,
17+
lines: 90,
18+
statements: 90,
19+
},
20+
'./lib/node-utils.ts': {
1521
branches: 90,
1622
functions: 90,
1723
lines: 90,
1824
statements: 90,
19-
}
25+
},
2026
},
2127
};

lib/create-testing-library-rule.ts

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { ESLintUtils, TSESLint } from '@typescript-eslint/experimental-utils';
2+
import { getDocsUrl } from './utils';
3+
import {
4+
detectTestingLibraryUtils,
5+
DetectionHelpers,
6+
} from './detect-testing-library-utils';
7+
8+
// These 2 types are copied from @typescript-eslint/experimental-utils
9+
type CreateRuleMetaDocs = Omit<TSESLint.RuleMetaDataDocs, 'url'>;
10+
type CreateRuleMeta<TMessageIds extends string> = {
11+
docs: CreateRuleMetaDocs;
12+
} & Omit<TSESLint.RuleMetaData<TMessageIds>, 'docs'>;
13+
14+
export function createTestingLibraryRule<
15+
TOptions extends readonly unknown[],
16+
TMessageIds extends string,
17+
TRuleListener extends TSESLint.RuleListener = TSESLint.RuleListener
18+
>(
19+
config: Readonly<{
20+
name: string;
21+
meta: CreateRuleMeta<TMessageIds>;
22+
defaultOptions: Readonly<TOptions>;
23+
create: (
24+
context: Readonly<TSESLint.RuleContext<TMessageIds, TOptions>>,
25+
optionsWithDefault: Readonly<TOptions>,
26+
detectionHelpers: Readonly<DetectionHelpers>
27+
) => TRuleListener;
28+
}>
29+
): TSESLint.RuleModule<TMessageIds, TOptions, TRuleListener> {
30+
const { create, ...remainingConfig } = config;
31+
32+
return ESLintUtils.RuleCreator(getDocsUrl)({
33+
...remainingConfig,
34+
create: detectTestingLibraryUtils<TOptions, TMessageIds, TRuleListener>(
35+
create
36+
),
37+
});
38+
}

lib/detect-testing-library-utils.ts

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils';
2+
3+
export type DetectionHelpers = {
4+
getIsImportingTestingLibrary: () => boolean;
5+
};
6+
7+
/**
8+
* Enhances a given rule `create` with helpers to detect Testing Library utils.
9+
*/
10+
export function detectTestingLibraryUtils<
11+
TOptions extends readonly unknown[],
12+
TMessageIds extends string,
13+
TRuleListener extends TSESLint.RuleListener = TSESLint.RuleListener
14+
>(
15+
ruleCreate: (
16+
context: Readonly<TSESLint.RuleContext<TMessageIds, TOptions>>,
17+
optionsWithDefault: Readonly<TOptions>,
18+
detectionHelpers: Readonly<DetectionHelpers>
19+
) => TRuleListener
20+
) {
21+
return (
22+
context: Readonly<TSESLint.RuleContext<TMessageIds, TOptions>>,
23+
optionsWithDefault: Readonly<TOptions>
24+
): TRuleListener => {
25+
let isImportingTestingLibrary = false;
26+
27+
// TODO: init here options based on shared ESLint config
28+
29+
// helpers for Testing Library detection
30+
const helpers: DetectionHelpers = {
31+
getIsImportingTestingLibrary() {
32+
return isImportingTestingLibrary;
33+
},
34+
};
35+
36+
// instructions for Testing Library detection
37+
const detectionInstructions: TSESLint.RuleListener = {
38+
ImportDeclaration(node: TSESTree.ImportDeclaration) {
39+
isImportingTestingLibrary = /testing-library/g.test(
40+
node.source.value as string
41+
);
42+
},
43+
};
44+
45+
// update given rule to inject Testing Library detection
46+
const ruleInstructions = ruleCreate(context, optionsWithDefault, helpers);
47+
const enhancedRuleInstructions = Object.assign({}, ruleInstructions);
48+
49+
Object.keys(detectionInstructions).forEach((instruction) => {
50+
(enhancedRuleInstructions as TSESLint.RuleListener)[instruction] = (
51+
node
52+
) => {
53+
if (instruction in detectionInstructions) {
54+
detectionInstructions[instruction](node);
55+
}
56+
57+
if (ruleInstructions[instruction]) {
58+
return ruleInstructions[instruction](node);
59+
}
60+
};
61+
});
62+
63+
return enhancedRuleInstructions;
64+
};
65+
}

lib/node-utils.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export function isImportSpecifier(
4040
export function isImportNamespaceSpecifier(
4141
node: TSESTree.Node
4242
): node is TSESTree.ImportNamespaceSpecifier {
43-
return node?.type === AST_NODE_TYPES.ImportNamespaceSpecifier
43+
return node?.type === AST_NODE_TYPES.ImportNamespaceSpecifier;
4444
}
4545

4646
export function isImportDefaultSpecifier(
@@ -145,7 +145,7 @@ export function isReturnStatement(
145145
export function isArrayExpression(
146146
node: TSESTree.Node
147147
): node is TSESTree.ArrayExpression {
148-
return node?.type === AST_NODE_TYPES.ArrayExpression
148+
return node?.type === AST_NODE_TYPES.ArrayExpression;
149149
}
150150

151151
export function isAwaited(node: TSESTree.Node): boolean {

lib/rules/no-node-access.ts

+6-14
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils';
2-
import { getDocsUrl, ALL_RETURNING_NODES } from '../utils';
1+
import { TSESTree } from '@typescript-eslint/experimental-utils';
2+
import { ALL_RETURNING_NODES } from '../utils';
33
import { isIdentifier } from '../node-utils';
4+
import { createTestingLibraryRule } from '../create-testing-library-rule';
45

56
export const RULE_NAME = 'no-node-access';
67
export type MessageIds = 'noNodeAccess';
78
type Options = [];
89

9-
export default ESLintUtils.RuleCreator(getDocsUrl)<Options, MessageIds>({
10+
export default createTestingLibraryRule<Options, MessageIds>({
1011
name: RULE_NAME,
1112
meta: {
1213
type: 'problem',
@@ -24,19 +25,11 @@ export default ESLintUtils.RuleCreator(getDocsUrl)<Options, MessageIds>({
2425
},
2526
defaultOptions: [],
2627

27-
create(context) {
28-
let isImportingTestingLibrary = false;
29-
30-
function checkTestingEnvironment(node: TSESTree.ImportDeclaration) {
31-
isImportingTestingLibrary = /testing-library/g.test(
32-
node.source.value as string
33-
);
34-
}
35-
28+
create: (context, _, helpers) => {
3629
function showErrorForNodeAccess(node: TSESTree.MemberExpression) {
3730
isIdentifier(node.property) &&
3831
ALL_RETURNING_NODES.includes(node.property.name) &&
39-
isImportingTestingLibrary &&
32+
helpers.getIsImportingTestingLibrary() &&
4033
context.report({
4134
node: node,
4235
loc: node.property.loc.start,
@@ -45,7 +38,6 @@ export default ESLintUtils.RuleCreator(getDocsUrl)<Options, MessageIds>({
4538
}
4639

4740
return {
48-
['ImportDeclaration']: checkTestingEnvironment,
4941
['ExpressionStatement MemberExpression']: showErrorForNodeAccess,
5042
['VariableDeclarator MemberExpression']: showErrorForNodeAccess,
5143
};

lib/utils.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -128,5 +128,5 @@ export {
128128
METHODS_RETURNING_NODES,
129129
ALL_RETURNING_NODES,
130130
PRESENCE_MATCHERS,
131-
ABSENCE_MATCHERS
131+
ABSENCE_MATCHERS,
132132
};

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@
2929
"postbuild": "cpy README.md ./dist && cpy package.json ./dist && cpy LICENSE ./dist",
3030
"lint": "eslint . --ext .js,.ts",
3131
"lint:fix": "npm run lint -- --fix",
32-
"format": "prettier --write README.md {lib,docs,tests}/**/*.{js,ts,md}",
33-
"format:check": "prettier --check README.md {lib,docs,tests}/**/*.{js,json,yml,ts,md}",
32+
"format": "prettier --write README.md \"{lib,docs,tests}/**/*.{js,ts,md}\"",
33+
"format:check": "prettier --check README.md \"{lib,docs,tests}/**/*.{js,json,yml,ts,md}\"",
3434
"test:local": "jest",
3535
"test:ci": "jest --coverage",
3636
"test:update": "npm run test:local -- --u",

tests/lib/rules/await-async-utils.test.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ ruleTester.run(RULE_NAME, rule, {
120120
});
121121
`,
122122
})),
123-
...ASYNC_UTILS.map(asyncUtil => ({
123+
...ASYNC_UTILS.map((asyncUtil) => ({
124124
code: `
125125
import { ${asyncUtil} } from '@testing-library/dom';
126126
test('${asyncUtil} util used in with Promise.all() does not trigger an error', async () => {
@@ -131,7 +131,7 @@ ruleTester.run(RULE_NAME, rule, {
131131
});
132132
`,
133133
})),
134-
...ASYNC_UTILS.map(asyncUtil => ({
134+
...ASYNC_UTILS.map((asyncUtil) => ({
135135
code: `
136136
import { ${asyncUtil} } from '@testing-library/dom';
137137
test('${asyncUtil} util used in with Promise.all() with an await does not trigger an error', async () => {
@@ -142,7 +142,7 @@ ruleTester.run(RULE_NAME, rule, {
142142
});
143143
`,
144144
})),
145-
...ASYNC_UTILS.map(asyncUtil => ({
145+
...ASYNC_UTILS.map((asyncUtil) => ({
146146
code: `
147147
import { ${asyncUtil} } from '@testing-library/dom';
148148
test('${asyncUtil} util used in with Promise.all() with ".then" does not trigger an error', async () => {
@@ -162,7 +162,7 @@ ruleTester.run(RULE_NAME, rule, {
162162
waitForElementToBeRemoved(() => document.querySelector('div.getOuttaHere')),
163163
])
164164
});
165-
`
165+
`,
166166
},
167167
{
168168
code: `
@@ -191,8 +191,8 @@ ruleTester.run(RULE_NAME, rule, {
191191
await foo().then(() => baz())
192192
])
193193
})
194-
`
195-
}
194+
`,
195+
},
196196
],
197197
invalid: [
198198
...ASYNC_UTILS.map((asyncUtil) => ({

0 commit comments

Comments
 (0)