diff --git a/packages/dmn-editor/package.json b/packages/dmn-editor/package.json index 3c9cb3e7b10..da8d3e908cc 100644 --- a/packages/dmn-editor/package.json +++ b/packages/dmn-editor/package.json @@ -33,6 +33,7 @@ "@kie-tools-core/patternfly-base": "workspace:*", "@kie-tools-core/switch-expression-ts": "workspace:*", "@kie-tools/boxed-expression-component": "workspace:*", + "@kie-tools/dmn-feel-antlr4-parser": "workspace:*", "@kie-tools/dmn-marshaller": "workspace:*", "@kie-tools/feel-input-component": "workspace:*", "@kie-tools/i18n-common-dictionary": "workspace:*", diff --git a/packages/dmn-editor/src/boxedExpressions/BoxedExpression.tsx b/packages/dmn-editor/src/boxedExpressions/BoxedExpression.tsx index bad86ab1f89..4d214cc67a1 100644 --- a/packages/dmn-editor/src/boxedExpressions/BoxedExpression.tsx +++ b/packages/dmn-editor/src/boxedExpressions/BoxedExpression.tsx @@ -67,15 +67,27 @@ import { } from "@kie-tools/pmml-editor-marshaller/dist/marshaller/model/pmml4_4"; import { PMMLFieldData } from "@kie-tools/pmml-editor-marshaller/dist/api/PMMLFieldData"; import { getDefaultColumnWidth } from "./getDefaultColumnWidth"; +import { FeelVariables } from "@kie-tools/dmn-feel-antlr4-parser"; +import { DmnLatestModel } from "@kie-tools/dmn-marshaller"; export function BoxedExpression({ container }: { container: React.RefObject }) { const thisDmn = useDmnEditorStore((s) => s.dmn); const diagram = useDmnEditorStore((s) => s.diagram); const dispatch = useDmnEditorStore((s) => s.dispatch); const boxedExpressionEditor = useDmnEditorStore((s) => s.boxedExpressionEditor); - + const { externalDmnsByNamespace } = useDmnEditorDerivedStore(); const dmnEditorStoreApi = useDmnEditorStoreApi(); + const feelVariables = useMemo(() => { + const externalModels = new Map(); + + for (const [key, externalDmn] of externalDmnsByNamespace) { + externalModels.set(key, externalDmn.model); + } + + return new FeelVariables(thisDmn.model.definitions, externalModels); + }, [externalDmnsByNamespace, thisDmn.model.definitions]); + const widthsById = useMemo(() => { return ( thisDmn.model.definitions["dmndi:DMNDI"]?.["dmndi:DMNDiagram"]?.[diagram.drdIndex]["di:extension"]?.[ @@ -307,6 +319,7 @@ export function BoxedExpression({ container }: { container: React.RefObject )} diff --git a/packages/dmn-feel-antlr4-parser/src/FeelVariables.ts b/packages/dmn-feel-antlr4-parser/src/FeelVariables.ts index eec2741d34d..4698eb19f65 100644 --- a/packages/dmn-feel-antlr4-parser/src/FeelVariables.ts +++ b/packages/dmn-feel-antlr4-parser/src/FeelVariables.ts @@ -18,17 +18,28 @@ */ import { FeelVariablesParser } from "./parser/FeelVariablesParser"; -import { VariablesRepository } from "./parser/VariablesRepository"; +import { DmnDefinitions, VariablesRepository } from "./parser/VariablesRepository"; +import { DmnLatestModel, getMarshaller } from "@kie-tools/dmn-marshaller"; export class FeelVariables { private readonly _parser: FeelVariablesParser; private readonly _repository: VariablesRepository; - constructor(xml: string) { - this._repository = new VariablesRepository(xml); + constructor(dmnDefinitions: DmnDefinitions, externalDefinitions: Map) { + this._repository = new VariablesRepository(dmnDefinitions, externalDefinitions); this._parser = new FeelVariablesParser(this._repository); } + static fromModelXml(xml: string): FeelVariables { + const def = this.getDefinitions(xml); + return new FeelVariables(def, new Map()); + } + + static getDefinitions(xml: string) { + const marshaller = getMarshaller(xml, { upgradeTo: "latest" }); + return marshaller.parser.parse().definitions; + } + get parser(): FeelVariablesParser { return this._parser; } diff --git a/packages/dmn-feel-antlr4-parser/src/parser/FeelVariable.ts b/packages/dmn-feel-antlr4-parser/src/parser/FeelVariable.ts index d1377e35ed3..49fa1f27c4d 100644 --- a/packages/dmn-feel-antlr4-parser/src/parser/FeelVariable.ts +++ b/packages/dmn-feel-antlr4-parser/src/parser/FeelVariable.ts @@ -19,26 +19,34 @@ import { FeelSyntacticSymbolNature } from "./FeelSyntacticSymbolNature"; import { FeelSymbol } from "./FeelSymbol"; +import { Variable } from "./Variable"; export class FeelVariable { private readonly _text: string; - private readonly _startIndex: number; + private _startIndex: number; private readonly _feelSymbolNature: FeelSyntacticSymbolNature; private readonly _length: number; private readonly _scopeSymbols: FeelSymbol[]; + private readonly _source: Variable | undefined; constructor( startIndex: number, length: number, symbolType: FeelSyntacticSymbolNature, text: string, - scopeSymbols?: FeelSymbol[] + scopeSymbols?: FeelSymbol[], + source?: Variable ) { this._startIndex = startIndex; this._length = length; this._feelSymbolNature = symbolType; this._text = text; this._scopeSymbols = scopeSymbols ?? []; + this._source = source; + } + + get source(): Variable | undefined { + return this._source; } get text(): string { @@ -49,6 +57,10 @@ export class FeelVariable { return this._startIndex; } + set startIndex(value: number) { + this._startIndex = value; + } + get feelSymbolNature(): FeelSyntacticSymbolNature { return this._feelSymbolNature; } diff --git a/packages/dmn-feel-antlr4-parser/src/parser/FeelVariablesParser.ts b/packages/dmn-feel-antlr4-parser/src/parser/FeelVariablesParser.ts index 74c27805a32..970895b4a7f 100644 --- a/packages/dmn-feel-antlr4-parser/src/parser/FeelVariablesParser.ts +++ b/packages/dmn-feel-antlr4-parser/src/parser/FeelVariablesParser.ts @@ -33,6 +33,20 @@ export class FeelVariablesParser { constructor(variablesSource: VariablesRepository) { this.variablesRepository = variablesSource; + this.refreshExpressions(); + } + + public refreshExpressions() { + for (const expression of this.variablesRepository.expressions.values()) { + for (const variable of expression.variables) { + variable.source?.expressions.delete(expression.uuid); + } + const parsedExpression = this.parse(expression.uuid, expression.fullExpression); + expression.variables = parsedExpression.feelVariables; + for (const variable of parsedExpression.feelVariables) { + variable.source?.expressions.set(expression.uuid, expression); + } + } } public parse(variableContextUuid: string, expression: string): ParsedExpression { @@ -117,7 +131,8 @@ export class FeelVariablesParser { parser.helper.defineVariable( context.variable.value, context.variable.typeRef ? this.createType(context.variable.typeRef) : undefined, - context.variable.feelSyntacticSymbolNature + context.variable.feelSyntacticSymbolNature, + context.variable ); } } diff --git a/packages/dmn-feel-antlr4-parser/src/parser/Variable.ts b/packages/dmn-feel-antlr4-parser/src/parser/Variable.ts index 3e047353ba4..9ea76356e4c 100644 --- a/packages/dmn-feel-antlr4-parser/src/parser/Variable.ts +++ b/packages/dmn-feel-antlr4-parser/src/parser/Variable.ts @@ -19,6 +19,7 @@ import { DataType } from "./DataType"; import { FeelSyntacticSymbolNature } from "./FeelSyntacticSymbolNature"; +import { Expression } from "./VariableOccurrence"; /** * Describe a variable in FEEL. @@ -38,4 +39,9 @@ export interface Variable { * The type of the variable, which can be a custom data type defined by the user, a built-in type or not defined. */ typeRef?: DataType | string | undefined; + + /** + * The expressions where this variable is being used. + */ + expressions: Map; } diff --git a/packages/dmn-feel-antlr4-parser/src/parser/VariableOccurrence.ts b/packages/dmn-feel-antlr4-parser/src/parser/VariableOccurrence.ts new file mode 100644 index 00000000000..47911bb4a4f --- /dev/null +++ b/packages/dmn-feel-antlr4-parser/src/parser/VariableOccurrence.ts @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Variable } from "./Variable"; +import { FeelVariable } from "./FeelVariable"; + +export class Expression { + private readonly _uuid: string; + private _fullExpression: string; + private _variables: Array; + + constructor(uuid: string, fullExpression?: string) { + this._uuid = uuid; + this._variables = new Array(); + this._fullExpression = fullExpression ?? ""; + } + + public renameVariable(renamedVariable: Variable, newName: String) { + // We assume that variables are already ordered by the parser + + let offset = 0; + for (const variable of this._variables) { + variable.startIndex += offset; + if (variable.source != undefined && variable.source === renamedVariable) { + this.replaceAt(variable.startIndex, renamedVariable.value.length, newName); + offset += renamedVariable.value.length - newName.length; + } + } + } + + private replaceAt(position: number, oldLength: number, newVariable: String) { + const part1 = this.fullExpression.substring(0, position); + const newPart = newVariable; + const part2 = this.fullExpression.substring(position + oldLength); + + this.fullExpression = part1 + newPart + part2; + } + + get variables(): Array { + return this._variables; + } + + set variables(value: Array) { + this._variables = value; + } + + get fullExpression(): string { + return this._fullExpression; + } + + set fullExpression(value: string) { + this._fullExpression = value; + } + + get uuid(): string { + return this._uuid; + } +} diff --git a/packages/dmn-feel-antlr4-parser/src/parser/VariablesRepository.ts b/packages/dmn-feel-antlr4-parser/src/parser/VariablesRepository.ts index dad16c9793b..f9302bab000 100644 --- a/packages/dmn-feel-antlr4-parser/src/parser/VariablesRepository.ts +++ b/packages/dmn-feel-antlr4-parser/src/parser/VariablesRepository.ts @@ -17,7 +17,6 @@ * under the License. */ -import { getMarshaller } from "@kie-tools/dmn-marshaller"; import { DataType } from "./DataType"; import { FeelSyntacticSymbolNature } from "./FeelSyntacticSymbolNature"; import { VariableContext } from "./VariableContext"; @@ -43,6 +42,8 @@ import { DMN15__tQuantified, DMN15__tRelation, } from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types"; +import { Expression } from "./VariableOccurrence"; +import { DmnLatestModel, getMarshaller } from "@kie-tools/dmn-marshaller"; type DmnLiteralExpression = { __$$element: "literalExpression" } & DMN15__tLiteralExpression; type DmnInvocation = { __$$element: "invocation" } & DMN15__tInvocation; @@ -62,7 +63,7 @@ type UnsupportedDmnExpressions = type DmnDecisionNode = { __$$element: "decision" } & DMN15__tDecision; type DmnBusinessKnowledgeModel = DMN15__tBusinessKnowledgeModel; -type DmnDefinitions = DMN15__tDefinitions; +export type DmnDefinitions = DMN15__tDefinitions; type DmnItemDefinition = DMN15__tItemDefinition; type DmnContextEntry = DMN15__tContextEntry; type DmnInputData = DMN15__tInputData; @@ -71,12 +72,21 @@ type DmnDecisionService = DMN15__tDecisionService; export class VariablesRepository { private readonly variablesIndexedByUuid: Map; + private readonly expressionsIndexedByUuid: Map; private readonly dataTypes: Map; + private currentVariablePrefix: string; + private currentUuidPrefix: string; - constructor(modelXml: string) { + constructor(dmnDefinitions: DmnDefinitions, externalDefinitions: Map) { this.dataTypes = new Map(); this.variablesIndexedByUuid = new Map(); - this.loadVariables(modelXml); + this.expressionsIndexedByUuid = new Map(); + this.loadImportedVariables(dmnDefinitions, externalDefinitions); + + this.currentVariablePrefix = ""; + this.currentUuidPrefix = ""; + + this.loadVariables(dmnDefinitions); } get variables(): Map { @@ -90,9 +100,17 @@ export class VariablesRepository { } } + get expressions(): Map { + return this.expressionsIndexedByUuid; + } + public renameVariable(variableUuid: string, newName: string) { const variableContext = this.variablesIndexedByUuid.get(variableUuid); if (variableContext) { + for (const expression of variableContext.variable.expressions.values()) { + expression.renameVariable(variableContext.variable, newName); + } + variableContext.variable.value = newName; } } @@ -104,6 +122,7 @@ export class VariablesRepository { value: variableName, feelSyntacticSymbolNature: FeelSyntacticSymbolNature.GlobalVariable, typeRef: undefined, + expressions: new Map(), }; const newContext = { @@ -150,13 +169,6 @@ export class VariablesRepository { } } - private loadVariables(xml: string) { - const marshaller = getMarshaller(xml, { upgradeTo: "latest" }); - const definitions = marshaller.parser.parse().definitions; - this.createDataTypes(definitions); - this.createVariables(definitions); - } - private createDataTypes(definitions: DmnDefinitions) { definitions.itemDefinition?.forEach((itemDefinition) => { const dataType = this.createDataType(itemDefinition); @@ -278,9 +290,15 @@ export class VariablesRepository { parent?: VariableContext, typeRef?: string ) { - const node = this.createVariableNode(uuid, name, variableType, parent, typeRef); + const node = this.createVariableNode( + this.buildVariableUuid(uuid), + this.buildName(name), + variableType, + parent, + typeRef + ); - this.variablesIndexedByUuid.set(uuid, node); + this.variablesIndexedByUuid.set(this.buildVariableUuid(uuid), node); return node; } @@ -301,6 +319,7 @@ export class VariablesRepository { value: name, feelSyntacticSymbolNature: variableType, typeRef: this.getTypeRef(typeRef), + expressions: new Map(), }, }; } @@ -311,7 +330,7 @@ export class VariablesRepository { private createDataType(itemDefinition: DmnItemDefinition) { return { - name: itemDefinition["@_name"], + name: this.buildName(itemDefinition["@_name"]), properties: new Map(), typeRef: itemDefinition["typeRef"] ?? itemDefinition["@_name"], }; @@ -342,7 +361,10 @@ export class VariablesRepository { } private addLiteralExpression(parent: VariableContext, element: DmnLiteralExpression) { - this.addVariable(element["@_id"] ?? "", "", FeelSyntacticSymbolNature.LocalVariable, parent); + const id = element["@_id"] ?? ""; + const expression = new Expression(id, element.text); + this.expressionsIndexedByUuid.set(id, expression); + this.addVariable(id, "", FeelSyntacticSymbolNature.LocalVariable, parent); } private addInvocation(parent: VariableContext, element: DmnInvocation) { @@ -506,4 +528,40 @@ export class VariablesRepository { parent.inputVariables.push(knowledgeRequirement.requiredKnowledge["@_href"]?.replace("#", "")); } } + + private buildVariableUuid(uuid: string) { + if (this.currentUuidPrefix.length != 0) { + return this.currentUuidPrefix + uuid; + } + + return uuid; + } + + private buildName(name: string) { + if (this.currentVariablePrefix.length != 0) { + return this.currentVariablePrefix + "." + name; + } + + return name; + } + + private loadVariables(dmnDefinitions: DmnDefinitions) { + this.createDataTypes(dmnDefinitions); + this.createVariables(dmnDefinitions); + } + + private loadImportedVariables(dmnDefinitions: DmnDefinitions, externalDefinitions: Map) { + if (dmnDefinitions.import) { + for (const dmnImport of dmnDefinitions.import) { + if (externalDefinitions.has(dmnImport["@_namespace"])) { + this.currentVariablePrefix = dmnImport["@_name"]; + this.currentUuidPrefix = dmnImport["@_namespace"]; + const externalDef = externalDefinitions.get(dmnImport["@_namespace"]); + if (externalDef) { + this.loadVariables(externalDef.definitions); + } + } + } + } + } } diff --git a/packages/dmn-feel-antlr4-parser/src/parser/grammar/ParserHelper.ts b/packages/dmn-feel-antlr4-parser/src/parser/grammar/ParserHelper.ts index cba93040d85..52c892303bf 100644 --- a/packages/dmn-feel-antlr4-parser/src/parser/grammar/ParserHelper.ts +++ b/packages/dmn-feel-antlr4-parser/src/parser/grammar/ParserHelper.ts @@ -30,6 +30,7 @@ import { ReservedWords } from "../ReservedWords"; import { FeelSyntacticSymbolNature } from "../FeelSyntacticSymbolNature"; import { MapBackedType } from "./MapBackedType"; import { FeelSymbol } from "../FeelSymbol"; +import { Variable } from "../Variable"; export class ParserHelper { private dynamicResolution = 0; @@ -101,11 +102,17 @@ export class ParserHelper { return key; } - defineVariable(variable: string | ParserRuleContext, type?: Type, variableType?: FeelSyntacticSymbolNature) { + defineVariable( + variable: string | ParserRuleContext, + type?: Type, + variableType?: FeelSyntacticSymbolNature, + variableSource?: Variable + ) { const variableSymbol = new VariableSymbol( variable instanceof ParserRuleContext ? this.getName(variable) : variable, type, - variableType + variableType, + variableSource ); if (variableSymbol.getId()) { diff --git a/packages/dmn-feel-antlr4-parser/src/parser/grammar/VariableSymbol.ts b/packages/dmn-feel-antlr4-parser/src/parser/grammar/VariableSymbol.ts index 99b54f4924f..eeeab198aba 100644 --- a/packages/dmn-feel-antlr4-parser/src/parser/grammar/VariableSymbol.ts +++ b/packages/dmn-feel-antlr4-parser/src/parser/grammar/VariableSymbol.ts @@ -21,16 +21,24 @@ import { BaseSymbol } from "./BaseSymbol"; import { Type } from "./Type"; import { FeelSyntacticSymbolNature } from "../FeelSyntacticSymbolNature"; +import { Variable } from "../Variable"; export class VariableSymbol extends BaseSymbol { private readonly _symbolType: FeelSyntacticSymbolNature | undefined; - constructor(id?: string, type?: Type, variableType?: FeelSyntacticSymbolNature) { + private readonly _variableSource: Variable | undefined; + + constructor(id?: string, type?: Type, variableType?: FeelSyntacticSymbolNature, variableSource?: Variable) { super(id, type); this._symbolType = variableType; + this._variableSource = variableSource; } get symbolType(): FeelSyntacticSymbolNature | undefined { return this._symbolType; } + + get variableSource(): Variable | undefined { + return this._variableSource; + } } diff --git a/packages/dmn-language-service/src/DmnLanguageService.ts b/packages/dmn-language-service/src/DmnLanguageService.ts index c11ab40e97b..80022e44f1e 100644 --- a/packages/dmn-language-service/src/DmnLanguageService.ts +++ b/packages/dmn-language-service/src/DmnLanguageService.ts @@ -19,7 +19,6 @@ import { DmnDocumentData } from "./DmnDocumentData"; import { DmnDecision } from "./DmnDecision"; -import { FeelVariables } from "@kie-tools/dmn-feel-antlr4-parser"; const IMPORT = "import"; const INPUT_DATA = "inputData"; diff --git a/packages/stunner-editors-dmn-loader/src/index.tsx b/packages/stunner-editors-dmn-loader/src/index.tsx index 78635a92583..5ce0a2c19b6 100644 --- a/packages/stunner-editors-dmn-loader/src/index.tsx +++ b/packages/stunner-editors-dmn-loader/src/index.tsx @@ -201,7 +201,7 @@ const renderImportJavaClasses = (selector: string) => { }; const getVariables = (xml: string): FeelVariables => { - return new FeelVariables(xml); + return FeelVariables.fromModelXml(xml); }; export { renderBoxedExpressionEditor, renderImportJavaClasses, unmountBoxedExpressionEditor, getVariables }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1034b0ff1a2..a5f0756b7b4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3212,6 +3212,9 @@ importers: "@kie-tools/boxed-expression-component": specifier: workspace:* version: link:../boxed-expression-component + "@kie-tools/dmn-feel-antlr4-parser": + specifier: workspace:* + version: link:../dmn-feel-antlr4-parser "@kie-tools/dmn-marshaller": specifier: workspace:* version: link:../dmn-marshaller