99 symbolName ,
1010 traverse ,
1111} from '#design-diff/ast'
12+ import { mutations } from '#design-diff/mutations'
1213import { previewValue } from '#design-diff/report'
1314import type { SourceTree } from '#design-diff/source'
1415import type { Data , Evidence } from '#design-diff/types'
@@ -160,8 +161,7 @@ export class Resolver {
160161 )
161162 )
162163 return true
163- // Canvas operations are specific; ambiguous DOM methods require a typed/ref receiver
164- // or another established DOM operation in the same function.
164+ /** Canvas operations are specific; ambiguous DOM methods require a typed/ref receiver or another established DOM operation in the same function. */
165165 if (
166166 / ^ (?: f i l l R e c t | s t r o k e R e c t | d r a w I m a g e | f i l l T e x t | s t r o k e T e x t | a d d C o l o r S t o p | g e t C o n t e x t ) $ / . test ( method )
167167 )
@@ -281,6 +281,28 @@ export class Resolver {
281281 }
282282 }
283283
284+ /** Conditions around writes can change the visible collection even when each value is unchanged. */
285+ private mutationValue ( write : NodePath , file : string , depth : number ) : Data {
286+ const conditions : Data [ ] = [ ]
287+ for (
288+ let parent = write . parentPath ;
289+ parent && ! parent . isFunction ( ) ;
290+ parent = parent . parentPath
291+ ) {
292+ if (
293+ parent . isIfStatement ( ) ||
294+ parent . isConditionalExpression ( ) ||
295+ parent . isWhileStatement ( ) ||
296+ parent . isDoWhileStatement ( ) ||
297+ parent . isForStatement ( )
298+ )
299+ conditions . push ( this . value ( child ( parent , 'test' ) , file , depth + 1 ) )
300+ if ( parent . isForOfStatement ( ) || parent . isForInStatement ( ) )
301+ conditions . push ( this . value ( child ( parent , 'right' ) , file , depth + 1 ) )
302+ }
303+ return { value : this . value ( write , file , depth + 1 ) , conditions }
304+ }
305+
284306 private currentFile = ''
285307
286308 /** Select a property before expanding siblings, including createEnv's schema convention. */
@@ -298,8 +320,18 @@ export class Resolver {
298320 return this . selected ( child ( path , 'expression' ) , key , file , depth + 1 , seen )
299321 if ( path . isIdentifier ( ) ) {
300322 const binding = path . scope . getBinding ( path . node . name )
301- if ( binding ?. constant && binding . path . isVariableDeclarator ( ) )
302- return this . selected ( child ( binding . path , 'init' ) , key , file , depth + 1 , seen )
323+ if ( binding ?. constant && binding . path . isVariableDeclarator ( ) ) {
324+ const initial = this . selected ( child ( binding . path , 'init' ) , key , file , depth + 1 , seen )
325+ const writes = mutations ( binding , key )
326+ if ( initial !== undefined && writes . length ) {
327+ this . unresolved . add ( 'Writes to this object property feed rendering' )
328+ return {
329+ $property : initial ,
330+ writes : writes . map ( ( write ) => this . mutationValue ( write , file , depth + 1 ) ) ,
331+ }
332+ }
333+ return initial
334+ }
303335 if ( binding ?. path . isImportSpecifier ( ) || binding ?. path . isImportDefaultSpecifier ( ) ) {
304336 const declaration = binding . path . parentPath
305337 if ( declaration . isImportDeclaration ( ) ) {
@@ -459,8 +491,7 @@ export class Resolver {
459491 t . traverseFast ( path . node , ( node ) => {
460492 if ( t . isJSXElement ( node ) || t . isJSXFragment ( node ) ) renders = true
461493 } )
462- // JSX definitions are extracted at their own source. Expanding component bodies
463- // again at every reference duplicates evidence and confuses refactors with prop changes.
494+ /** JSX definitions are extracted at their own source. Expanding component bodies again at every reference duplicates evidence and confuses refactors with prop changes. */
464495 if ( renders ) return { $renderFunction : { file, symbol : symbolName ( path ) } }
465496 const body = child ( path , 'body' )
466497 if ( ! body . isBlockStatement ( ) )
@@ -803,6 +834,14 @@ export class Resolver {
803834 writes : binding . constantViolations . map ( ( violation ) => fingerprint ( violation . node ) ) ,
804835 }
805836 }
837+ const writes = mutations ( binding )
838+ if ( writes . length && binding . path . isVariableDeclarator ( ) ) {
839+ this . unresolved . add ( 'Collection or object mutations feed this visual input' )
840+ return {
841+ $state : this . value ( child ( binding . path , 'init' ) , file , depth + 1 ) ,
842+ writes : writes . map ( ( write ) => this . mutationValue ( write , file , depth + 1 ) ) ,
843+ }
844+ }
806845 const bound = binding . path
807846 const parameter = this . parameter ( bound , node . name , file , depth )
808847 if ( parameter !== undefined ) return parameter
@@ -934,6 +973,8 @@ export class Resolver {
934973 return { $condition : test , then : read ( 'consequent' ) , else : read ( 'alternate' ) }
935974 }
936975 if ( t . isFunction ( node ) ) return this . functionValue ( path , file , depth )
976+ if ( t . isAssignmentExpression ( node ) )
977+ return { $assignment : node . operator , target : fingerprint ( node . left ) , value : read ( 'right' ) }
937978 if ( t . isNewExpression ( node ) ) return { $new : read ( 'callee' ) , arguments : readList ( 'arguments' ) }
938979 if ( t . isTaggedTemplateExpression ( node ) ) return { $tag : read ( 'tag' ) , template : read ( 'quasi' ) }
939980 if ( t . isAwaitExpression ( node ) ) return { $await : read ( 'argument' ) }
0 commit comments