Skip to content

Commit e0a5344

Browse files
committed
Adding restrict-dom-node-retrieval rule to prevent direct dom manipulation (should be done through react refs)
1 parent 77f3943 commit e0a5344

File tree

3 files changed

+226
-0
lines changed

3 files changed

+226
-0
lines changed

README.md

+15
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,21 @@ For example, you could warn if somebody tries to import `Difference` from the `j
142142
#### Motivation
143143
We use this plugin ourselves when we use a library but want to prevent certain imports from it that have unexpected side effects.
144144

145+
### restrict-dome-node-retrieval
146+
Rule that restricts using dom-retrieval methods ("querySelector", "querySelectorAll", "getElementsByTagName", "getElementsByTagNameNS", "getElementsByClassName", "getElementById") from document.
147+
148+
#### Configuration
149+
```json
150+
{
151+
"rules": {
152+
"moxio/restrict-dome-node-retrieval": "warn"
153+
}
154+
}
155+
```
156+
157+
#### Motivation
158+
We use this rule ourselves to prevent direct dom manipulation in our React projects: https://react.dev/learn/manipulating-the-dom-with-refs
159+
145160
Versioning
146161
----------
147162
This project adheres to [Semantic Versioning](http://semver.org/).
+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*global module*/
2+
/**
3+
* @fileoverview Rule that reports errors if dom-node retrieval methods ("querySelector", "querySelectorAll", "getElementsByTagName", "getElementsByTagNameNS", "getElementsByClassName", "getElementById") are called on document
4+
*
5+
*/
6+
7+
"use strict";
8+
9+
module.exports = {
10+
meta: {
11+
type: "problem",
12+
docs: {
13+
description: "Don't retrieve dom nodes directly, use react-refs to read/manipulate the dom",
14+
category: "Possible Errors",
15+
recommended: true,
16+
},
17+
schema: [],
18+
},
19+
create: function (context) {
20+
const restricted = [ "querySelector", "querySelectorAll", "getElementsByTagName", "getElementsByTagNameNS", "getElementsByClassName", "getElementById" ];
21+
22+
return {
23+
MemberExpression: function (node) {
24+
const isRestrictedDocumentMethodCall = node.object &&
25+
node.object.name === "document" &&
26+
node.property &&
27+
restricted.includes(node.property.name);
28+
29+
const isRestrictedWindowMethodCall = node.object &&
30+
node.object.name === "window" &&
31+
node.property &&
32+
node.property.name === "document" &&
33+
node.parent &&
34+
node.parent.property &&
35+
restricted.includes(node.parent.property.name);
36+
37+
if (isRestrictedDocumentMethodCall === false && isRestrictedWindowMethodCall === false) {
38+
return;
39+
}
40+
41+
const isTestCase = findParent(node, (parent) => {
42+
return (
43+
parent.type === "CallExpression" && (parent.callee.name === "it" || parent.callee.name === "describe")
44+
);
45+
});
46+
47+
if (isTestCase) {
48+
return;
49+
}
50+
51+
context.report({
52+
node: isRestrictedDocumentMethodCall ? node.property : node.parent.property,
53+
message: "Don't retrieve dom nodes directly, use react-refs to read/manipulate the dom",
54+
});
55+
},
56+
};
57+
},
58+
};
59+
60+
function findParent(node, testFn) {
61+
if (testFn(node)) {
62+
return node;
63+
} else if (node.parent) {
64+
return findParent(node.parent, testFn);
65+
}
66+
return null;
67+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/* global require */
2+
3+
"use strict";
4+
5+
const rule = require("../../../lib/rules/restrict-dom-node-retrieval");
6+
const RuleTester = require("eslint").RuleTester;
7+
8+
const ruleTester = new RuleTester({
9+
languageOptions: {
10+
parserOptions: {
11+
ecmaVersion: 6,
12+
ecmaFeatures: {
13+
jsx: true,
14+
},
15+
},
16+
},
17+
});
18+
19+
const ERROR_MSG = "Don't retrieve dom nodes directly, use react-refs to read/manipulate the dom";
20+
21+
ruleTester.run("restrict-dom-node-retrieval", rule, {
22+
valid: [
23+
{
24+
code: `const MyComponent = () => {
25+
return <div>valid</div>
26+
}`,
27+
},
28+
{
29+
code: `function MyComponent() {
30+
return <div>valid</div>
31+
}`,
32+
},
33+
{
34+
code: `it("should be allowed in a test", () => {
35+
const element = document.querySelector(".class");
36+
});`,
37+
},
38+
{
39+
code: `it("should be allowed in a test", () => {
40+
const element = window.document.querySelector(".class");
41+
});`,
42+
},
43+
{
44+
code: `it("should be allowed in a test", () => {
45+
const element = document.querySelectorAll(".class");
46+
});`,
47+
},
48+
{
49+
code: `it("should be allowed in a test", () => {
50+
const element = window.document.querySelectorAll(".class");
51+
});`,
52+
},
53+
{
54+
code: `it("should be allowed in a test", () => {
55+
const element = document.getElementsByTagName("div");
56+
});`,
57+
},
58+
{
59+
code: `it("should be allowed in a test", () => {
60+
const element = window.document.getElementsByTagName("div");
61+
});`,
62+
},
63+
{
64+
code: `it("should be allowed in a test", () => {
65+
const element = document.getElementsByTagNameNS("http://www.w3.org/1999/xhtml", "div");
66+
});`,
67+
},
68+
{
69+
code: `it("should be allowed in a test", () => {
70+
const element = window.document.getElementsByTagNameNS("http://www.w3.org/1999/xhtml", "div");
71+
});`,
72+
},
73+
{
74+
code: `it("should be allowed in a test", () => {
75+
const element = document.getElementsByClassName("class");
76+
});`,
77+
},
78+
{
79+
code: `it("should be allowed in a test", () => {
80+
const element = window.document.getElementsByClassName("class");
81+
});`,
82+
},
83+
{
84+
code: `it("should be allowed in a test", () => {
85+
const element = document.getElementById("id");
86+
});`,
87+
},
88+
{
89+
code: `it("should be allowed in a test", () => {
90+
const element = window.document.getElementById("id");
91+
});`,
92+
},
93+
],
94+
invalid: [
95+
{
96+
code: `element = document.querySelector(".class")`,
97+
errors: [ { message: ERROR_MSG, type: "CallExpression" } ],
98+
},
99+
{
100+
code: `element = window.document.querySelector(".class")`,
101+
errors: [ { message: ERROR_MSG, type: "CallExpression" } ],
102+
},
103+
{
104+
code: `element = document.querySelectorAll(".class")`,
105+
errors: [ { message: ERROR_MSG, type: "CallExpression" } ],
106+
},
107+
{
108+
code: `element = window.document.querySelectorAll(".class")`,
109+
errors: [ { message: ERROR_MSG, type: "CallExpression" } ],
110+
},
111+
{
112+
code: `element = document.getElementsByTagName("div")`,
113+
errors: [ { message: ERROR_MSG, type: "CallExpression" } ],
114+
},
115+
{
116+
code: `element = window.document.getElementsByTagName("div")`,
117+
errors: [ { message: ERROR_MSG, type: "CallExpression" } ],
118+
},
119+
{
120+
code: `element = document.getElementsByTagNameNS("http://www.w3.org/1999/xhtml", "div")`,
121+
errors: [ { message: ERROR_MSG, type: "CallExpression" } ],
122+
},
123+
{
124+
code: `element = window.document.getElementsByTagNameNS("http://www.w3.org/1999/xhtml", "div")`,
125+
errors: [ { message: ERROR_MSG, type: "CallExpression" } ],
126+
},
127+
{
128+
code: `element = document.getElementsByClassName("class")`,
129+
errors: [ { message: ERROR_MSG, type: "CallExpression" } ],
130+
},
131+
{
132+
code: `element = window.document.getElementsByClassName("class")`,
133+
errors: [ { message: ERROR_MSG, type: "CallExpression" } ],
134+
},
135+
{
136+
code: `element = document.getElementById("id")`,
137+
errors: [ { message: ERROR_MSG, type: "CallExpression" } ],
138+
},
139+
{
140+
code: `element = window.document.getElementById("id")`,
141+
errors: [ { message: ERROR_MSG, type: "CallExpression" } ],
142+
},
143+
],
144+
});

0 commit comments

Comments
 (0)