11import { getBusinessObject , is } from 'bpmn-js/lib/util/ModelUtil' ;
2+ import { has } from 'min-dash' ;
23import CachedValue from './util/CachedValue' ;
34import { mergeList } from './util/listUtil' ;
45import { getParents } from './util/elementsUtil' ;
@@ -15,8 +16,9 @@ import { getParents } from './util/elementsUtil';
1516
1617/**
1718 * @typedef {AdditionalVariable } ProcessVariable
18- * @property {Array<ModdleElement> } origin
19- * @property {ModdleElement } scope
19+ * @property {Array<ModdleElement> } [origin]
20+ * @property {Array<ModdleElement> } [usedBy]
21+ * @property {ModdleElement } [scope]
2022 * @property {Array<Object> } provider
2123 */
2224
@@ -159,12 +161,26 @@ export class BaseVariableResolver {
159161 variables . forEach ( variable => {
160162 const existingVariable = mergedVariables . find ( v =>
161163 v . name === variable . name && v . scope === variable . scope
164+ && ( v . scope || ! v . usedBy ) && ( variable . scope || ! variable . usedBy )
162165 ) ;
163166
164167 if ( existingVariable ) {
165168 merge ( 'origin' , existingVariable , variable ) ;
166169 merge ( 'provider' , existingVariable , variable ) ;
167170 mergeEntries ( existingVariable , variable ) ;
171+
172+ // Preserve usedBy from either side during merge
173+ if ( variable . usedBy ) {
174+ if ( ! existingVariable . usedBy ) {
175+ existingVariable . usedBy = [ ...variable . usedBy ] ;
176+ } else {
177+ variable . usedBy . forEach ( target => {
178+ if ( ! existingVariable . usedBy . includes ( target ) ) {
179+ existingVariable . usedBy . push ( target ) ;
180+ }
181+ } ) ;
182+ }
183+ }
168184 } else {
169185 mergedVariables . push ( variable ) ;
170186 }
@@ -247,40 +263,86 @@ export class BaseVariableResolver {
247263 }
248264
249265 /**
250- * Returns all variables in the scope of the given element.
266+ * Returns variables for the given element.
267+ *
268+ * Default behavior returns written variables in scope (same as previous
269+ * `getVariablesForElement` behavior).
270+ *
271+ * If one of `read` / `written` is set explicitly, only the requested types
272+ * are returned.
251273 *
252274 * @async
253275 * @param {ModdleElement } element
254- * @returns {Array<ProcessVariable> } variables
276+ * @param {Object } [options]
277+ * @param {boolean } [options.read]
278+ * @param {boolean } [options.written]
279+ * @param {boolean } [options.local=true] Include local read variables
280+ * @returns {Promise<Array<ProcessVariable>> } variables
255281 */
256- async getVariablesForElement ( element ) {
282+ async getVariablesForElement ( element , options = { } ) {
257283 const bo = getBusinessObject ( element ) ;
258284
285+ const {
286+ read,
287+ written,
288+ local
289+ } = normalizeGetVariablesForElementOptions ( options ) ;
290+
291+ const writtenVariables = written
292+ ? await this . _getWrittenVariablesForElement ( bo )
293+ : [ ] ;
294+
295+ const readVariables = read
296+ ? await this . _getReadVariablesForElement ( bo , { local } )
297+ : [ ] ;
298+
299+ return [ ...writtenVariables , ...readVariables ] ;
300+ }
301+
302+ async _getWrittenVariablesForElement ( bo ) {
259303 const root = getRootElement ( bo ) ;
260304 const allVariables = await this . getProcessVariables ( root ) ;
261305
262- // (1) get variables for given scope
263- var scopeVariables = allVariables . filter ( function ( variable ) {
264- return variable . scope . id === bo . id ;
265- } ) ;
306+ const parentIds = new Set ( getParents ( bo ) . map ( parent => parent . id ) ) ;
307+ const scopeVariables = [ ] ;
308+ const parentsScopeVariables = [ ] ;
309+ const leakedVariables = [ ] ;
266310
267- // (2) get variables for parent scopes
268- var parents = getParents ( bo ) ;
311+ // Categorize in one pass to avoid repeated full-array scans.
312+ for ( const variable of allVariables ) {
313+ const scope = variable . scope ;
269314
270- var parentsScopeVariables = allVariables . filter ( function ( variable ) {
271- return parents . find ( function ( parent ) {
272- return parent . id === variable . scope . id ;
273- } ) ;
274- } ) ;
315+ if ( ! scope ) {
316+ continue ;
317+ }
318+
319+ if ( scope . id === bo . id ) {
320+ scopeVariables . push ( variable ) ;
321+ continue ;
322+ }
323+
324+ if ( parentIds . has ( scope . id ) ) {
325+ parentsScopeVariables . push ( variable ) ;
326+ continue ;
327+ }
328+
329+ // Include descendant-scoped variables that are used outside their own
330+ // scope but still within the current scope (cross-scope leak).
331+ if ( isElementInScope ( scope , bo ) && isUsedInScope ( variable , bo ) && isUsedOutsideOwnScope ( variable ) ) {
332+ leakedVariables . push ( variable ) ;
333+ }
334+ }
275335
276- const reversedVariables = [ ...scopeVariables , ...parentsScopeVariables ] . reverse ( ) ;
336+ const reversedVariables = [ ...leakedVariables , ... scopeVariables , ...parentsScopeVariables ] . reverse ( ) ;
277337
278338 const seenNames = new Set ( ) ;
279339
280- return reversedVariables . filter ( variable => {
340+ const deduplicatedVariables = reversedVariables . filter ( variable => {
341+
342+ const provider = variable . provider || [ ] ;
281343
282344 // if external variable, keep
283- if ( variable . provider . find ( extractor => extractor !== this . _baseExtractor ) ) {
345+ if ( provider . some ( extractor => extractor !== this . _baseExtractor ) ) {
284346 return true ;
285347 }
286348
@@ -293,6 +355,37 @@ export class BaseVariableResolver {
293355
294356 return false ;
295357 } ) ;
358+
359+ return deduplicatedVariables . map ( variable => {
360+ if ( ! variable . usedBy || ! Array . isArray ( variable . usedBy ) ) {
361+ return variable ;
362+ }
363+
364+ const usedBy = filterUsedByForElement ( variable , bo ) ;
365+
366+ if ( usedBy . length === variable . usedBy . length ) {
367+ return variable ;
368+ }
369+
370+ return {
371+ ...variable ,
372+ usedBy : usedBy . length ? usedBy : undefined
373+ } ;
374+ } ) ;
375+ }
376+
377+ async _getReadVariablesForElement ( element , options = { } ) {
378+ const root = getRootElement ( element ) ;
379+ const allVariablesByRoot = await this . parsedVariables . get ( ) ;
380+ const allVariables = allVariablesByRoot [ root . id ] || [ ] ;
381+
382+ if ( ! options . local ) {
383+ return allVariables . filter ( variable => isConsumedByElement ( variable , element ) ) ;
384+ }
385+
386+ return allVariables . filter ( variable => {
387+ return isConsumedByElement ( variable , element ) || isLocalReadByElement ( variable , element ) ;
388+ } ) ;
296389 }
297390
298391 _getScope ( element , containerElement , variableName , checkYourself ) {
@@ -396,4 +489,117 @@ function cloneVariable(variable) {
396489 }
397490
398491 return newVariable ;
399- }
492+ }
493+
494+ function isUsedInScope ( variable , scopeElement ) {
495+ if ( ! variable . usedBy || ! Array . isArray ( variable . usedBy ) ) {
496+ return false ;
497+ }
498+
499+ return variable . usedBy . some ( usedBy => isElementInScope ( usedBy , scopeElement ) ) ;
500+ }
501+
502+ function isElementInScope ( element , scopeElement ) {
503+ if ( ! element || ! element . id || ! scopeElement || ! scopeElement . id ) {
504+ return false ;
505+ }
506+
507+ if ( element . id === scopeElement . id ) {
508+ return true ;
509+ }
510+
511+ return getParents ( element ) . some ( parent => parent . id === scopeElement . id ) ;
512+ }
513+
514+ function isUsedOutsideOwnScope ( variable ) {
515+ if ( ! variable . scope || ! Array . isArray ( variable . usedBy ) ) {
516+ return false ;
517+ }
518+
519+ return variable . usedBy . some ( usedBy => {
520+ return usedBy && usedBy . id && ! isElementInScope ( usedBy , variable . scope ) ;
521+ } ) ;
522+ }
523+
524+ function filterUsedByForElement ( variable , element ) {
525+ const names = variable . usedBy . filter ( usage => typeof usage === 'string' ) ;
526+ const elements = variable . usedBy . filter ( usage => usage && usage . id ) ;
527+
528+ if ( ! variable . scope ) {
529+ return [ ...names , ...elements . filter ( usage => isElementInScope ( usage , element ) ) ] ;
530+ }
531+
532+ // Querying the variable's own scope: show local consumers.
533+ if ( element . id === variable . scope . id ) {
534+ return [
535+ ...names ,
536+ ...elements . filter ( usage => isElementInScope ( usage , variable . scope ) )
537+ ] ;
538+ }
539+
540+ // Querying an ancestor scope: show consumers outside the variable's own scope.
541+ if ( isElementInScope ( variable . scope , element ) ) {
542+ return [
543+ ...names ,
544+ ...elements . filter ( usage =>
545+ isElementInScope ( usage , element )
546+ && ! isElementInScope ( usage , variable . scope )
547+ )
548+ ] ;
549+ }
550+
551+ // Querying a child scope: show consumers in that child scope only.
552+ if ( isElementInScope ( element , variable . scope ) ) {
553+ return [
554+ ...names ,
555+ ...elements . filter ( usage => isElementInScope ( usage , element ) )
556+ ] ;
557+ }
558+
559+ return names ;
560+ }
561+
562+ function normalizeGetVariablesForElementOptions ( options = { } ) {
563+ if ( ! has ( options , 'read' ) && ! has ( options , 'written' ) ) {
564+ return {
565+ read : false ,
566+ written : true ,
567+ local : options . local !== false
568+ } ;
569+ }
570+
571+ return {
572+ read : ! ! options . read ,
573+ written : ! ! options . written ,
574+ local : options . local !== false
575+ } ;
576+ }
577+
578+
579+ function isConsumedByElement ( variable , element ) {
580+ return ! variable . scope && hasElementUsage ( variable , element ) ;
581+ }
582+
583+ function isLocalReadByElement ( variable , element ) {
584+ if ( ! variable . scope || ! Array . isArray ( variable . usedBy ) ) {
585+ return false ;
586+ }
587+
588+ if ( hasElementUsage ( variable , element ) ) {
589+ return true ;
590+ }
591+
592+ if ( variable . scope . id !== element . id ) {
593+ return false ;
594+ }
595+
596+ return variable . usedBy . some ( usage => typeof usage === 'string' ) ;
597+ }
598+
599+ function hasElementUsage ( variable , element ) {
600+ if ( ! Array . isArray ( variable . usedBy ) ) {
601+ return false ;
602+ }
603+
604+ return variable . usedBy . some ( usage => usage && usage . id === element . id ) ;
605+ }
0 commit comments