Skip to content

Commit 9ebf3d2

Browse files
feat(no-node-access): add rule
with few test cases
1 parent 4d41edc commit 9ebf3d2

File tree

4 files changed

+122
-7
lines changed

4 files changed

+122
-7
lines changed

lib/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import noContainer from './rules/no-container';
77
import noDebug from './rules/no-debug';
88
import noDomImport from './rules/no-dom-import';
99
import noManualCleanup from './rules/no-manual-cleanup';
10+
import noNodeAccess from './rules/no-node-access';
1011
import noWaitForEmptyCallback from './rules/no-wait-for-empty-callback';
1112
import noPromiseInFireEvent from './rules/no-promise-in-fire-event';
1213
import preferExplicitAssert from './rules/prefer-explicit-assert';
@@ -25,6 +26,7 @@ const rules = {
2526
'no-debug': noDebug,
2627
'no-dom-import': noDomImport,
2728
'no-manual-cleanup': noManualCleanup,
29+
'no-node-access': noNodeAccess,
2830
'no-promise-in-fire-event': noPromiseInFireEvent,
2931
'no-wait-for-empty-callback': noWaitForEmptyCallback,
3032
'prefer-explicit-assert': preferExplicitAssert,
@@ -49,13 +51,15 @@ const angularRules = {
4951
'testing-library/no-container': 'error',
5052
'testing-library/no-debug': 'warn',
5153
'testing-library/no-dom-import': ['error', 'angular'],
54+
'testing-library/no-node-access': 'error',
5255
};
5356

5457
const reactRules = {
5558
...domRules,
5659
'testing-library/no-container': 'error',
5760
'testing-library/no-debug': 'warn',
5861
'testing-library/no-dom-import': ['error', 'react'],
62+
'testing-library/no-node-access': 'error',
5963
};
6064

6165
const vueRules = {
@@ -64,6 +68,7 @@ const vueRules = {
6468
'testing-library/no-container': 'error',
6569
'testing-library/no-debug': 'warn',
6670
'testing-library/no-dom-import': ['error', 'vue'],
71+
'testing-library/no-node-access': 'error',
6772
};
6873

6974
export = {

lib/rules/no-node-access.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils';
2+
import { isIdentifier, isMemberExpression, isLiteral } from '../node-utils';
3+
import {
4+
getDocsUrl,
5+
ALL_QUERIES_METHODS,
6+
PROPERTIES_RETURNING_NODES,
7+
METHODS_RETURNING_NODES,
8+
} from '../utils';
9+
10+
export const RULE_NAME = 'no-node-access';
11+
12+
const ALL_RETURNING_NODES = [
13+
...PROPERTIES_RETURNING_NODES,
14+
...METHODS_RETURNING_NODES,
15+
];
16+
17+
export default ESLintUtils.RuleCreator(getDocsUrl)({
18+
name: RULE_NAME,
19+
meta: {
20+
type: 'problem',
21+
docs: {
22+
description: 'Disallow the use of Node methods',
23+
category: 'Best Practices',
24+
recommended: 'error',
25+
},
26+
messages: {
27+
noNodeAccess:
28+
'Avoid direct Node access. Prefer using the methods from Testing Library."',
29+
},
30+
fixable: null,
31+
schema: [],
32+
},
33+
defaultOptions: [],
34+
35+
create(context) {
36+
const variablesWithNodes: string[] = [];
37+
38+
function identifyVariablesWithNodes(node: TSESTree.MemberExpression) {
39+
const methodCalled = ALL_QUERIES_METHODS.filter(
40+
methodName =>
41+
isIdentifier(node.property) && node.property.name.includes(methodName)
42+
);
43+
const returnsNodeElement = Boolean(methodCalled.length);
44+
45+
const callExpression = node.parent as TSESTree.CallExpression;
46+
const variableDeclarator = callExpression.parent as TSESTree.VariableDeclarator;
47+
const variableName =
48+
isIdentifier(variableDeclarator.id) && variableDeclarator.id.name;
49+
50+
if (returnsNodeElement) {
51+
variablesWithNodes.push(variableName);
52+
}
53+
}
54+
55+
function showErrorForNodeAccess(node: TSESTree.Identifier) {
56+
if (variablesWithNodes.includes(node.name)) {
57+
if (
58+
isMemberExpression(node.parent) &&
59+
isLiteral(node.parent.property) &&
60+
typeof node.parent.property.value === 'number'
61+
) {
62+
context.report({
63+
node: node,
64+
messageId: 'noNodeAccess',
65+
});
66+
}
67+
isMemberExpression(node.parent) &&
68+
isIdentifier(node.parent.property) &&
69+
ALL_RETURNING_NODES.includes(node.parent.property.name) &&
70+
context.report({
71+
node: node,
72+
messageId: 'noNodeAccess',
73+
});
74+
}
75+
}
76+
77+
return {
78+
['VariableDeclarator > CallExpression > MemberExpression']: identifyVariablesWithNodes,
79+
['MemberExpression > Identifier']: showErrorForNodeAccess,
80+
};
81+
},
82+
});

tests/__snapshots__/index.test.ts.snap

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Object {
1515
"error",
1616
"angular",
1717
],
18+
"testing-library/no-node-access": "error",
1819
"testing-library/no-promise-in-fire-event": "error",
1920
"testing-library/no-wait-for-empty-callback": "error",
2021
"testing-library/prefer-find-by": "error",
@@ -55,6 +56,7 @@ Object {
5556
"error",
5657
"react",
5758
],
59+
"testing-library/no-node-access": "error",
5860
"testing-library/no-promise-in-fire-event": "error",
5961
"testing-library/no-wait-for-empty-callback": "error",
6062
"testing-library/prefer-find-by": "error",
@@ -79,6 +81,7 @@ Object {
7981
"error",
8082
"vue",
8183
],
84+
"testing-library/no-node-access": "error",
8285
"testing-library/no-promise-in-fire-event": "error",
8386
"testing-library/no-wait-for-empty-callback": "error",
8487
"testing-library/prefer-find-by": "error",

tests/lib/rules/no-node-access.test.ts

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,16 @@ ruleTester.run(RULE_NAME, rule, {
1111
valid: [
1212
{
1313
code: `
14-
const buttonText = screen.getByText('submit');
15-
`,
14+
const buttonText = screen.getByText('submit');
15+
`,
1616
},
1717
{
1818
code: `
19-
const obj = {
20-
firstChild: <div>child</div>
21-
}
22-
obj.firstChild
23-
`,
19+
const obj = {
20+
firstChild: <div>child</div>
21+
}
22+
obj.firstChild
23+
`,
2424
},
2525
],
2626
invalid: [
@@ -49,5 +49,30 @@ ruleTester.run(RULE_NAME, rule, {
4949
},
5050
],
5151
},
52+
{
53+
code: `
54+
function getExampleDOM() {
55+
const container = document.createElement('div');
56+
container.innerHTML = \`
57+
<label for="username">Username</label>
58+
<input id="username" />
59+
<button>Print Username</button>
60+
<label for="password">Password</label>
61+
<input id="password" />
62+
<button>Print password</button>
63+
<button type="submit">Submit</button>
64+
\`;
65+
return container;
66+
}
67+
const exampleDOM = getExampleDOM();
68+
const submitButton = screen.getByText(exampleDOM, 'Submit');
69+
const previousSibling = submitButton.previousSibling
70+
`,
71+
errors: [
72+
{
73+
messageId: 'noNodeAccess',
74+
},
75+
],
76+
},
5277
],
5378
});

0 commit comments

Comments
 (0)