Skip to content

Commit ef354f4

Browse files
committed
feat: provide variables used in feel expressions
1 parent f590b31 commit ef354f4

14 files changed

+11629
-83
lines changed

lib/base/VariableResolver.js

Lines changed: 226 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { getBusinessObject, is } from 'bpmn-js/lib/util/ModelUtil';
2+
import { has } from 'min-dash';
23
import CachedValue from './util/CachedValue';
34
import { mergeList } from './util/listUtil';
45
import { 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

Comments
 (0)