Skip to content

Commit 01e688a

Browse files
Copilotmattcosta7
andcommitted
Implement prefer-observers rule enhancements for viewport detection
- Allow window.resize and window.orientationchange listeners (viewport detection) - Continue flagging element-level resize listeners (suggest ResizeObserver) - Flag ResizeObserver on document.documentElement/body (suggest window.resize + orientationchange) - Add variable tracking to detect indirect document root observations - Add comprehensive test cases covering all scenarios Co-authored-by: mattcosta7 <8616962+mattcosta7@users.noreply.github.com>
1 parent 3811313 commit 01e688a

File tree

2 files changed

+153
-1
lines changed

2 files changed

+153
-1
lines changed

lib/rules/prefer-observers.js

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,31 @@ const observerMap = {
44
scroll: 'IntersectionObserver',
55
resize: 'ResizeObserver',
66
}
7+
8+
/**
9+
* Checks if a node refers to the window object
10+
*/
11+
function isWindowObject(node) {
12+
return node.type === 'Identifier' && node.name === 'window'
13+
}
14+
15+
/**
16+
* Checks if a node refers to document.documentElement or document.body
17+
*/
18+
function isDocumentRoot(node) {
19+
if (node.type === 'MemberExpression') {
20+
if (
21+
node.object.type === 'Identifier' &&
22+
node.object.name === 'document' &&
23+
node.property.type === 'Identifier' &&
24+
(node.property.name === 'documentElement' || node.property.name === 'body')
25+
) {
26+
return true
27+
}
28+
}
29+
return false
30+
}
31+
732
export default {
833
meta: {
934
type: 'suggestion',
@@ -15,21 +40,68 @@ export default {
1540
schema: [],
1641
messages: {
1742
avoid: 'Avoid using "{{name}}" event listener. Consider using {{observer}} instead',
43+
avoidResizeObserverOnRoot:
44+
'Avoid using ResizeObserver on document root elements. Consider using window.addEventListener("resize", ...) combined with window.addEventListener("orientationchange", ...) for viewport detection instead',
1845
},
1946
},
2047

2148
create(context) {
49+
// Track variables that reference document.documentElement or document.body
50+
const documentRootVariables = new Map()
51+
2252
return {
23-
['CallExpression[callee.property.name="addEventListener"]']: function (node) {
53+
// Track variable declarations that assign document.documentElement or document.body
54+
VariableDeclarator(node) {
55+
if (node.init && isDocumentRoot(node.init) && node.id.type === 'Identifier') {
56+
documentRootVariables.set(node.id.name, node.init)
57+
}
58+
},
59+
60+
// Check addEventListener calls
61+
['CallExpression[callee.property.name="addEventListener"]'](node) {
2462
const [name] = node.arguments
2563
if (name.type !== 'Literal') return
2664
if (!(name.value in observerMap)) return
65+
66+
// Allow window.addEventListener("resize", ...) and window.addEventListener("orientationchange", ...)
67+
if (node.callee.object && isWindowObject(node.callee.object)) {
68+
if (name.value === 'resize') return
69+
}
70+
2771
context.report({
2872
node,
2973
messageId: 'avoid',
3074
data: {name: name.value, observer: observerMap[name.value]},
3175
})
3276
},
77+
78+
// Check ResizeObserver.observe() calls
79+
['CallExpression[callee.property.name="observe"]'](node) {
80+
// Check if this is a ResizeObserver instance
81+
const callee = node.callee
82+
if (!callee.object) return
83+
84+
// Get the first argument (the element being observed)
85+
const [observedElement] = node.arguments
86+
if (!observedElement) return
87+
88+
// Check if observing document.documentElement or document.body directly
89+
if (isDocumentRoot(observedElement)) {
90+
context.report({
91+
node,
92+
messageId: 'avoidResizeObserverOnRoot',
93+
})
94+
return
95+
}
96+
97+
// Check if observing a variable that was assigned document.documentElement or document.body
98+
if (observedElement.type === 'Identifier' && documentRootVariables.has(observedElement.name)) {
99+
context.report({
100+
node,
101+
messageId: 'avoidResizeObserverOnRoot',
102+
})
103+
}
104+
},
33105
}
34106
},
35107
}

tests/prefer-observers.js

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,21 @@ ruleTester.run('prefer-observers', rule, {
88
{
99
code: 'document.addEventListener("touchstart", function(event) {})',
1010
},
11+
// window.resize is valid for viewport detection
12+
{
13+
code: 'window.addEventListener("resize", function(event) {})',
14+
},
15+
// window.orientationchange is valid for viewport detection
16+
{
17+
code: 'window.addEventListener("orientationchange", function(event) {})',
18+
},
19+
// ResizeObserver on regular elements is valid
20+
{
21+
code: 'const observer = new ResizeObserver(() => {}); observer.observe(someElement)',
22+
},
23+
{
24+
code: 'const observer = new ResizeObserver(() => {}); const el = document.querySelector(".item"); observer.observe(el)',
25+
},
1126
],
1227
invalid: [
1328
{
@@ -28,5 +43,70 @@ ruleTester.run('prefer-observers', rule, {
2843
},
2944
],
3045
},
46+
// element.resize should be flagged
47+
{
48+
code: 'element.addEventListener("resize", function(event) {})',
49+
errors: [
50+
{
51+
message: 'Avoid using "resize" event listener. Consider using ResizeObserver instead',
52+
type: 'CallExpression',
53+
},
54+
],
55+
},
56+
// ResizeObserver on document.documentElement should be flagged
57+
{
58+
code: 'const observer = new ResizeObserver(() => {}); observer.observe(document.documentElement)',
59+
errors: [
60+
{
61+
message:
62+
'Avoid using ResizeObserver on document root elements. Consider using window.addEventListener("resize", ...) combined with window.addEventListener("orientationchange", ...) for viewport detection instead',
63+
type: 'CallExpression',
64+
},
65+
],
66+
},
67+
// ResizeObserver on document.body should be flagged
68+
{
69+
code: 'const observer = new ResizeObserver(() => {}); observer.observe(document.body)',
70+
errors: [
71+
{
72+
message:
73+
'Avoid using ResizeObserver on document root elements. Consider using window.addEventListener("resize", ...) combined with window.addEventListener("orientationchange", ...) for viewport detection instead',
74+
type: 'CallExpression',
75+
},
76+
],
77+
},
78+
// Inline ResizeObserver with document.documentElement should be flagged
79+
{
80+
code: 'new ResizeObserver(() => {}).observe(document.documentElement)',
81+
errors: [
82+
{
83+
message:
84+
'Avoid using ResizeObserver on document root elements. Consider using window.addEventListener("resize", ...) combined with window.addEventListener("orientationchange", ...) for viewport detection instead',
85+
type: 'CallExpression',
86+
},
87+
],
88+
},
89+
// Variable tracking: document.documentElement assigned to variable
90+
{
91+
code: 'const el = document.documentElement; observer.observe(el)',
92+
errors: [
93+
{
94+
message:
95+
'Avoid using ResizeObserver on document root elements. Consider using window.addEventListener("resize", ...) combined with window.addEventListener("orientationchange", ...) for viewport detection instead',
96+
type: 'CallExpression',
97+
},
98+
],
99+
},
100+
// Variable tracking: document.body assigned to variable
101+
{
102+
code: 'const root = document.body; observer.observe(root)',
103+
errors: [
104+
{
105+
message:
106+
'Avoid using ResizeObserver on document root elements. Consider using window.addEventListener("resize", ...) combined with window.addEventListener("orientationchange", ...) for viewport detection instead',
107+
type: 'CallExpression',
108+
},
109+
],
110+
},
31111
],
32112
})

0 commit comments

Comments
 (0)