@@ -5,6 +5,8 @@ import { isFunctionLike } from "../../utils/is-function-like.js";
55import { isNodeOfType } from "../../utils/is-node-of-type.js" ;
66import { getImportedName } from "../../utils/get-imported-name.js" ;
77import type { EsTreeNodeOfType } from "../../utils/es-tree-node-of-type.js" ;
8+ import { getStaticPropertyName } from "../../utils/get-static-property-name.js" ;
9+ import { stripParenExpression } from "../../utils/strip-paren-expression.js" ;
810import { walkAst } from "../../utils/walk-ast.js" ;
911
1012// Libraries that position or attach to freshly committed DOM — the
@@ -38,6 +40,18 @@ const DOM_MEASUREMENT_NAMES: ReadonlySet<string> = new Set([
3840const MEASUREMENT_HELPER_CALLEE_PATTERN =
3941 / ^ (?: g e t | m e a s u r e | r e a d ) \w * (?: W i d t h | H e i g h t | R e c t | R e c t s | S i z e | B o u n d s | P o s i t i o n ) $ / ;
4042
43+ const IMPERATIVE_DOM_MUTATION_NAMES : ReadonlySet < string > = new Set ( [
44+ "blur" ,
45+ "focus" ,
46+ "restoreSelection" ,
47+ "scroll" ,
48+ "scrollBy" ,
49+ "scrollIntoView" ,
50+ "scrollTo" ,
51+ "setRangeText" ,
52+ "setSelectionRange" ,
53+ ] ) ;
54+
4155const subtreeReadsDomMeasurement = ( root : EsTreeNode | null | undefined ) : boolean => {
4256 if ( ! root ) return false ;
4357 let found = false ;
@@ -61,17 +75,14 @@ const subtreeReadsDomMeasurement = (root: EsTreeNode | null | undefined): boolea
6175 return found ;
6276} ;
6377
64- // Local function bindings whose body reads DOM measurements, including
65- // hook-wrapped ones (`const measure = useCallback(() => el.offsetWidth)`).
66- const collectMeasuringFunctionNames = ( program : EsTreeNode ) : Set < string > => {
78+ const collectFunctionNamesMatchingBody = (
79+ program : EsTreeNode ,
80+ matchesBody : ( body : EsTreeNode | null | undefined ) => boolean ,
81+ ) : Set < string > => {
6782 const names = new Set < string > ( ) ;
6883 walkAst ( program , ( child : EsTreeNode ) => {
6984 if ( isNodeOfType ( child , "FunctionDeclaration" ) ) {
70- if (
71- child . id &&
72- isNodeOfType ( child . id , "Identifier" ) &&
73- subtreeReadsDomMeasurement ( child . body )
74- ) {
85+ if ( child . id && isNodeOfType ( child . id , "Identifier" ) && matchesBody ( child . body ) ) {
7586 names . add ( child . id . name ) ;
7687 }
7788 return ;
@@ -86,22 +97,50 @@ const collectMeasuringFunctionNames = (program: EsTreeNode): Set<string> => {
8697 ) {
8798 functionValue = functionValue . arguments ?. [ 0 ] ;
8899 }
89- if (
90- functionValue &&
91- isFunctionLike ( functionValue ) &&
92- subtreeReadsDomMeasurement ( functionValue . body )
93- ) {
100+ if ( functionValue && isFunctionLike ( functionValue ) && matchesBody ( functionValue . body ) ) {
94101 names . add ( child . id . name ) ;
95102 }
96103 } ) ;
97104 return names ;
98105} ;
99106
100- const callsAnyName = ( root : EsTreeNode | null | undefined , names : ReadonlySet < string > ) : boolean => {
101- if ( ! root || names . size === 0 ) return false ;
107+ const collectMeasuringFunctionNames = ( program : EsTreeNode ) : Set < string > =>
108+ collectFunctionNamesMatchingBody ( program , subtreeReadsDomMeasurement ) ;
109+
110+ const subtreeMutatesDomImperatively = ( root : EsTreeNode | null | undefined ) : boolean => {
111+ if ( ! root || isFunctionLike ( root ) ) return false ;
112+ let found = false ;
113+ walkAst ( root , ( child : EsTreeNode ) => {
114+ if ( found ) return false ;
115+ if ( child !== root && isFunctionLike ( child ) ) return false ;
116+ if ( ! isNodeOfType ( child , "CallExpression" ) ) return ;
117+ const callee = stripParenExpression ( child . callee ) ;
118+ const propertyName = isNodeOfType ( callee , "MemberExpression" )
119+ ? getStaticPropertyName ( callee )
120+ : null ;
121+ if ( propertyName !== null && IMPERATIVE_DOM_MUTATION_NAMES . has ( propertyName ) ) {
122+ found = true ;
123+ return false ;
124+ }
125+ } ) ;
126+ return found ;
127+ } ;
128+
129+ const collectImperativeDomFunctionNames = ( program : EsTreeNode ) : Set < string > =>
130+ collectFunctionNamesMatchingBody ( program , subtreeMutatesDomImperatively ) ;
131+
132+ const callsAnyName = (
133+ root : EsTreeNode | null | undefined ,
134+ names : ReadonlySet < string > ,
135+ shouldSkipNestedFunctions = false ,
136+ ) : boolean => {
137+ if ( ! root || names . size === 0 || ( shouldSkipNestedFunctions && isFunctionLike ( root ) ) ) {
138+ return false ;
139+ }
102140 let found = false ;
103141 walkAst ( root , ( child : EsTreeNode ) => {
104142 if ( found ) return false ;
143+ if ( shouldSkipNestedFunctions && child !== root && isFunctionLike ( child ) ) return false ;
105144 if (
106145 isNodeOfType ( child , "CallExpression" ) &&
107146 isNodeOfType ( child . callee , "Identifier" ) &&
@@ -113,6 +152,45 @@ const callsAnyName = (root: EsTreeNode | null | undefined, names: ReadonlySet<st
113152 return found ;
114153} ;
115154
155+ const isFollowedByImperativeDomMutation = (
156+ call : EsTreeNode ,
157+ imperativeDomFunctionNames : ReadonlySet < string > ,
158+ ) : boolean => {
159+ let statement : EsTreeNode = call ;
160+ let parent = statement . parent ;
161+ while ( parent ) {
162+ const statements =
163+ isNodeOfType ( parent , "BlockStatement" ) ||
164+ isNodeOfType ( parent , "Program" ) ||
165+ isNodeOfType ( parent , "StaticBlock" )
166+ ? parent . body
167+ : isNodeOfType ( parent , "SwitchCase" )
168+ ? parent . consequent
169+ : null ;
170+ if ( statements ) {
171+ const statementIndex = statements . findIndex (
172+ ( siblingStatement ) => siblingStatement === statement ,
173+ ) ;
174+ if ( statementIndex >= 0 ) {
175+ const nextStatement = statements [ statementIndex + 1 ] ;
176+ return (
177+ subtreeMutatesDomImperatively ( nextStatement ) ||
178+ callsAnyName ( nextStatement , imperativeDomFunctionNames , true )
179+ ) ;
180+ }
181+ }
182+ if (
183+ isFunctionLike ( parent ) ||
184+ ( parent . type . endsWith ( "Statement" ) && ! isNodeOfType ( parent , "ExpressionStatement" ) )
185+ ) {
186+ return false ;
187+ }
188+ statement = parent ;
189+ parent = parent . parent ;
190+ }
191+ return false ;
192+ } ;
193+
116194const isInsideStartViewTransition = ( node : EsTreeNode ) : boolean => {
117195 let cursor : EsTreeNode | null | undefined = node . parent ;
118196 while ( cursor ) {
@@ -172,6 +250,7 @@ const importsImperativeDomLibrary = (program: EsTreeNode): boolean => {
172250// diagnostic.
173251const hasExemptFlushSyncCall = ( program : EsTreeNode , localName : string ) : boolean => {
174252 const measuringFunctionNames = collectMeasuringFunctionNames ( program ) ;
253+ const imperativeDomFunctionNames = collectImperativeDomFunctionNames ( program ) ;
175254 let exempt = false ;
176255 walkAst ( program , ( child : EsTreeNode ) => {
177256 if ( exempt ) return false ;
@@ -184,7 +263,8 @@ const hasExemptFlushSyncCall = (program: EsTreeNode, localName: string): boolean
184263 }
185264 if (
186265 isInsideStartViewTransition ( child ) ||
187- enclosingFunctionChainReadsMeasurement ( child , measuringFunctionNames )
266+ enclosingFunctionChainReadsMeasurement ( child , measuringFunctionNames ) ||
267+ isFollowedByImperativeDomMutation ( child , imperativeDomFunctionNames )
188268 ) {
189269 exempt = true ;
190270 return false ;
0 commit comments