@@ -82,22 +82,83 @@ export function parseVariables(variables) {
8282
8383 // Step 4 - Annotate locally-provided variables with usedBy information
8484 for ( const { variableName, targetName, origin } of localUsages ) {
85- const variable = variables . find ( v =>
86- v . name === variableName && v . scope === origin
87- ) ;
85+ const variable = findNearestScopedVariable ( variables , variableName , origin ) ;
8886
89- if ( variable ) {
90- if ( ! variable . usedBy ) {
91- variable . usedBy = [ ] ;
87+ if ( ! variable ) {
88+ continue ;
89+ }
90+
91+ // Keep existing behavior for same-origin mappings (`usedBy: [ 'targetVar' ]`)
92+ // and use the consuming element for ancestor-scoped variables.
93+ const usage = variable . scope === origin ? targetName : origin ;
94+
95+ if ( ! variable . usedBy ) {
96+ variable . usedBy = [ ] ;
97+ }
98+
99+ if ( ! hasUsage ( variable . usedBy , usage ) ) {
100+ variable . usedBy . push ( usage ) ;
101+ }
102+ }
103+
104+ // Step 5 - Bridge consumed usages back to uniquely scoped declarations
105+ annotateConsumedUsagesToScopedVariables ( variables , consumedVariables ) ;
106+
107+ return { resolvedVariables, consumedVariables } ;
108+ }
109+
110+ function annotateConsumedUsagesToScopedVariables ( variables , consumedVariables ) {
111+ for ( const consumedVariable of consumedVariables ) {
112+ const scopedCandidates = variables . filter ( v => v . name === consumedVariable . name && v . scope ) ;
113+
114+ if ( scopedCandidates . length !== 1 ) {
115+ continue ;
116+ }
117+
118+ const scopedVariable = scopedCandidates [ 0 ] ;
119+
120+ if ( ! scopedVariable . usedBy ) {
121+ scopedVariable . usedBy = [ ] ;
122+ }
123+
124+ for ( const usage of consumedVariable . usedBy || [ ] ) {
125+ if ( ! usage || ! usage . id ) {
126+ continue ;
92127 }
93128
94- if ( ! variable . usedBy . includes ( targetName ) ) {
95- variable . usedBy . push ( targetName ) ;
129+ if ( ! hasUsage ( scopedVariable . usedBy , usage ) ) {
130+ scopedVariable . usedBy . push ( usage ) ;
96131 }
97132 }
98133 }
134+ }
99135
100- return { resolvedVariables, consumedVariables } ;
136+ function findNearestScopedVariable ( variables , variableName , origin ) {
137+ const scopes = [ origin , ...getParents ( origin ) ] ;
138+
139+ for ( const scope of scopes ) {
140+ const variable = variables . find ( v => v . name === variableName && v . scope === scope ) ;
141+
142+ if ( variable ) {
143+ return variable ;
144+ }
145+ }
146+
147+ return null ;
148+ }
149+
150+ function hasUsage ( usages , usage ) {
151+ return usages . some ( existing => {
152+ if ( existing === usage ) {
153+ return true ;
154+ }
155+
156+ if ( typeof existing === 'string' && typeof usage === 'string' ) {
157+ return existing === usage ;
158+ }
159+
160+ return existing && usage && existing . id && usage . id && existing . id === usage . id ;
161+ } ) ;
101162}
102163
103164/**
@@ -669,21 +730,12 @@ function buildConsumedVariables(analysisResults) {
669730 const inputMappingTargetsCache = { } ;
670731
671732 for ( const { origin, targetName, inputs, expressionType } of analysisResults ) {
672-
673- if ( ! inputMappingTargetsCache [ origin . id ] ) {
674- inputMappingTargetsCache [ origin . id ] = getInputMappingTargetNames ( origin ) ;
675- }
676- const orderedTargets = inputMappingTargetsCache [ origin . id ] ;
677-
678- // Input mappings are order-sensitive: only earlier targets are available.
679- // Scripts can reference all input mapping targets.
680- let availableLocalTargets ;
681- if ( expressionType === 'input-mapping' ) {
682- const targetIndex = orderedTargets . indexOf ( targetName ) ;
683- availableLocalTargets = new Set ( orderedTargets . slice ( 0 , targetIndex ) ) ;
684- } else {
685- availableLocalTargets = new Set ( orderedTargets ) ;
686- }
733+ const availableLocalTargets = getAvailableLocalTargets (
734+ origin ,
735+ expressionType ,
736+ targetName ,
737+ inputMappingTargetsCache
738+ ) ;
687739
688740 for ( const inputVar of inputs ) {
689741
@@ -735,4 +787,31 @@ function getInputMappingTargetNames(origin) {
735787 return ioMapping . inputParameters . map ( p => p . target ) ;
736788}
737789
790+ function getAvailableLocalTargets ( origin , expressionType , targetName , inputMappingTargetsCache ) {
791+ const availableTargets = new Set ( ) ;
792+ const scopes = [ origin , ...getParents ( origin ) ] ;
793+
794+ for ( const scope of scopes ) {
795+ if ( ! inputMappingTargetsCache [ scope . id ] ) {
796+ inputMappingTargetsCache [ scope . id ] = getInputMappingTargetNames ( scope ) ;
797+ }
798+
799+ const orderedTargets = inputMappingTargetsCache [ scope . id ] ;
800+
801+ // Input mappings on the current element are order-sensitive; ancestor
802+ // mappings are already established and fully available.
803+ if ( scope === origin && expressionType === 'input-mapping' ) {
804+ const targetIndex = orderedTargets . indexOf ( targetName ) ;
805+ const availableOwnTargets = targetIndex === - 1 ? orderedTargets : orderedTargets . slice ( 0 , targetIndex ) ;
806+
807+ availableOwnTargets . forEach ( target => availableTargets . add ( target ) ) ;
808+ continue ;
809+ }
810+
811+ orderedTargets . forEach ( target => availableTargets . add ( target ) ) ;
812+ }
813+
814+ return availableTargets ;
815+ }
816+
738817
0 commit comments