From 2c10e0f15036b311d499c26f4b7c8e82b45fa97b Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Tue, 5 Mar 2024 09:01:44 +0100 Subject: [PATCH 01/28] comp IDs are underlined when description is shown + min size for gates --- extension/package.json | 3 +- .../fta/diagram/fta-diagram-generator.ts | 29 ++++++++++++------- .../fta/diagram/fta-layout-config.ts | 11 +++++-- 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/extension/package.json b/extension/package.json index 39452ca..9c1ea7f 100644 --- a/extension/package.json +++ b/extension/package.json @@ -215,8 +215,7 @@ "when": "pasta-diagram-focused" }, { - "command": "pasta.diagram.export", - "when": "pasta-diagram-focused" + "command": "pasta.diagram.export" }, { "command": "pasta.stpa.checks.setCheckResponsibilitiesForConstraints", diff --git a/extension/src-language-server/fta/diagram/fta-diagram-generator.ts b/extension/src-language-server/fta/diagram/fta-diagram-generator.ts index 59ecbbd..2993890 100644 --- a/extension/src-language-server/fta/diagram/fta-diagram-generator.ts +++ b/extension/src-language-server/fta/diagram/fta-diagram-generator.ts @@ -36,7 +36,7 @@ import { } from "./fta-model"; import { FtaSynthesisOptions, noCutSet, spofsSet } from "./fta-synthesis-options"; import { getFTNodeType, getTargets } from "./utils"; - +import { HEADER_LABEL_TYPE } from "../../stpa/diagram/stpa-model"; export class FtaDiagramGenerator extends LangiumDiagramGenerator { protected readonly options: FtaSynthesisOptions; @@ -70,11 +70,11 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { const ftaChildren: SModelElement[] = [ // create nodes for top event, components, conditions, and gates - ...model.components.map((component) => this.generateFTNode(component, idCache)), - ...model.conditions.map((condition) => this.generateFTNode(condition, idCache)), - ...model.gates.map((gate) => this.generateGate(gate, idCache)), + ...model.components.map(component => this.generateFTNode(component, idCache)), + ...model.conditions.map(condition => this.generateFTNode(condition, idCache)), + ...model.gates.map(gate => this.generateGate(gate, idCache)), // create edges for the gates and the top event - ...model.gates.map((gate) => this.generateEdges(gate, idCache)).flat(1), + ...model.gates.map(gate => this.generateEdges(gate, idCache)).flat(1), ]; if (model.topEvent) { @@ -287,8 +287,13 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { */ protected generateFTNode(node: namedFtaElement, idCache: IdCache): FTANode { const nodeId = idCache.uniqueId(node.name.replace(" ", ""), node); - const children: SModelElement[] = this.createNodeLabel(node.name, nodeId, idCache); - if (this.options.getShowComponentDescriptions() && (node.$type === Component || node.$type === Condition) && node.description !== undefined) { + let children: SModelElement[] = []; + if ( + this.options.getShowComponentDescriptions() && + (node.$type === Component || node.$type === Condition) && + node.description !== undefined + ) { + children = this.createNodeLabel(node.name, nodeId, idCache, HEADER_LABEL_TYPE); const label = getDescription( node.description, this.options.getLabelManagement(), @@ -297,6 +302,8 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { idCache ); children.push(...label.reverse()); + } else { + children = this.createNodeLabel(node.name, nodeId, idCache); } // one port for outgoing edges const port = this.createFTAPort(idCache.uniqueId(nodeId + "_port"), PortSide.NORTH); @@ -304,7 +311,8 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { this.nodeToPort.set(nodeId, port); const description = isComponent(node) || isCondition(node) ? node.description : ""; const set = this.options.getCutSet(); - let includedInCutSet = set !== noCutSet.id ? set.includes(node.name + ",") || set.includes(node.name + "]") : false; + let includedInCutSet = + set !== noCutSet.id ? set.includes(node.name + ",") || set.includes(node.name + "]") : false; let notConnected = set !== noCutSet.id ? !includedInCutSet : false; // single points of failure should be shown if (set === spofsSet.id) { @@ -368,12 +376,13 @@ export class FtaDiagramGenerator extends LangiumDiagramGenerator { * @param label Label to translate to SLabel element. * @param id The ID of the element for which the label should be generated. * @param idCache The ID cache of the FTA model. + * @param type The type of the label. * @returns SLabel element representing {@code label}. */ - protected createNodeLabel(label: string, id: string, idCache: IdCache): SLabel[] { + protected createNodeLabel(label: string, id: string, idCache: IdCache, type = "label"): SLabel[] { return [ { - type: 'label', + type: type, id: idCache.uniqueId(id + "_label"), text: label, }, diff --git a/extension/src-language-server/fta/diagram/fta-layout-config.ts b/extension/src-language-server/fta/diagram/fta-layout-config.ts index 5e45594..6798fcc 100644 --- a/extension/src-language-server/fta/diagram/fta-layout-config.ts +++ b/extension/src-language-server/fta/diagram/fta-layout-config.ts @@ -27,9 +27,8 @@ export class FtaLayoutConfigurator extends DefaultLayoutConfigurator { "org.eclipse.elk.direction": "DOWN", "org.eclipse.elk.layered.nodePlacement.strategy": "BRANDES_KOEPF", "org.eclipse.elk.portConstraints": "FIXED_SIDE", - "org.eclipse.elk.hierarchyHandling": "INCLUDE_CHILDREN", "org.eclipse.elk.spacing.portPort": "0.0", - "org.eclipse.elk.layered.nodePlacement.bk.fixedAlignment": "BALANCED" + "org.eclipse.elk.layered.nodePlacement.bk.fixedAlignment": "BALANCED", }; if (sgraph.modelOrder) { @@ -53,7 +52,6 @@ export class FtaLayoutConfigurator extends DefaultLayoutConfigurator { options["org.eclipse.elk.direction"] = "DOWN"; options["org.eclipse.elk.padding"] = "[top=0.0,left=0.0,bottom=10.0,right=0.0]"; options["org.eclipse.elk.layered.spacing.nodeNodeBetweenLayers"] = "2"; - options["org.eclipse.elk.hierarchyHandling"] = "INCLUDE_CHILDREN"; break; case FTNodeType.COMPONENT: case FTNodeType.CONDITION: @@ -61,6 +59,13 @@ export class FtaLayoutConfigurator extends DefaultLayoutConfigurator { options["org.eclipse.elk.nodeSize.minimum"] = "(30, 30)"; options["org.eclipse.elk.spacing.labelNode"] = "20.0"; break; + case FTNodeType.AND: + case FTNodeType.OR: + case FTNodeType.KN: + case FTNodeType.INHIBIT: + options["org.eclipse.elk.nodeSize.constraints"] = "MINIMUM_SIZE"; + options["org.eclipse.elk.nodeSize.minimum"] = "(32.34, 35)"; + break; } break; case FTA_DESCRIPTION_NODE_TYPE: From dc0857fb5666cf94c4e99a4588108435684410ca Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Wed, 6 Mar 2024 10:26:42 +0100 Subject: [PATCH 02/28] stpa: IDs are underlined when description is also shown --- .../stpa/diagram/diagram-elements.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/extension/src-language-server/stpa/diagram/diagram-elements.ts b/extension/src-language-server/stpa/diagram/diagram-elements.ts index 2fcca8c..ce7ffef 100644 --- a/extension/src-language-server/stpa/diagram/diagram-elements.ts +++ b/extension/src-language-server/stpa/diagram/diagram-elements.ts @@ -21,7 +21,15 @@ import { SLabel, SModelElement } from "sprotty-protocol"; import { Model } from "../../generated/ast"; import { getDescription } from "../../utils"; import { CSEdge, CSNode, PastaPort, STPAEdge, STPANode } from "./stpa-interfaces"; -import { DUMMY_NODE_TYPE, EdgeType, PORT_TYPE, PortSide, STPAAspect, STPA_NODE_TYPE } from "./stpa-model"; +import { + DUMMY_NODE_TYPE, + EdgeType, + HEADER_LABEL_TYPE, + PORT_TYPE, + PortSide, + STPAAspect, + STPA_NODE_TYPE, +} from "./stpa-model"; import { getAspect } from "./utils"; import { StpaSynthesisOptions } from "./stpa-synthesis-options"; @@ -222,7 +230,7 @@ export function generateDescriptionLabels( // show the name in the top line children.push({ - type: "label", + type: showDescription ? HEADER_LABEL_TYPE : "label", id: idCache.uniqueId(nodeId + "_label"), text: nodeName, }); From b54ca660da03957611b5ccd09b1103f29151fcc0 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Thu, 7 Mar 2024 14:29:57 +0100 Subject: [PATCH 03/28] added check for conflicting UCAs --- .../stpa/stpa-validator.ts | 99 ++++++++++++++++--- 1 file changed, 84 insertions(+), 15 deletions(-) diff --git a/extension/src-language-server/stpa/stpa-validator.ts b/extension/src-language-server/stpa/stpa-validator.ts index a4e21cb..a1746b7 100644 --- a/extension/src-language-server/stpa/stpa-validator.ts +++ b/extension/src-language-server/stpa/stpa-validator.ts @@ -30,9 +30,10 @@ import { SystemConstraint, isModel, Graph, + Rule, } from "../generated/ast"; import { StpaServices } from "./stpa-module"; -import { collectElementsWithSubComps, elementWithName, elementWithRefs } from "./utils"; +import { UCA_TYPE, collectElementsWithSubComps, elementWithName, elementWithRefs } from "./utils"; /** * Registry for validation checks. @@ -72,6 +73,8 @@ export class StpaValidator { /** Boolean option to toggle the check whether all UCAs are covered by safety requirements. */ checkSafetyRequirementsForUCAs = true; + checkForConflictingUCAs = true; + /** * Executes validation checks for the whole model. * @param model The model to validate. @@ -182,7 +185,20 @@ export class StpaValidator { // check for duplicate ActionUCA definition this.checkActionUcasForDuplicates(model, accept); // check for duplicate rule definition - this.checkRulesForDuplicates(model, accept); + // group rules by action and system + const ruleMap = new Map(); + for (const rule of model.rules) { + const key = rule.system?.ref?.name + "." + rule.action?.ref?.name; + if (ruleMap.has(key)) { + ruleMap.get(key)?.push(rule); + } else { + ruleMap.set(key, [rule]); + } + } + this.checkRulesForDuplicates(ruleMap, accept); + if (this.checkForConflictingUCAs) { + this.checkForConflictingRules(ruleMap, accept); + } } /** @@ -204,30 +220,83 @@ export class StpaValidator { /** * Validates that at most one UCA rule is defined for a control action and type. - * @param model The model containing the rules. + * @param ruleMap The rules mapped by their control action. * @param accept */ - checkRulesForDuplicates(model: Model, accept: ValidationAcceptor): void { - const actionTypePairs = new Map(); - for (const rule of model.rules) { - const action = rule.system?.$refText + "." + rule.action?.$refText; - const type = rule.type; - if (actionTypePairs.has(action)) { - const definedTypes = actionTypePairs.get(action); - if (definedTypes?.includes(type)) { + checkRulesForDuplicates(ruleMap: Map, accept: ValidationAcceptor): void { + for (const rules of ruleMap.values()) { + const types = new Set(); + for (const rule of rules) { + if (types.has(rule.type)) { accept("warning", "This UCA type is already covered by another rule for the stated action", { node: rule, property: "type", }); } else { - definedTypes?.push(type); + types.add(rule.type); } - } else { - actionTypePairs.set(action, [type]); } } } + /** + * Validates that rules for the same control action and type do not conflict. + * @param ruleMap The rules mapped by their control action. + * @param accept + */ + protected checkForConflictingRules(ruleMap: Map, accept: ValidationAcceptor): void { + for (const rules of ruleMap.values()) { + // UCAs can only conflict if one of them is a provided UCA + const providedRule = rules.find(rule => rule.type === UCA_TYPE.PROVIDED); + if (providedRule) { + for (const rule of rules) { + // check the UCAs of type provided against all other UCAs + if (rule.type !== UCA_TYPE.PROVIDED) { + for (const context of rule.contexts) { + for (const otherContext of providedRule.contexts) { + if (this.isSameContext(context, otherContext)) { + accept("warning", "Conflict with " + providedRule.name + " " + otherContext.name + " detected", { + node: context, + }); + } + } + } + } + } + } + } + } + + /** + * Checks whether the contexts of two rules are the same or whether one of them is a subset of the other. + * @param context1 The first context to compare. + * @param context2 The second context to compare. + * @returns true if the contexts are the same or one of them is a subset of the other, false otherwise. + */ + protected isSameContext(context1: Context, context2: Context): boolean { + let isSame = true; + // check whether context1 is a subset of context2 + for (let i = 0; i < context1.vars.length; i++) { + const varIndex = context2.vars.findIndex(v => v.$refText === context1.vars[i].$refText); + if (varIndex === -1 || context2.values[varIndex] !== context1.values[i]) { + isSame = false; + break; + } + } + if (!isSame) { + isSame = true; + // check whether context2 is a subset of context1 + for (let i = 0; i < context2.vars.length; i++) { + const varIndex = context1.vars.findIndex(v => v.$refText === context2.vars[i].$refText); + if (varIndex === -1 || context1.values[varIndex] !== context2.values[i]) { + isSame = false; + break; + } + } + } + return isSame; + } + /** * Validates the variable values of {@code context}. * @param context The Context to check. @@ -292,7 +361,7 @@ export class StpaValidator { /** * Executes validation checks for the control structure. * @param graph The control structure to check. - * @param accept + * @param accept */ checkControlStructure(graph: Graph, accept: ValidationAcceptor): void { const nodes = [...graph.nodes, ...graph.nodes.map(node => this.getChildren(node)).flat(1)]; From 54551d602c34f40e2ec5273b2f9226190bc7fe7a Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Thu, 11 Apr 2024 16:52:45 +0200 Subject: [PATCH 04/28] added chekc for conflicting DCAs with UCAs --- .../stpa/stpa-validator.ts | 45 ++++++++++++++++--- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/extension/src-language-server/stpa/stpa-validator.ts b/extension/src-language-server/stpa/stpa-validator.ts index a1746b7..b25344a 100644 --- a/extension/src-language-server/stpa/stpa-validator.ts +++ b/extension/src-language-server/stpa/stpa-validator.ts @@ -31,6 +31,8 @@ import { isModel, Graph, Rule, + DCARule, + DCAContext, } from "../generated/ast"; import { StpaServices } from "./stpa-module"; import { UCA_TYPE, collectElementsWithSubComps, elementWithName, elementWithRefs } from "./utils"; @@ -199,6 +201,7 @@ export class StpaValidator { if (this.checkForConflictingUCAs) { this.checkForConflictingRules(ruleMap, accept); } + this.checkForConflictsBetweenUCAsAndDCAs(model.rules, model.allDCAs, accept); } /** @@ -242,7 +245,7 @@ export class StpaValidator { /** * Validates that rules for the same control action and type do not conflict. * @param ruleMap The rules mapped by their control action. - * @param accept + * @param accept */ protected checkForConflictingRules(ruleMap: Map, accept: ValidationAcceptor): void { for (const rules of ruleMap.values()) { @@ -255,9 +258,13 @@ export class StpaValidator { for (const context of rule.contexts) { for (const otherContext of providedRule.contexts) { if (this.isSameContext(context, otherContext)) { - accept("warning", "Conflict with " + providedRule.name + " " + otherContext.name + " detected", { - node: context, - }); + accept( + "warning", + "Conflict with " + providedRule.name + " " + otherContext.name + " detected", + { + node: context, + } + ); } } } @@ -267,13 +274,41 @@ export class StpaValidator { } } + /** + * Validates that there are no conflicts between UCAs and DCAs. + * A DCA is not allowed to have the same type and context as a UCA. + * @param ucas The UCAs to check. + * @param dcas The DCAs to check. + * @param accept + */ + protected checkForConflictsBetweenUCAsAndDCAs(ucas: Rule[], dcas: DCARule[], accept: ValidationAcceptor): void { + for (const dca of dcas) { + for (const uca of ucas) { + // if they have different types, they cannot conflict + const ucaType = uca.type === UCA_TYPE.PROVIDED ? UCA_TYPE.PROVIDED : UCA_TYPE.NOT_PROVIDED; + if (dca.type === ucaType) { + for (const context of dca.contexts) { + for (const otherContext of uca.contexts) { + // if they have the same type and context, they conflict + if (this.isSameContext(context, otherContext)) { + accept("warning", "Conflict with " + uca.name + " " + otherContext.name + " detected", { + node: context, + }); + } + } + } + } + } + } + } + /** * Checks whether the contexts of two rules are the same or whether one of them is a subset of the other. * @param context1 The first context to compare. * @param context2 The second context to compare. * @returns true if the contexts are the same or one of them is a subset of the other, false otherwise. */ - protected isSameContext(context1: Context, context2: Context): boolean { + protected isSameContext(context1: Context | DCAContext, context2: Context | DCAContext): boolean { let isSame = true; // check whether context1 is a subset of context2 for (let i = 0; i < context1.vars.length; i++) { From d26cf0ff7369b73de21f41e8e583944eb1e2a9e7 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Mon, 3 Jun 2024 12:41:52 +0200 Subject: [PATCH 05/28] removed "hide" from synthesis options --- .../stpa/diagram/filtering.ts | 18 +-- .../stpa/diagram/stpa-synthesis-options.ts | 146 +++++++++--------- .../stpa/result-report/svg-generator.ts | 116 +++++++------- 3 files changed, 140 insertions(+), 140 deletions(-) diff --git a/extension/src-language-server/stpa/diagram/filtering.ts b/extension/src-language-server/stpa/diagram/filtering.ts index 2db10f1..9296483 100644 --- a/extension/src-language-server/stpa/diagram/filtering.ts +++ b/extension/src-language-server/stpa/diagram/filtering.ts @@ -64,19 +64,19 @@ export function filterModel(model: Model, options: StpaSynthesisOptions): Custom newModel.losses = model.losses; newModel.hazards = model.hazards; - newModel.systemLevelConstraints = options.getHideSysCons() ? [] : model.systemLevelConstraints; + newModel.systemLevelConstraints = !options.getShowSysCons() ? [] : model.systemLevelConstraints; newModel.responsibilities = - options.getHideSysCons() || options.getHideRespsCons() ? [] : model.responsibilities; + !options.getShowSysCons() || !options.getShowRespsCons() ? [] : model.responsibilities; // filter UCAs by the filteringUCA option - newModel.allUCAs = options.getHideUCAs() + newModel.allUCAs = !options.getShowUCAs() ? [] : model.allUCAs?.filter( (allUCA) => allUCA.system.ref?.name + "." + allUCA.action.ref?.name === options.getFilteringUCAs() || options.getFilteringUCAs() === "all UCAs" ); - newModel.rules = options.getHideUCAs() + newModel.rules = !options.getShowUCAs() ? [] : model.rules?.filter( (rule) => @@ -84,7 +84,7 @@ export function filterModel(model: Model, options: StpaSynthesisOptions): Custom options.getFilteringUCAs() === "all UCAs" ); newModel.controllerConstraints = - options.getHideUCAs() || options.getHideContCons() + !options.getShowUCAs() || !options.getShowContCons() ? [] : model.controllerConstraints?.filter( (cons) => @@ -96,12 +96,12 @@ export function filterModel(model: Model, options: StpaSynthesisOptions): Custom // remaining scenarios must be saved to filter safety constraints const remainingScenarios = new Set(); - newModel.scenarios = options.getHideScenarios() + newModel.scenarios = !options.getShowScenarios() ? [] : model.scenarios?.filter((scenario) => { if ( - (!scenario.uca && !options.getHideScenariosWithHazard()) || - (scenario.uca && !options.getHideUCAs() && + (!scenario.uca && !options.getShowScenariosWithHazard()) || + (scenario.uca && !options.getShowUCAs() && (scenario.uca?.ref?.$container.system.ref?.name + "." + scenario.uca?.ref?.$container.action.ref?.name === @@ -114,7 +114,7 @@ export function filterModel(model: Model, options: StpaSynthesisOptions): Custom }); // filter safety constraints by the remaining scenarios newModel.safetyCons = - options.getHideSafetyConstraints() || options.getHideScenarios() + !options.getShowSafetyConstraints() || !options.getShowScenarios() ? [] : model.safetyCons?.filter( (safetyCons) => diff --git a/extension/src-language-server/stpa/diagram/stpa-synthesis-options.ts b/extension/src-language-server/stpa/diagram/stpa-synthesis-options.ts index 148af50..83d31ee 100644 --- a/extension/src-language-server/stpa/diagram/stpa-synthesis-options.ts +++ b/extension/src-language-server/stpa/diagram/stpa-synthesis-options.ts @@ -27,13 +27,13 @@ const hierarchyID = "hierarchy"; const groupingUCAsID = "groupingUCAs"; export const filteringUCAsID = "filteringUCAs"; -const hideSysConsID = "hideSysCons"; -const hideRespsID = "hideResps"; -const hideContConsID = "hideContCons"; -const hideScenariosID = "hideScenarios"; -const hideScenariosWithHazardID = "hideScenariosWithHazards"; -const hideUCAsID = "hideUCAs"; -const hideSafetyConstraintsID = "hideSafetyConstraints"; +const showSysConsID = "showSysCons"; +const showRespsID = "showResps"; +const showContConsID = "showContCons"; +const showScenariosID = "showScenarios"; +const showScenariosWithHazardID = "showScenariosWithHazards"; +const showUCAsID = "showUCAs"; +const showSafetyConstraintsID = "showSafetyConstraints"; const showLabelsID = "showLabels"; @@ -69,7 +69,7 @@ const filterCategoryOption: ValuedSynthesisOption = { const showControlStructureOption: ValuedSynthesisOption = { synthesisOption: { id: showControlStructureID, - name: "Show Control Structure", + name: "Control Structure", type: TransformationOptionType.CHECK, initialValue: true, currentValue: true, @@ -85,7 +85,7 @@ const showControlStructureOption: ValuedSynthesisOption = { const showProcessModelsOption: ValuedSynthesisOption = { synthesisOption: { id: showProcessModelsID, - name: "Show Process Models", + name: "Process Models", type: TransformationOptionType.CHECK, initialValue: false, currentValue: false, @@ -101,7 +101,7 @@ const showProcessModelsOption: ValuedSynthesisOption = { const showRelationshipGraphOption: ValuedSynthesisOption = { synthesisOption: { id: showRelationshipGraphID, - name: "Show Relationship Graph", + name: "Relationship Graph", type: TransformationOptionType.CHECK, initialValue: true, currentValue: true, @@ -116,15 +116,15 @@ const showRelationshipGraphOption: ValuedSynthesisOption = { */ const hideUCAsOption: ValuedSynthesisOption = { synthesisOption: { - id: hideUCAsID, - name: "Hide UCAs", + id: showUCAsID, + name: "UCAs", type: TransformationOptionType.CHECK, - initialValue: false, - currentValue: false, + initialValue: true, + currentValue: true, values: [true, false], category: filterCategory, }, - currentValue: false, + currentValue: true, }; /** @@ -132,15 +132,15 @@ const hideUCAsOption: ValuedSynthesisOption = { */ const hideSafetyConstraintsOption: ValuedSynthesisOption = { synthesisOption: { - id: hideSafetyConstraintsID, - name: "Hide Safety Constraints", + id: showSafetyConstraintsID, + name: "Safety Constraints", type: TransformationOptionType.CHECK, - initialValue: false, - currentValue: false, + initialValue: true, + currentValue: true, values: [true, false], category: filterCategory, }, - currentValue: false, + currentValue: true, }; /** @@ -208,15 +208,15 @@ const filteringOfUCAs: ValuedSynthesisOption = { */ const hideSysConsOption: ValuedSynthesisOption = { synthesisOption: { - id: hideSysConsID, - name: "Hide System-level Constraints", + id: showSysConsID, + name: "System-level Constraints", type: TransformationOptionType.CHECK, - initialValue: false, - currentValue: false, + initialValue: true, + currentValue: true, values: [true, false], category: filterCategory, }, - currentValue: false, + currentValue: true, }; /** @@ -224,15 +224,15 @@ const hideSysConsOption: ValuedSynthesisOption = { */ const hideRespsOption: ValuedSynthesisOption = { synthesisOption: { - id: hideRespsID, - name: "Hide Responsibilities", + id: showRespsID, + name: "Responsibilities", type: TransformationOptionType.CHECK, - initialValue: false, - currentValue: false, + initialValue: true, + currentValue: true, values: [true, false], category: filterCategory, }, - currentValue: false, + currentValue: true, }; /** @@ -240,15 +240,15 @@ const hideRespsOption: ValuedSynthesisOption = { */ const hideContConsOption: ValuedSynthesisOption = { synthesisOption: { - id: hideContConsID, - name: "Hide Controller Constraints", + id: showContConsID, + name: "Controller Constraints", type: TransformationOptionType.CHECK, - initialValue: false, - currentValue: false, + initialValue: true, + currentValue: true, values: [true, false], category: filterCategory, }, - currentValue: false, + currentValue: true, }; /** @@ -256,15 +256,15 @@ const hideContConsOption: ValuedSynthesisOption = { */ const hideScenariosOption: ValuedSynthesisOption = { synthesisOption: { - id: hideScenariosID, - name: "Hide Loss Scenarios", + id: showScenariosID, + name: "Loss Scenarios", type: TransformationOptionType.CHECK, - initialValue: false, - currentValue: false, + initialValue: true, + currentValue: true, values: [true, false], category: filterCategory, }, - currentValue: false, + currentValue: true, }; /** @@ -272,15 +272,15 @@ const hideScenariosOption: ValuedSynthesisOption = { */ const hideScenariosWithHazardsOption: ValuedSynthesisOption = { synthesisOption: { - id: hideScenariosWithHazardID, - name: "Hide Loss Scenarios Without UCAs", + id: showScenariosWithHazardID, + name: "Loss Scenarios Without UCAs", type: TransformationOptionType.CHECK, - initialValue: false, - currentValue: false, + initialValue: true, + currentValue: true, values: [true, false], category: filterCategory, }, - currentValue: false, + currentValue: true, }; /** @@ -455,60 +455,60 @@ export class StpaSynthesisOptions extends SynthesisOptions { return this.getOption(filteringUCAsID)?.currentValue; } - setHideSysCons(value: boolean): void { - this.setOption(hideSysConsID, value); + setShowSysCons(value: boolean): void { + this.setOption(showSysConsID, value); } - getHideSysCons(): boolean { - return this.getOption(hideSysConsID)?.currentValue; + getShowSysCons(): boolean { + return this.getOption(showSysConsID)?.currentValue; } - setHideResps(value: boolean): void { - this.setOption(hideRespsID, value); + setShowResps(value: boolean): void { + this.setOption(showRespsID, value); } - getHideRespsCons(): boolean { - return this.getOption(hideRespsID)?.currentValue; + getShowRespsCons(): boolean { + return this.getOption(showRespsID)?.currentValue; } - setHideUCAs(value: boolean): void { - this.setOption(hideUCAsID, value); + setShowUCAs(value: boolean): void { + this.setOption(showUCAsID, value); } - getHideUCAs(): boolean { - return this.getOption(hideUCAsID)?.currentValue; + getShowUCAs(): boolean { + return this.getOption(showUCAsID)?.currentValue; } - setHideContCons(value: boolean): void { - this.setOption(hideContConsID, value); + setShowContCons(value: boolean): void { + this.setOption(showContConsID, value); } - getHideContCons(): boolean { - return this.getOption(hideContConsID)?.currentValue; + getShowContCons(): boolean { + return this.getOption(showContConsID)?.currentValue; } - setHideScenarios(value: boolean): void { - this.setOption(hideScenariosID, value); + setShowScenarios(value: boolean): void { + this.setOption(showScenariosID, value); } - getHideScenarios(): boolean { - return this.getOption(hideScenariosID)?.currentValue; + getShowScenarios(): boolean { + return this.getOption(showScenariosID)?.currentValue; } - setHideScenariosWithHazard(value: boolean): void { - this.setOption(hideScenariosWithHazardID, value); + setShowScenariosWithHazard(value: boolean): void { + this.setOption(showScenariosWithHazardID, value); } - getHideScenariosWithHazard(): boolean { - return this.getOption(hideScenariosWithHazardID)?.currentValue; + getShowScenariosWithHazard(): boolean { + return this.getOption(showScenariosWithHazardID)?.currentValue; } - setHideSafetyConstraints(value: boolean): void { - this.setOption(hideSafetyConstraintsID, value); + setShowSafetyConstraints(value: boolean): void { + this.setOption(showSafetyConstraintsID, value); } - getHideSafetyConstraints(): boolean { - return this.getOption(hideSafetyConstraintsID)?.currentValue; + getShowSafetyConstraints(): boolean { + return this.getOption(showSafetyConstraintsID)?.currentValue; } /** diff --git a/extension/src-language-server/stpa/result-report/svg-generator.ts b/extension/src-language-server/stpa/result-report/svg-generator.ts index e7eaab6..0890657 100644 --- a/extension/src-language-server/stpa/result-report/svg-generator.ts +++ b/extension/src-language-server/stpa/result-report/svg-generator.ts @@ -89,12 +89,12 @@ export function setHazardGraphOptions(options: StpaSynthesisOptions): void { options.setShowRelationshipGraph(true); options.setShowControlStructure(false); options.setFilteringUCAs("all UCAs"); - options.setHideSysCons(true); - options.setHideResps(true); - options.setHideUCAs(true); - options.setHideContCons(true); - options.setHideScenarios(true); - options.setHideSafetyConstraints(true); + options.setShowSysCons(false); + options.setShowResps(false); + options.setShowUCAs(false); + options.setShowContCons(false); + options.setShowScenarios(false); + options.setShowSafetyConstraints(false); } /** @@ -105,12 +105,12 @@ export function setSystemConstraintGraphOptions(options: StpaSynthesisOptions): options.setShowRelationshipGraph(true); options.setShowControlStructure(false); options.setFilteringUCAs("all UCAs"); - options.setHideSysCons(false); - options.setHideResps(true); - options.setHideUCAs(true); - options.setHideContCons(true); - options.setHideScenarios(true); - options.setHideSafetyConstraints(true); + options.setShowSysCons(true); + options.setShowResps(false); + options.setShowUCAs(false); + options.setShowContCons(false); + options.setShowScenarios(false); + options.setShowSafetyConstraints(false); } /** @@ -121,12 +121,12 @@ export function setResponsibilityGraphOptions(options: StpaSynthesisOptions): vo options.setShowRelationshipGraph(true); options.setShowControlStructure(false); options.setFilteringUCAs("all UCAs"); - options.setHideSysCons(false); - options.setHideResps(false); - options.setHideUCAs(true); - options.setHideContCons(true); - options.setHideScenarios(true); - options.setHideSafetyConstraints(true); + options.setShowSysCons(true); + options.setShowResps(true); + options.setShowUCAs(false); + options.setShowContCons(false); + options.setShowScenarios(false); + options.setShowSafetyConstraints(false); } /** @@ -138,12 +138,12 @@ export function setFilteredUcaGraphOptions(options: StpaSynthesisOptions, value: options.setShowRelationshipGraph(true); options.setShowControlStructure(false); options.setFilteringUCAs(value); - options.setHideSysCons(true); - options.setHideResps(false); - options.setHideUCAs(false); - options.setHideContCons(true); - options.setHideScenarios(true); - options.setHideSafetyConstraints(true); + options.setShowSysCons(false); + options.setShowResps(true); + options.setShowUCAs(true); + options.setShowContCons(false); + options.setShowScenarios(false); + options.setShowSafetyConstraints(false); } /** @@ -155,12 +155,12 @@ export function setControllerConstraintWithFilteredUcaGraphOptions(options: Stpa options.setShowRelationshipGraph(true); options.setShowControlStructure(false); options.setFilteringUCAs(value); - options.setHideSysCons(true); - options.setHideResps(false); - options.setHideUCAs(false); - options.setHideContCons(false); - options.setHideScenarios(true); - options.setHideSafetyConstraints(true); + options.setShowSysCons(false); + options.setShowResps(true); + options.setShowUCAs(true); + options.setShowContCons(true); + options.setShowScenarios(false); + options.setShowSafetyConstraints(false); } /** @@ -172,13 +172,13 @@ export function setScenarioWithFilteredUCAGraphOptions(options: StpaSynthesisOpt options.setShowRelationshipGraph(true); options.setShowControlStructure(false); options.setFilteringUCAs(value); - options.setHideSysCons(true); - options.setHideResps(false); - options.setHideUCAs(false); - options.setHideContCons(true); - options.setHideScenarios(false); - options.setHideScenariosWithHazard(true); - options.setHideSafetyConstraints(true); + options.setShowSysCons(false); + options.setShowResps(true); + options.setShowUCAs(true); + options.setShowContCons(false); + options.setShowScenarios(true); + options.setShowScenariosWithHazard(false); + options.setShowSafetyConstraints(false); } /** @@ -190,13 +190,13 @@ export function setScenarioWithNoUCAGraphOptions(options: StpaSynthesisOptions): options.setShowRelationshipGraph(true); options.setShowControlStructure(false); options.setFilteringUCAs("all UCAs"); - options.setHideSysCons(true); - options.setHideResps(false); - options.setHideUCAs(true); - options.setHideContCons(true); - options.setHideScenarios(false); - options.setHideScenariosWithHazard(false); - options.setHideSafetyConstraints(true); + options.setShowSysCons(false); + options.setShowResps(true); + options.setShowUCAs(false); + options.setShowContCons(false); + options.setShowScenarios(true); + options.setShowScenariosWithHazard(true); + options.setShowSafetyConstraints(false); } /** @@ -207,13 +207,13 @@ export function setSafetyRequirementGraphOptions(options: StpaSynthesisOptions): options.setShowRelationshipGraph(true); options.setShowControlStructure(false); options.setFilteringUCAs("all UCAs"); - options.setHideSysCons(true); - options.setHideResps(false); - options.setHideUCAs(false); - options.setHideContCons(false); - options.setHideScenarios(false); - options.setHideScenariosWithHazard(false); - options.setHideSafetyConstraints(false); + options.setShowSysCons(false); + options.setShowResps(true); + options.setShowUCAs(true); + options.setShowContCons(true); + options.setShowScenarios(true); + options.setShowScenariosWithHazard(true); + options.setShowSafetyConstraints(true); } /** @@ -224,11 +224,11 @@ export function setRelationshipGraphOptions(options: StpaSynthesisOptions): void options.setShowRelationshipGraph(true); options.setShowControlStructure(false); options.setFilteringUCAs("all UCAs"); - options.setHideSysCons(false); - options.setHideResps(false); - options.setHideUCAs(false); - options.setHideContCons(false); - options.setHideScenarios(false); - options.setHideScenariosWithHazard(false); - options.setHideSafetyConstraints(false); + options.setShowSysCons(true); + options.setShowResps(true); + options.setShowUCAs(true); + options.setShowContCons(true); + options.setShowScenarios(true); + options.setShowScenariosWithHazard(true); + options.setShowSafetyConstraints(true); } From 69f694b36b985201bcc730170877a1b047c5bc3a Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Tue, 11 Jun 2024 09:08:12 +0200 Subject: [PATCH 06/28] fixed node ids for translation of json to elkt --- .../stpa/diagram/diagram-relationshipGraph.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension/src-language-server/stpa/diagram/diagram-relationshipGraph.ts b/extension/src-language-server/stpa/diagram/diagram-relationshipGraph.ts index 6351792..30aa3a9 100644 --- a/extension/src-language-server/stpa/diagram/diagram-relationshipGraph.ts +++ b/extension/src-language-server/stpa/diagram/diagram-relationshipGraph.ts @@ -318,7 +318,7 @@ export function generateSTPANode( options: StpaSynthesisOptions, idCache: IdCache ): STPANode { - const nodeId = idCache.uniqueId(node.name, node); + const nodeId = idCache.uniqueId(node.name.replace(/[.]/g, "_"), node); // determines the hierarchy level for subcomponents. For other components the value is 0. let lvl = 0; let container = node.$container; From ec31aefc2a2ff8dfe3b747dccdedbbc3fd40cfe2 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Tue, 11 Jun 2024 09:17:34 +0200 Subject: [PATCH 07/28] fixed conflict detection of UCAs and DCAs --- extension/src-language-server/stpa/stpa-validator.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/extension/src-language-server/stpa/stpa-validator.ts b/extension/src-language-server/stpa/stpa-validator.ts index b25344a..a982523 100644 --- a/extension/src-language-server/stpa/stpa-validator.ts +++ b/extension/src-language-server/stpa/stpa-validator.ts @@ -284,12 +284,14 @@ export class StpaValidator { protected checkForConflictsBetweenUCAsAndDCAs(ucas: Rule[], dcas: DCARule[], accept: ValidationAcceptor): void { for (const dca of dcas) { for (const uca of ucas) { - // if they have different types, they cannot conflict + // if they have different types or different control actions, they cannot conflict const ucaType = uca.type === UCA_TYPE.PROVIDED ? UCA_TYPE.PROVIDED : UCA_TYPE.NOT_PROVIDED; - if (dca.type === ucaType) { + const dcaAction = dca.system?.$refText + "." + dca.action?.$refText; + const ucaAction = uca.system?.$refText + "." + uca.action?.$refText; + if (dcaAction === ucaAction && dca.type === ucaType) { for (const context of dca.contexts) { for (const otherContext of uca.contexts) { - // if they have the same type and context, they conflict + // if they have the same type and context for same control action, they conflict if (this.isSameContext(context, otherContext)) { accept("warning", "Conflict with " + uca.name + " " + otherContext.name + " detected", { node: context, From cd0b77159922effe817080b97d48133ac1451adf Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Tue, 11 Jun 2024 10:45:09 +0200 Subject: [PATCH 08/28] added check for dublicate DCAs --- .../stpa/stpa-validator.ts | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/extension/src-language-server/stpa/stpa-validator.ts b/extension/src-language-server/stpa/stpa-validator.ts index a982523..e47d89b 100644 --- a/extension/src-language-server/stpa/stpa-validator.ts +++ b/extension/src-language-server/stpa/stpa-validator.ts @@ -33,6 +33,7 @@ import { Rule, DCARule, DCAContext, + isRule, } from "../generated/ast"; import { StpaServices } from "./stpa-module"; import { UCA_TYPE, collectElementsWithSubComps, elementWithName, elementWithRefs } from "./utils"; @@ -183,7 +184,16 @@ export class StpaValidator { }) ) ); + // check UCAs and DCAs + this.checkUCAsAndDCAs(model, accept); + } + /** + * Checks the UCAs and DCAs for duplicates and conflicts. + * @param model The model containing the UCAs and DCAs. + * @param accept + */ + protected checkUCAsAndDCAs(model: Model, accept: ValidationAcceptor): void { // check for duplicate ActionUCA definition this.checkActionUcasForDuplicates(model, accept); // check for duplicate rule definition @@ -198,9 +208,23 @@ export class StpaValidator { } } this.checkRulesForDuplicates(ruleMap, accept); + // check for duplicate dca rule definition + // group dca rules by action and system + const dcaRuleMap = new Map(); + for (const dcaRule of model.allDCAs) { + const key = dcaRule.system?.ref?.name + "." + dcaRule.action?.ref?.name; + if (dcaRuleMap.has(key)) { + dcaRuleMap.get(key)?.push(dcaRule); + } else { + dcaRuleMap.set(key, [dcaRule]); + } + } + this.checkRulesForDuplicates(dcaRuleMap, accept); + // check for conflicting UCAs if (this.checkForConflictingUCAs) { this.checkForConflictingRules(ruleMap, accept); } + // check for conflicts between UCAs and DCAs this.checkForConflictsBetweenUCAsAndDCAs(model.rules, model.allDCAs, accept); } @@ -226,12 +250,13 @@ export class StpaValidator { * @param ruleMap The rules mapped by their control action. * @param accept */ - checkRulesForDuplicates(ruleMap: Map, accept: ValidationAcceptor): void { + checkRulesForDuplicates(ruleMap: Map | Map, accept: ValidationAcceptor): void { for (const rules of ruleMap.values()) { const types = new Set(); for (const rule of rules) { if (types.has(rule.type)) { - accept("warning", "This UCA type is already covered by another rule for the stated action", { + const action = isRule(rule) ? "UCA" : "DCA"; + accept("warning", `This ${action} type is already covered by another rule for the stated action`, { node: rule, property: "type", }); From 97483f6991a8f035a9cb8075bff6c738f203e302 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Tue, 11 Jun 2024 10:45:38 +0200 Subject: [PATCH 09/28] subcomponents in control structure have flexible node size option --- .../src-language-server/stpa/diagram/layout-config.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/extension/src-language-server/stpa/diagram/layout-config.ts b/extension/src-language-server/stpa/diagram/layout-config.ts index aa1f5fd..ce8376c 100644 --- a/extension/src-language-server/stpa/diagram/layout-config.ts +++ b/extension/src-language-server/stpa/diagram/layout-config.ts @@ -115,7 +115,10 @@ export class StpaLayoutConfigurator extends DefaultLayoutConfigurator { "org.eclipse.elk.direction": "DOWN", "org.eclipse.elk.portConstraints": "FIXED_SIDE", "org.eclipse.elk.layered.considerModelOrder.strategy": "NODES_AND_EDGES", - "org.eclipse.elk.layered.crossingMinimization.forceNodeModelOrder": "true" + "org.eclipse.elk.layered.crossingMinimization.forceNodeModelOrder": "true", + // nodes with many edges are streched + "org.eclipse.elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX", + "org.eclipse.elk.layered.nodePlacement.networkSimplex.nodeFlexibility.default": "NODE_SIZE", }; } @@ -173,9 +176,12 @@ export class StpaLayoutConfigurator extends DefaultLayoutConfigurator { // edges do no start at the border of the node "org.eclipse.elk.spacing.portsSurrounding": "[top=10.0,left=10.0,bottom=10.0,right=10.0]", "org.eclipse.elk.portConstraints": "FIXED_SIDE", + // nodes with many edges are streched + "org.eclipse.elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX", + "org.eclipse.elk.layered.nodePlacement.networkSimplex.nodeFlexibility.default": "NODE_SIZE", }; if (node.children?.find(child => child.type.startsWith("node"))) { - // node hast children nodes + // node has children nodes options["org.eclipse.elk.nodeLabels.placement"] = "INSIDE V_TOP H_CENTER"; options["org.eclipse.elk.direction"] = "DOWN"; options["org.eclipse.elk.partitioning.activate"] = "true"; From 8f4a18642edfc0d3074db1cce3864a2503835778 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Tue, 11 Jun 2024 10:49:58 +0200 Subject: [PATCH 10/28] fixed linking of context table with UCAs in editor --- extension/package.json | 2 +- yarn.lock | 27 ++++++++++++++++----------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/extension/package.json b/extension/package.json index e6a9a7a..fc7e978 100644 --- a/extension/package.json +++ b/extension/package.json @@ -456,7 +456,7 @@ "reflect-metadata": "^0.1.13", "feather-icons": "^4.28.0", "sprotty-vscode-webview": "^0.5.0", - "@kieler/table-webview": "^0.0.3", + "@kieler/table-webview": "^0.0.5", "snabbdom": "^3.5.1", "dayjs": "^1.11.8" }, diff --git a/yarn.lock b/yarn.lock index 5f0fa11..de89a57 100644 --- a/yarn.lock +++ b/yarn.lock @@ -238,13 +238,13 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" -"@kieler/table-webview@^0.0.3": - version "0.0.3" - resolved "https://registry.yarnpkg.com/@kieler/table-webview/-/table-webview-0.0.3.tgz#33199d9b0d8cd88d0aad6d4230617b94942482aa" - integrity sha512-XiDfn/MwHzVEpXLWC5DT6Ysg/5Zke3GlbtjBDDPRD1mLFXIekOCxkGYAKu068djqSAg3hsoiIhwLwWBfm48VNQ== +"@kieler/table-webview@^0.0.5": + version "0.0.5" + resolved "https://registry.yarnpkg.com/@kieler/table-webview/-/table-webview-0.0.5.tgz#b8b0f32aed2ec36aa4efac63bcf14931a28b874c" + integrity sha512-vLVbwinjHGhcEPOnupcTWkhmhEvhDF3saZt7EJVbfPyAYiTMRmrT3LifPJ74K3Gie0HyVvCdt2qPuZf88asnog== dependencies: - "@types/vscode" "^1.56.0" - reflect-metadata "^0.1.13" + "@types/vscode" "^1.85.0" + reflect-metadata "^0.2.1" snabbdom "^3.5.1" "@lerna/add@3.21.0": @@ -1187,16 +1187,16 @@ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91" integrity sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw== -"@types/vscode@^1.56.0": - version "1.81.0" - resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.81.0.tgz#c27228dd063002e0e00611be70b0497beaa24d39" - integrity sha512-YIaCwpT+O2E7WOMq0eCgBEABE++SX3Yl/O02GoMIF2DO3qAtvw7m6BXFYsxnc6XyzwZgh6/s/UG78LSSombl2w== - "@types/vscode@^1.80.0": version "1.80.0" resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.80.0.tgz#e004dd6cde74dafdb7fab64a6e1754bf8165b981" integrity sha512-qK/CmOdS2o7ry3k6YqU4zD3R2AYlJfbwBoSbKpBoP+GpXNE+0NEgJOli4n0bm0diK5kfBnchgCEj4igQz/44Hg== +"@types/vscode@^1.85.0": + version "1.90.0" + resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.90.0.tgz#c122384d51bd774cec4aa86ca443858adc9edef2" + integrity sha512-oT+ZJL7qHS9Z8bs0+WKf/kQ27qWYR3trsXpq46YDjFqBsMLG4ygGGjPaJ2tyrH0wJzjOEmDyg9PDJBBhWg9pkQ== + "@typescript-eslint/eslint-plugin@^5.54.1": version "5.54.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.54.1.tgz#0c5091289ce28372e38ab8d28e861d2dbe1ab29e" @@ -6348,6 +6348,11 @@ reflect-metadata@^0.1.13: resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08" integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg== +reflect-metadata@^0.2.1: + version "0.2.2" + resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.2.2.tgz#400c845b6cba87a21f2c65c4aeb158f4fa4d9c5b" + integrity sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q== + regex-not@^1.0.0, regex-not@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" From 2b2603ae2df7662d0e1a2804d28ac2d89a309f6c Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Wed, 19 Jun 2024 12:51:02 +0200 Subject: [PATCH 11/28] updated node version in github actions --- .github/workflows/package.yml | 2 +- .github/workflows/publish.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 6804da7..6ab6d22 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -16,7 +16,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-node@v2 with: - node-version: 14.x + node-version: 16.x registry-url: 'https://registry.npmjs.org' scope: '@kieler' - run: yarn diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 76bce99..a2206be 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-node@v2 with: - node-version: "14.x" + node-version: "16.x" registry-url: 'https://registry.npmjs.org' scope: '@kieler' - run: yarn @@ -35,7 +35,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-node@v2 with: - node-version: "14.x" + node-version: "16.x" - run: yarn - run: yarn package From 5e947169969b22eef0373b300605e9ad1f7af2e5 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Wed, 19 Jun 2024 12:55:43 +0200 Subject: [PATCH 12/28] updated node version in github actions --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9b5dd6a..e933140 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-node@v2 with: - node-version: 14.x + node-version: 16.x - run: yarn - run: yarn lint - run: yarn build From 0cabd9d27cef0981ba396328c8301fbbe27dadbf Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Fri, 21 Jun 2024 14:14:55 +0200 Subject: [PATCH 13/28] fixed showing of description --- .../stpa/diagram/diagram-relationshipGraph.ts | 241 +++++++++--------- 1 file changed, 120 insertions(+), 121 deletions(-) diff --git a/extension/src-language-server/stpa/diagram/diagram-relationshipGraph.ts b/extension/src-language-server/stpa/diagram/diagram-relationshipGraph.ts index 30aa3a9..6dd17ba 100644 --- a/extension/src-language-server/stpa/diagram/diagram-relationshipGraph.ts +++ b/extension/src-language-server/stpa/diagram/diagram-relationshipGraph.ts @@ -39,6 +39,7 @@ import { getTargets, setLevelsForSTPANodes, } from "./utils"; +import { labelManagementValue } from "../../synthesis-options"; /** * Creates the relationship graph for the STPA model. @@ -93,21 +94,27 @@ export function createRelationshipGraphChildren( idCache: IdCache ): SModelElement[] { const showLabels = options.getShowLabels(); + const labelManagement = options.getLabelManagement(); // aspects that should have a description when showLabel option is set to automatic const aspectsToShowDescriptions = getAspectsThatShouldHaveDesriptions(model); // determine the children for the STPA graph // for each component a node is generated with edges representing the references of the component // in order to be able to set the target IDs of the edges, the nodes must be created in the correct order + const showLossLabel = showLabelOfAspect(STPAAspect.LOSS, aspectsToShowDescriptions, showLabels, labelManagement); let stpaChildren: SModelElement[] = filteredModel.losses?.map(l => - generateSTPANode( - l, - showLabels === showLabelsValue.ALL || - showLabels === showLabelsValue.LOSSES || - (showLabels === showLabelsValue.AUTOMATIC && aspectsToShowDescriptions.includes(STPAAspect.LOSS)), - idToSNode, - options, - idCache - ) + generateSTPANode(l, showLossLabel, idToSNode, options, idCache) + ); + const showHazardDescription = showLabelOfAspect( + STPAAspect.HAZARD, + aspectsToShowDescriptions, + showLabels, + labelManagement + ); + const showSystemConstraintDescription = showLabelOfAspect( + STPAAspect.SYSTEMCONSTRAINT, + aspectsToShowDescriptions, + showLabels, + labelManagement ); // the hierarchy option determines whether subcomponents are contained in ther parent or not if (!options.getHierarchy()) { @@ -116,31 +123,11 @@ export function createRelationshipGraphChildren( const sysCons = collectElementsWithSubComps(filteredModel.systemLevelConstraints); stpaChildren = stpaChildren?.concat([ ...hazards - .map(hazard => - generateAspectWithEdges( - hazard, - showLabels === showLabelsValue.ALL || - showLabels === showLabelsValue.HAZARDS || - (showLabels === showLabelsValue.AUTOMATIC && - aspectsToShowDescriptions.includes(STPAAspect.HAZARD)), - idToSNode, - options, - idCache - ) - ) + .map(hazard => generateAspectWithEdges(hazard, showHazardDescription, idToSNode, options, idCache)) .flat(1), ...sysCons .map(systemConstraint => - generateAspectWithEdges( - systemConstraint, - showLabels === showLabelsValue.ALL || - showLabels === showLabelsValue.SYSTEM_CONSTRAINTS || - (showLabels === showLabelsValue.AUTOMATIC && - aspectsToShowDescriptions.includes(STPAAspect.SYSTEMCONSTRAINT)), - idToSNode, - options, - idCache - ) + generateAspectWithEdges(systemConstraint, showSystemConstraintDescription, idToSNode, options, idCache) ) .flat(1), ]); @@ -148,31 +135,11 @@ export function createRelationshipGraphChildren( // subcomponents are contained in the parent stpaChildren = stpaChildren?.concat([ ...filteredModel.hazards - ?.map(hazard => - generateAspectWithEdges( - hazard, - showLabels === showLabelsValue.ALL || - showLabels === showLabelsValue.HAZARDS || - (showLabels === showLabelsValue.AUTOMATIC && - aspectsToShowDescriptions.includes(STPAAspect.HAZARD)), - idToSNode, - options, - idCache - ) - ) + ?.map(hazard => generateAspectWithEdges(hazard, showHazardDescription, idToSNode, options, idCache)) .flat(1), ...filteredModel.systemLevelConstraints ?.map(systemConstraint => - generateAspectWithEdges( - systemConstraint, - showLabels === showLabelsValue.ALL || - showLabels === showLabelsValue.SYSTEM_CONSTRAINTS || - (showLabels === showLabelsValue.AUTOMATIC && - aspectsToShowDescriptions.includes(STPAAspect.SYSTEMCONSTRAINT)), - idToSNode, - options, - idCache - ) + generateAspectWithEdges(systemConstraint, showSystemConstraintDescription, idToSNode, options, idCache) ) .flat(1), ...filteredModel.systemLevelConstraints @@ -184,20 +151,36 @@ export function createRelationshipGraphChildren( .flat(2), ]); } + const showResponsibilitiesDescription = showLabelOfAspect( + STPAAspect.RESPONSIBILITY, + aspectsToShowDescriptions, + showLabels, + labelManagement + ); + const showUCAsDescription = showLabelOfAspect(STPAAspect.UCA, aspectsToShowDescriptions, showLabels, labelManagement); + const showControllerConstraintDescription = showLabelOfAspect( + STPAAspect.CONTROLLERCONSTRAINT, + aspectsToShowDescriptions, + showLabels, + labelManagement + ); + const showScenarioDescription = showLabelOfAspect( + STPAAspect.SCENARIO, + aspectsToShowDescriptions, + showLabels, + labelManagement + ); + const showSafetyConsDescription = showLabelOfAspect( + STPAAspect.SAFETYREQUIREMENT, + aspectsToShowDescriptions, + showLabels, + labelManagement + ); stpaChildren = stpaChildren?.concat([ ...filteredModel.responsibilities ?.map(r => r.responsiblitiesForOneSystem.map(resp => - generateAspectWithEdges( - resp, - showLabels === showLabelsValue.ALL || - showLabels === showLabelsValue.RESPONSIBILITIES || - (showLabels === showLabelsValue.AUTOMATIC && - aspectsToShowDescriptions.includes(STPAAspect.RESPONSIBILITY)), - idToSNode, - options, - idCache - ) + generateAspectWithEdges(resp, showResponsibilitiesDescription, idToSNode, options, idCache) ) ) .flat(2), @@ -205,82 +188,98 @@ export function createRelationshipGraphChildren( ?.map(sysUCA => sysUCA.providingUcas .concat(sysUCA.notProvidingUcas, sysUCA.wrongTimingUcas, sysUCA.continousUcas) - .map(uca => - generateAspectWithEdges( - uca, - showLabels === showLabelsValue.ALL || - showLabels === showLabelsValue.UCAS || - (showLabels === showLabelsValue.AUTOMATIC && - aspectsToShowDescriptions.includes(STPAAspect.UCA)), - idToSNode, - options, - idCache - ) - ) + .map(uca => generateAspectWithEdges(uca, showUCAsDescription, idToSNode, options, idCache)) ) .flat(2), ...filteredModel.rules ?.map(rule => rule.contexts.map(context => - generateAspectWithEdges( - context, - showLabels === showLabelsValue.ALL || - showLabels === showLabelsValue.UCAS || - (showLabels === showLabelsValue.AUTOMATIC && - aspectsToShowDescriptions.includes(STPAAspect.UCA)), - idToSNode, - options, - idCache - ) + generateAspectWithEdges(context, showUCAsDescription, idToSNode, options, idCache) ) ) .flat(2), ...filteredModel.controllerConstraints - ?.map(c => - generateAspectWithEdges( - c, - showLabels === showLabelsValue.ALL || - showLabels === showLabelsValue.CONTROLLER_CONSTRAINTS || - (showLabels === showLabelsValue.AUTOMATIC && - aspectsToShowDescriptions.includes(STPAAspect.CONTROLLERCONSTRAINT)), - idToSNode, - options, - idCache - ) - ) + ?.map(c => generateAspectWithEdges(c, showControllerConstraintDescription, idToSNode, options, idCache)) .flat(1), ...filteredModel.scenarios - ?.map(s => - generateAspectWithEdges( - s, - showLabels === showLabelsValue.ALL || - showLabels === showLabelsValue.SCENARIOS || - (showLabels === showLabelsValue.AUTOMATIC && - aspectsToShowDescriptions.includes(STPAAspect.SCENARIO)), - idToSNode, - options, - idCache - ) - ) + ?.map(s => generateAspectWithEdges(s, showScenarioDescription, idToSNode, options, idCache)) .flat(1), ...filteredModel.safetyCons - ?.map(sr => - generateAspectWithEdges( - sr, - showLabels === showLabelsValue.ALL || - showLabels === showLabelsValue.SAFETY_CONSTRAINTS || - (showLabels === showLabelsValue.AUTOMATIC && - aspectsToShowDescriptions.includes(STPAAspect.SAFETYREQUIREMENT)), - idToSNode, - options, - idCache - ) - ) + ?.map(sr => generateAspectWithEdges(sr, showSafetyConsDescription, idToSNode, options, idCache)) .flat(1), ]); return stpaChildren; } +/** + * Determines whether the label of the given {@code aspect} should be shown based on the given {@code showLabels} and {@code labelManagement}. + * @param aspect The aspect for which the label should be shown. + * @param aspectsToShowDescriptions The aspects that should have a description when the showLabel option is set to automatic. + * @param showLabels The showLabel option of the STPA model. + * @param labelManagement The labelManagement option of the STPA model. + * @returns whether the label of the given {@code aspect} should be shown. + */ +function showLabelOfAspect( + aspect: STPAAspect, + aspectsToShowDescriptions: STPAAspect[], + showLabels: showLabelsValue, + labelManagement: labelManagementValue +): boolean { + if (labelManagement === labelManagementValue.NO_LABELS) { + return false; + } + if (showLabels === showLabelsValue.ALL) { + return true; + } + switch (aspect) { + case STPAAspect.LOSS: + return ( + showLabels === showLabelsValue.LOSSES || + (showLabels === showLabelsValue.AUTOMATIC && aspectsToShowDescriptions.includes(STPAAspect.LOSS)) + ); + case STPAAspect.HAZARD: + return ( + showLabels === showLabelsValue.HAZARDS || + (showLabels === showLabelsValue.AUTOMATIC && aspectsToShowDescriptions.includes(STPAAspect.HAZARD)) + ); + case STPAAspect.SYSTEMCONSTRAINT: + return ( + showLabels === showLabelsValue.SYSTEM_CONSTRAINTS || + (showLabels === showLabelsValue.AUTOMATIC && + aspectsToShowDescriptions.includes(STPAAspect.SYSTEMCONSTRAINT)) + ); + case STPAAspect.RESPONSIBILITY: + return ( + showLabels === showLabelsValue.RESPONSIBILITIES || + (showLabels === showLabelsValue.AUTOMATIC && + aspectsToShowDescriptions.includes(STPAAspect.RESPONSIBILITY)) + ); + case STPAAspect.UCA: + return ( + showLabels === showLabelsValue.UCAS || + (showLabels === showLabelsValue.AUTOMATIC && aspectsToShowDescriptions.includes(STPAAspect.UCA)) + ); + case STPAAspect.CONTROLLERCONSTRAINT: + return ( + showLabels === showLabelsValue.CONTROLLER_CONSTRAINTS || + (showLabels === showLabelsValue.AUTOMATIC && + aspectsToShowDescriptions.includes(STPAAspect.CONTROLLERCONSTRAINT)) + ); + case STPAAspect.SCENARIO: + return ( + showLabels === showLabelsValue.SCENARIOS || + (showLabels === showLabelsValue.AUTOMATIC && aspectsToShowDescriptions.includes(STPAAspect.SCENARIO)) + ); + case STPAAspect.SAFETYREQUIREMENT: + return ( + showLabels === showLabelsValue.SAFETY_CONSTRAINTS || + (showLabels === showLabelsValue.AUTOMATIC && + aspectsToShowDescriptions.includes(STPAAspect.SAFETYREQUIREMENT)) + ); + } + return false; +} + /** * Generates a node and the edges for the given {@code node}. * @param node STPA component for which a node and edges should be generated. From a8e2bcf9dc003de52fe815f9b04c5f5d678a75f9 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Fri, 21 Jun 2024 15:57:47 +0200 Subject: [PATCH 14/28] update diagram without resetting viewport --- .../src-language-server/diagram-server.ts | 8 +++--- extension/src-language-server/stpa/actions.ts | 26 ++++++++++++++++++- extension/src/actions.ts | 24 +++++++++++++++++ extension/src/extension.ts | 15 +++++++++-- 4 files changed, 67 insertions(+), 6 deletions(-) diff --git a/extension/src-language-server/diagram-server.ts b/extension/src-language-server/diagram-server.ts index 5a739bf..23d2a46 100644 --- a/extension/src-language-server/diagram-server.ts +++ b/extension/src-language-server/diagram-server.ts @@ -17,12 +17,13 @@ import { Action, DiagramServices, JsonMap, RequestAction, RequestModelAction, ResponseAction } from "sprotty-protocol"; import { Connection } from "vscode-languageserver"; +import { FtaServices } from "./fta/fta-module"; import { SetSynthesisOptionsAction, UpdateOptionsAction } from "./options/actions"; import { DropDownOption } from "./options/option-models"; import { SnippetDiagramServer } from "./snippets/snippet-diagram-server"; import { LanguageSnippet } from "./snippets/snippet-model"; import { StpaDiagramSnippets } from "./snippets/stpa-snippets"; -import { GenerateSVGsAction, RequestSvgAction, SvgAction } from "./stpa/actions"; +import { GenerateSVGsAction, RequestSvgAction, SvgAction, UpdateDiagramAction } from "./stpa/actions"; import { StpaSynthesisOptions, filteringUCAsID } from "./stpa/diagram/stpa-synthesis-options"; import { COMPLETE_GRAPH_PATH, @@ -48,9 +49,8 @@ import { setScenarioWithNoUCAGraphOptions, setSystemConstraintGraphOptions, } from "./stpa/result-report/svg-generator"; -import { SynthesisOptions } from "./synthesis-options"; import { StpaServices } from "./stpa/stpa-module"; -import { FtaServices } from "./fta/fta-module"; +import { SynthesisOptions } from "./synthesis-options"; export class PastaDiagramServer extends SnippetDiagramServer { protected synthesisOptions: SynthesisOptions | undefined; @@ -100,6 +100,8 @@ export class PastaDiagramServer extends SnippetDiagramServer { return this.handleSetSynthesisOption(action as SetSynthesisOptionsAction); case GenerateSVGsAction.KIND: return this.handleGenerateSVGDiagrams(action as GenerateSVGsAction); + case UpdateDiagramAction.KIND: + return this.updateView(this.state.options); } return super.handleAction(action); } diff --git a/extension/src-language-server/stpa/actions.ts b/extension/src-language-server/stpa/actions.ts index ffb0377..831ba1e 100644 --- a/extension/src-language-server/stpa/actions.ts +++ b/extension/src-language-server/stpa/actions.ts @@ -78,4 +78,28 @@ export namespace SvgAction { responseId: requestId }; } -} \ No newline at end of file +} + +/** Send to server to update the diagram. */ +export interface UpdateDiagramAction extends Action { + kind: typeof UpdateDiagramAction.KIND + options?: JsonMap; +} + +export namespace UpdateDiagramAction { + export const KIND = "updateDiagram"; + + + export function create( + options?: JsonMap + ): UpdateDiagramAction { + return { + kind: KIND, + options + }; + } + + export function isThisAction(action: Action): action is UpdateDiagramAction { + return action.kind === UpdateDiagramAction.KIND; + } +} diff --git a/extension/src/actions.ts b/extension/src/actions.ts index e59d875..58bb694 100644 --- a/extension/src/actions.ts +++ b/extension/src/actions.ts @@ -134,3 +134,27 @@ export namespace SendDefaultSnippetsAction { return action.kind === SendDefaultSnippetsAction.KIND; } } + +/** Send to server to update the diagram. */ +export interface UpdateDiagramAction extends Action { + kind: typeof UpdateDiagramAction.KIND + options?: JsonMap; +} + +export namespace UpdateDiagramAction { + export const KIND = "updateDiagram"; + + + export function create( + options?: JsonMap + ): UpdateDiagramAction { + return { + kind: KIND, + options + }; + } + + export function isThisAction(action: Action): action is UpdateDiagramAction { + return action.kind === UpdateDiagramAction.KIND; + } +} diff --git a/extension/src/extension.ts b/extension/src/extension.ts index bbcf352..200cf01 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -16,8 +16,9 @@ */ import * as path from "path"; +import { ActionMessage } from "sprotty-protocol"; import { createFileUri, registerDefaultCommands } from "sprotty-vscode"; -import { LspSprottyEditorProvider, LspSprottyViewProvider } from "sprotty-vscode/lib/lsp"; +import { LspSprottyEditorProvider, LspSprottyViewProvider, acceptMessageType } from "sprotty-vscode/lib/lsp"; import * as vscode from "vscode"; import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind } from "vscode-languageclient/node"; import { Messenger } from "vscode-messenger"; @@ -29,6 +30,7 @@ import { StpaResult } from "./report/utils"; import { createSBMs } from "./sbm/sbm-generation"; import { LTLFormula } from "./sbm/utils"; import { createFile, createOutputChannel, createQuickPickForWorkspaceOptions } from "./utils"; +import { UpdateDiagramAction } from './actions'; let languageClient: LanguageClient; @@ -325,7 +327,16 @@ function registerTextEditorSync(manager: StpaLspVscodeExtension, context: vscode if (currentCursorPosition) { await languageClient.sendNotification("editor/save", document.offsetAt(currentCursorPosition)); } - manager.openDiagram(document.uri, { preserveFocus: true }); + + // update diagram without reseting viewport + const mes: ActionMessage = { + clientId: manager.clientId!, + action: { + kind: UpdateDiagramAction.KIND, + } as UpdateDiagramAction, + }; + languageClient.sendNotification(acceptMessageType, mes); + if (manager.contextTable) { languageClient.sendNotification("contextTable/getData", document.uri.toString()); } From 8a9096c9df8bfe52721dac1c50c5f9f7fc4d2483 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Fri, 21 Jun 2024 16:09:55 +0200 Subject: [PATCH 15/28] fixed filtering of loss scenarios --- extension/src-language-server/stpa/diagram/filtering.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extension/src-language-server/stpa/diagram/filtering.ts b/extension/src-language-server/stpa/diagram/filtering.ts index 9296483..7fd626c 100644 --- a/extension/src-language-server/stpa/diagram/filtering.ts +++ b/extension/src-language-server/stpa/diagram/filtering.ts @@ -100,8 +100,8 @@ export function filterModel(model: Model, options: StpaSynthesisOptions): Custom ? [] : model.scenarios?.filter((scenario) => { if ( - (!scenario.uca && !options.getShowScenariosWithHazard()) || - (scenario.uca && !options.getShowUCAs() && + (!scenario.uca && options.getShowScenariosWithHazard()) || + (scenario.uca && options.getShowUCAs() && (scenario.uca?.ref?.$container.system.ref?.name + "." + scenario.uca?.ref?.$container.action.ref?.name === From 882f80ffb4401b1652a76121c0b1ba594b4dae7d Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Mon, 24 Jun 2024 12:01:37 +0200 Subject: [PATCH 16/28] update diagram on change --- .../src-language-server/diagram-server.ts | 21 +++++++++++-------- .../stpa/diagram/diagram-generator.ts | 17 +++++++++++---- extension/src/extension.ts | 3 ++- 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/extension/src-language-server/diagram-server.ts b/extension/src-language-server/diagram-server.ts index 23d2a46..2c26a61 100644 --- a/extension/src-language-server/diagram-server.ts +++ b/extension/src-language-server/diagram-server.ts @@ -234,15 +234,18 @@ export class PastaDiagramServer extends SnippetDiagramServer { options: this.state.options ?? {}, state: this.state, }); - newRoot.revision = ++this.state.revision; - this.state.currentRoot = newRoot; - await this.submitModel(this.state.currentRoot, true); - // ensures the the filterUCA option is correct - this.dispatch({ - kind: UpdateOptionsAction.KIND, - valuedSynthesisOptions: this.synthesisOptions?.getSynthesisOptions() ?? [], - clientId: this.clientId, - }); + // only update the view if the new root has children, otherwise an error occured + if (newRoot.children?.length !== 0) { + newRoot.revision = ++this.state.revision; + this.state.currentRoot = newRoot; + await this.submitModel(this.state.currentRoot, true); + // ensures the the filterUCA option is correct + this.dispatch({ + kind: UpdateOptionsAction.KIND, + valuedSynthesisOptions: this.synthesisOptions?.getSynthesisOptions() ?? [], + clientId: this.clientId, + }); + } } catch (err) { this.rejectRemoteRequest(undefined, err as Error); console.error("Failed to generate diagram:", err); diff --git a/extension/src-language-server/stpa/diagram/diagram-generator.ts b/extension/src-language-server/stpa/diagram/diagram-generator.ts index bf96a95..d8d366e 100644 --- a/extension/src-language-server/stpa/diagram/diagram-generator.ts +++ b/extension/src-language-server/stpa/diagram/diagram-generator.ts @@ -132,11 +132,20 @@ export class StpaDiagramGenerator extends SnippetGraphGenerator { */ protected generateRoot(args: GeneratorContext): SModelRoot { const { document } = args; - const model: Model = document.parseResult.value; - if (!this.idCache) { - this.idCache = args.idCache; + if (document.parseResult.lexerErrors.length === 0 && document.parseResult.parserErrors.length === 0) { + const model: Model = document.parseResult.value; + if (!this.idCache) { + this.idCache = args.idCache; + } + return this.generateGraph(model); + } else { + // return empty graph if the model is not valid + return { + type: "graph", + id: "root", + children: [], + }; } - return this.generateGraph(model); } /** diff --git a/extension/src/extension.ts b/extension/src/extension.ts index 200cf01..c4752db 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -320,7 +320,8 @@ function createLanguageClient(context: vscode.ExtensionContext): LanguageClient function registerTextEditorSync(manager: StpaLspVscodeExtension, context: vscode.ExtensionContext): void { context.subscriptions.push( - vscode.workspace.onDidSaveTextDocument(async document => { + vscode.workspace.onDidChangeTextDocument(async changeEvent => { + const document = changeEvent.document; if (document) { await languageClient.sendRequest("cutSets/reset"); const currentCursorPosition = vscode.window.activeTextEditor?.selection.active; From cf76985e10b12fc33c31ffd06ed03eb0965d55fc Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Mon, 24 Jun 2024 13:30:19 +0200 Subject: [PATCH 17/28] context table: selected control action is not resetted when updating --- extension/src-context-table/main.ts | 107 +++++++++++++++++++--------- 1 file changed, 72 insertions(+), 35 deletions(-) diff --git a/extension/src-context-table/main.ts b/extension/src-context-table/main.ts index fa24eaf..f1e15a3 100644 --- a/extension/src-context-table/main.ts +++ b/extension/src-context-table/main.ts @@ -15,16 +15,26 @@ * SPDX-License-Identifier: EPL-2.0 */ -import './css/table.css'; -import { Table } from '@kieler/table-webview/lib/table'; -import { SendContextTableDataAction } from './actions'; -import { createHeaderElement, createHeaderRow, createRow, createTable, createTHead, patch } from './html'; +import "./css/table.css"; +import { Table } from "@kieler/table-webview/lib/table"; +import { SendContextTableDataAction } from "./actions"; +import { createHeaderElement, createHeaderRow, createRow, createTable, createTHead, patch } from "./html"; import { - addSelector, addText, ContextCell, ContextTableControlAction, convertControlActionsToStrings, replaceSelector, ContextTableRule, ContextTableSystemVariables, - Type, ContextTableVariable, ContextTableVariableValues, Row -} from './utils'; + addSelector, + addText, + ContextCell, + ContextTableControlAction, + convertControlActionsToStrings, + replaceSelector, + ContextTableRule, + ContextTableSystemVariables, + Type, + ContextTableVariable, + ContextTableVariableValues, + Row, +} from "./utils"; import { VNode } from "snabbdom"; -import { createResults, determineUsedRules } from './context-table-logic'; +import { createResults, determineUsedRules } from "./context-table-logic"; interface vscode { postMessage(message: any): void; @@ -32,7 +42,6 @@ interface vscode { declare const vscode: vscode; export class ContextTable extends Table { - /** Ids for the html elements */ protected actionSelectorId = "select_action"; protected typeSelectorId = "select_type"; @@ -62,17 +71,17 @@ export class ContextTable extends Table { constructor() { super(); - document.addEventListener('click', (event) => { + document.addEventListener("click", event => { const node = event.target; const owner = (node as HTMLElement).parentElement; if (owner) { if (this.lastSelected) { - this.lastSelected.parentElement?.classList.remove('focused'); - this.lastSelected.classList.remove('selected'); + this.lastSelected.parentElement?.classList.remove("focused"); + this.lastSelected.classList.remove("selected"); } this.lastSelected = node as HTMLElement; - owner.classList.add('focused'); - (node as HTMLElement).classList.add('selected'); + owner.classList.add("focused"); + (node as HTMLElement).classList.add("selected"); } }); } @@ -115,7 +124,7 @@ export class ContextTable extends Table { protected initHtml(identifier: string): void { this.identifier = identifier; this.tableId = this.identifier + "_table"; - const mainDiv = document.getElementById(identifier + '_container'); + const mainDiv = document.getElementById(identifier + "_container"); if (mainDiv) { // Create text and selector element for selecting a control action addText(mainDiv, "Choose a Control Action:"); @@ -123,11 +132,18 @@ export class ContextTable extends Table { // Create text and selector element for selecting the action type addText(mainDiv, "Choose a Type:"); - addSelector(mainDiv, this.typeSelectorId, this.selectedType, ["provided", "not provided", "both"], "43px", "115px"); + addSelector( + mainDiv, + this.typeSelectorId, + this.selectedType, + ["provided", "not provided", "both"], + "43px", + "115px" + ); // add listener const htmlTypeSelector = document.getElementById(this.typeSelectorId) as HTMLSelectElement; - htmlTypeSelector.addEventListener('change', () => { + htmlTypeSelector.addEventListener("change", () => { switch (htmlTypeSelector.selectedIndex) { case 0: this.selectedType = Type.PROVIDED; @@ -160,14 +176,21 @@ export class ContextTable extends Table { if (selector) { // translate control actions to strings and add them to the selector const actions = convertControlActionsToStrings(this.controlActions); - replaceSelector(selector, actions, 0); + // set the selector to the previously selected action if it still exists + const currentSelected = selector.value; + const newIndex = actions.findIndex(action => action === currentSelected); + replaceSelector(selector, actions, newIndex === -1 ? 0 : newIndex); // update currently selected control action - this.updateControlActionSelection(0); + this.updateControlActionSelection(newIndex === -1 ? 0 : newIndex); // add listener const htmlActionSelector = document.getElementById(this.actionSelectorId) as HTMLSelectElement; - htmlActionSelector.addEventListener('change', () => { + if (newIndex !== -1) { + // update the selected control action to the one before the change + htmlActionSelector.value = actions[newIndex]; + } + htmlActionSelector.addEventListener("change", () => { this.updateControlActionSelection(htmlActionSelector.selectedIndex); this.updateTable(); }); @@ -178,7 +201,9 @@ export class ContextTable extends Table { * Sets the current variables based on the current controller. */ protected setCurrentVariables(): void { - const variables = this.systemVariables.find(systemVariable => systemVariable.system === this.selectedControlAction.controller)?.variables; + const variables = this.systemVariables.find( + systemVariable => systemVariable.system === this.selectedControlAction.controller + )?.variables; if (variables) { this.currentVariables = variables; } else { @@ -193,7 +218,12 @@ export class ContextTable extends Table { const headers: VNode[] = []; // the first header column is for the context and needs to span as many columns as there are context variables if (this.currentVariables.length > 0) { - const contextVariablesHeader = createHeaderElement("Context Variables", "0px", undefined, this.currentVariables.length); + const contextVariablesHeader = createHeaderElement( + "Context Variables", + "0px", + undefined, + this.currentVariables.length + ); headers.push(contextVariablesHeader); } @@ -295,8 +325,13 @@ export class ContextTable extends Table { for (let i = 0; i < this.contexts.length; i++) { const variables = this.contexts[i]; // determine the used rules - const usedRules = determineUsedRules(variables, this.rules, this.selectedControlAction.controller, - this.selectedControlAction.action, this.selectedType); + const usedRules = determineUsedRules( + variables, + this.rules, + this.selectedControlAction.controller, + this.selectedControlAction.action, + this.selectedType + ); // save them and map to the current context this.resultsRules.push(usedRules); this.resultRulesToContext.set(this.resultsRules.length - 1, i); @@ -317,7 +352,7 @@ export class ContextTable extends Table { } /** - * Creates a row instance based on the {@code resultRulesIndex}. + * Creates a row instance based on the {@code resultRulesIndex}. * @param resultRulesIndex Determines which resultsRule should be used. * @returns a row instance with the context and the result determines by {@code resultRulesIndex}. */ @@ -325,16 +360,15 @@ export class ContextTable extends Table { // get the context const context = this.contexts[this.resultRulesToContext.get(resultRulesIndex)!]; // get the hazards and rules - const results: { hazards: string[], rules: ContextTableRule[]; }[] = []; + const results: { hazards: string[]; rules: ContextTableRule[] }[] = []; this.resultsRules[resultRulesIndex].forEach(resultRules => { results.push({ hazards: resultRules.map(rule => rule.hazards.toString()), rules: resultRules }); }); return { variables: context, results: results }; } - /** - * Creates and appends one non-header row to the table. + * Creates and appends one non-header row to the table. * @param table The HTMLTableElement to apply the row to. * @param row The row to add. * @param id The id of the row. @@ -347,7 +381,9 @@ export class ContextTable extends Table { let cells: ContextCell[] = []; if (row.variables.length > 0) { // values of the context variables - cells = row.variables.map(variable => { return { cssClass: "context-variable", value: variable.value, colSpan: 1 }; }); + cells = row.variables.map(variable => { + return { cssClass: "context-variable", value: variable.value, colSpan: 1 }; + }); // append the result cells cells = cells.concat(createResults(row.results)); } else { @@ -372,7 +408,6 @@ export class ContextTable extends Table { patch(placeholderRow, htmlRow); } - /** * Generates all possible value combinations of the given variables. * @param variableIndex Index to determine from which variable to apply a value next. @@ -380,8 +415,12 @@ export class ContextTable extends Table { * @param determinedValues The already determined variable values. * @returns All possible value combinations of the given variables. */ - protected createContexts(variableIndex: number, variableValues: ContextTableVariableValues[], determinedValues: ContextTableVariable[]): (ContextTableVariable[])[] { - let result: (ContextTableVariable[])[] = []; + protected createContexts( + variableIndex: number, + variableValues: ContextTableVariableValues[], + determinedValues: ContextTableVariable[] + ): ContextTableVariable[][] { + let result: ContextTableVariable[][] = []; // load the values of the current recursion's variable const currentValues = variableValues[variableIndex].values; const lastVariable = variableIndex === variableValues.length - 1; @@ -403,8 +442,6 @@ export class ContextTable extends Table { } return result; } - - } -new ContextTable(); \ No newline at end of file +new ContextTable(); From 1e1d986548d1807abce519df0f5c0e20749233c7 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Mon, 24 Jun 2024 13:47:33 +0200 Subject: [PATCH 18/28] context table: no hard reset when file changes --- extension/src-context-table/html.tsx | 17 +++++++++++++---- extension/src-context-table/main.ts | 4 ++-- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/extension/src-context-table/html.tsx b/extension/src-context-table/html.tsx index eda7691..68a127d 100644 --- a/extension/src-context-table/html.tsx +++ b/extension/src-context-table/html.tsx @@ -52,13 +52,22 @@ export function createSelector(id: string, index: number, options: string[], top } } +/** + * Creates a table VNode enclosed by a div element with the 'context table' class. + * @param id The id of the table. + * @returns A table VNode enclosed by a div element. + */ +export function initContextTable(id: string): VNode { + return
{createTable(id)}
; +} + /** * Creates a table VNode. * @param id The id of the table. * @returns A table VNode. */ export function createTable(id: string): VNode { - return
; + return
; } /** @@ -87,7 +96,7 @@ export function createText(text: string): VNode { * @param colspan The colspan of the header. * @returns A header element. */ -export function createHeaderElement(header: string, top: string, rowspan?: number, colspan?: number) { +export function createHeaderElement(header: string, top: string, rowspan?: number, colspan?: number): VNode { if (rowspan && colspan) { return {header}; } else if (rowspan) { @@ -104,7 +113,7 @@ export function createHeaderElement(header: string, top: string, rowspan?: numbe * @param headers The headers of the header row. * @returns A header row element. */ -export function createHeaderRow(headers: VNode[]) { +export function createHeaderRow(headers: VNode[]): VNode { return {...headers} ; @@ -115,7 +124,7 @@ export function createHeaderRow(headers: VNode[]) { * @param headers The header rows * @returns A thead element containing the given header rows. */ -export function createTHead(headers: VNode[]) { +export function createTHead(headers: VNode[]): VNode { return {...headers}; } diff --git a/extension/src-context-table/main.ts b/extension/src-context-table/main.ts index f1e15a3..e2a0c79 100644 --- a/extension/src-context-table/main.ts +++ b/extension/src-context-table/main.ts @@ -18,7 +18,7 @@ import "./css/table.css"; import { Table } from "@kieler/table-webview/lib/table"; import { SendContextTableDataAction } from "./actions"; -import { createHeaderElement, createHeaderRow, createRow, createTable, createTHead, patch } from "./html"; +import { createHeaderElement, createHeaderRow, createRow, createTable, createTHead, initContextTable, patch } from "./html"; import { addSelector, addText, @@ -163,7 +163,7 @@ export class ContextTable extends Table { // create a table const placeholderTable = document.createElement("div"); mainDiv.append(placeholderTable); - const table = createTable(this.tableId); + const table = initContextTable(this.tableId); patch(placeholderTable, table); } } From 76493f9cb79e02752402d167d705a78265b87312 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Mon, 24 Jun 2024 14:34:37 +0200 Subject: [PATCH 19/28] context table: subcomponents are shown for action selection --- .../stpa/contextTable/context-dataProvider.ts | 38 +++++++++----- .../stpa/message-handler.ts | 4 +- extension/src/extension.ts | 49 +++++++++++-------- 3 files changed, 55 insertions(+), 36 deletions(-) diff --git a/extension/src-language-server/stpa/contextTable/context-dataProvider.ts b/extension/src-language-server/stpa/contextTable/context-dataProvider.ts index b072c56..a6e4d23 100644 --- a/extension/src-language-server/stpa/contextTable/context-dataProvider.ts +++ b/extension/src-language-server/stpa/contextTable/context-dataProvider.ts @@ -25,7 +25,7 @@ import { ContextTableVariable, ContextTableVariableValues, } from "../../../src-context-table/utils"; -import { Model } from "../../generated/ast"; +import { Model, Node } from "../../generated/ast"; import { getModel } from "../../utils"; import { StpaServices } from "../stpa-module"; @@ -60,6 +60,29 @@ export class ContextTableProvider { return range; } + /** + * Collects all control actions and variables of the given system component and its sub-components. + * @param component The system component to collect the data from. + * @param actions The array to store the control actions. + * @param variables The array to store the system variables. + */ + protected collectControlActionsAndVariables(component: Node, actions: ContextTableControlAction[], variables: ContextTableSystemVariables[]): void { + // control actions of the current system component + component.actions.forEach((action) => { + action.comms.forEach((command) => { + actions.push({ controller: component.name, action: command.name }); + }); + }); + // variables of the current system component + const variableValues: ContextTableVariableValues[] = []; + component.variables.forEach((variable) => { + variableValues.push({ name: variable.name, values: variable.values.map((value) => value.name) }); + }); + variables.push({ system: component.name, variables: variableValues }); + // recursive call for sub-components + component.children.forEach((subComponent) => this.collectControlActionsAndVariables(subComponent, actions, variables)); + } + /** * Collects all the data needed for constructing the context table. * @returns The data in a set of arrays. @@ -74,18 +97,7 @@ export class ContextTableProvider { // collect control actions and variables model.controlStructure?.nodes.forEach((systemComponent) => { - // control actions of the current system component - systemComponent.actions.forEach((action) => { - action.comms.forEach((command) => { - actions.push({ controller: systemComponent.name, action: command.name }); - }); - }); - // variables of the current system component - const variableValues: ContextTableVariableValues[] = []; - systemComponent.variables.forEach((variable) => { - variableValues.push({ name: variable.name, values: variable.values.map((value) => value.name) }); - }); - variables.push({ system: systemComponent.name, variables: variableValues }); + this.collectControlActionsAndVariables(systemComponent, actions, variables); }); // collect rules model.rules.forEach((rule) => { diff --git a/extension/src-language-server/stpa/message-handler.ts b/extension/src-language-server/stpa/message-handler.ts index fd50a88..f2a9187 100644 --- a/extension/src-language-server/stpa/message-handler.ts +++ b/extension/src-language-server/stpa/message-handler.ts @@ -110,8 +110,8 @@ function addTextChangeHandler(connection: Connection, stpaServices: StpaServices textChanges = []; } }); - // update the cursor position on save - connection.onNotification("editor/save", async (offset) => { + // update the cursor position on editor change + connection.onNotification("editor/change", async (offset) => { setCurrentCursorOffset(offset); }); } diff --git a/extension/src/extension.ts b/extension/src/extension.ts index c4752db..c8f8ef2 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -30,7 +30,7 @@ import { StpaResult } from "./report/utils"; import { createSBMs } from "./sbm/sbm-generation"; import { LTLFormula } from "./sbm/utils"; import { createFile, createOutputChannel, createQuickPickForWorkspaceOptions } from "./utils"; -import { UpdateDiagramAction } from './actions'; +import { UpdateDiagramAction } from "./actions"; let languageClient: LanguageClient; @@ -322,30 +322,37 @@ function registerTextEditorSync(manager: StpaLspVscodeExtension, context: vscode context.subscriptions.push( vscode.workspace.onDidChangeTextDocument(async changeEvent => { const document = changeEvent.document; - if (document) { - await languageClient.sendRequest("cutSets/reset"); - const currentCursorPosition = vscode.window.activeTextEditor?.selection.active; - if (currentCursorPosition) { - await languageClient.sendNotification("editor/save", document.offsetAt(currentCursorPosition)); - } - - // update diagram without reseting viewport - const mes: ActionMessage = { - clientId: manager.clientId!, - action: { - kind: UpdateDiagramAction.KIND, - } as UpdateDiagramAction, - }; - languageClient.sendNotification(acceptMessageType, mes); - - if (manager.contextTable) { - languageClient.sendNotification("contextTable/getData", document.uri.toString()); - } - } + updateViews(manager, document); }) ); } +async function updateViews(manager: StpaLspVscodeExtension, document: vscode.TextDocument): Promise { + if (document) { + // reset cut sets + await languageClient.sendRequest("cutSets/reset"); + // save the current cursor position + const currentCursorPosition = vscode.window.activeTextEditor?.selection.active; + if (currentCursorPosition) { + await languageClient.sendNotification("editor/change", document.offsetAt(currentCursorPosition)); + } + + // update diagram without reseting viewport + const mes: ActionMessage = { + clientId: manager.clientId!, + action: { + kind: UpdateDiagramAction.KIND, + } as UpdateDiagramAction, + }; + languageClient.sendNotification(acceptMessageType, mes); + + // update the context table + if (manager.contextTable) { + languageClient.sendNotification("contextTable/getData", document.uri.toString()); + } + } +} + /** * Registers the webview for the diagram snippets. * @param manager The manager that handles the webview panels. From 3fb5bbfeb9049753f53da5a058c1b96907ea3a5e Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Mon, 24 Jun 2024 14:49:16 +0200 Subject: [PATCH 20/28] diagram reload when opening new text document --- extension/src/extension.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/extension/src/extension.ts b/extension/src/extension.ts index c8f8ef2..583a4f4 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -323,6 +323,9 @@ function registerTextEditorSync(manager: StpaLspVscodeExtension, context: vscode vscode.workspace.onDidChangeTextDocument(async changeEvent => { const document = changeEvent.document; updateViews(manager, document); + }), + vscode.workspace.onDidOpenTextDocument(async document => { + manager.openDiagram(document.uri, { preserveFocus: true }); }) ); } From 28ade7dc7bbb4142c7da6dfeb32f97dbcd3d7fa4 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Mon, 24 Jun 2024 15:07:19 +0200 Subject: [PATCH 21/28] validator checks that actions of subcomps are referenced bz UCA --- .../stpa/stpa-validator.ts | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/extension/src-language-server/stpa/stpa-validator.ts b/extension/src-language-server/stpa/stpa-validator.ts index e47d89b..aeecbea 100644 --- a/extension/src-language-server/stpa/stpa-validator.ts +++ b/extension/src-language-server/stpa/stpa-validator.ts @@ -171,27 +171,42 @@ export class StpaValidator { ...model.allUCAs.map(alluca => alluca.system?.ref?.name + "." + alluca.action?.ref?.name), ...model.rules.map(rule => rule.system?.ref?.name + "." + rule.action?.ref?.name), ]; - model.controlStructure?.nodes.forEach(node => + this.checkControlActionsReferencedByUCA(model.controlStructure?.nodes ?? [], ucaActions, accept); + // check UCAs and DCAs + this.checkUCAsAndDCAs(model, accept); + } + + /** + * Check whether the control actions of a node are referenced by at least one UCA. + * @param nodes The nodes to check. + * @param ucaActions The control actions that are referenced by a UCA. + * @param accept + */ + protected checkControlActionsReferencedByUCA( + nodes: Node[], + ucaActions: string[], + accept: ValidationAcceptor + ): void { + nodes.forEach(node => { node.actions.forEach(action => action.comms.forEach(command => { const name = node.name + "." + command.name; if (!ucaActions.includes(name)) { - accept("warning", "This element is not referenced by a UCA", { + accept("warning", "This action is not referenced by a UCA", { node: command, property: "name", }); } }) - ) - ); - // check UCAs and DCAs - this.checkUCAsAndDCAs(model, accept); + ); + this.checkControlActionsReferencedByUCA(node.children, ucaActions, accept); + }); } /** * Checks the UCAs and DCAs for duplicates and conflicts. * @param model The model containing the UCAs and DCAs. - * @param accept + * @param accept */ protected checkUCAsAndDCAs(model: Model, accept: ValidationAcceptor): void { // check for duplicate ActionUCA definition From dca11f704108c239666e891ca3eafae93d081a87 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Mon, 24 Jun 2024 16:27:00 +0200 Subject: [PATCH 22/28] improved content assist for UCAs + DCAs --- .../stpa/contextTable/context-dataProvider.ts | 6 +-- .../src-language-server/stpa/diagram/utils.ts | 6 +-- .../stpa/modelChecking/model-checking.ts | 8 ++-- .../stpa/result-report/result-generator.ts | 6 +-- .../stpa/stpa-scopeProvider.ts | 45 ++++++++++++++----- .../stpa/stpa-validator.ts | 32 +++---------- .../src-language-server/stpa/stpa.langium | 7 ++- 7 files changed, 58 insertions(+), 52 deletions(-) diff --git a/extension/src-language-server/stpa/contextTable/context-dataProvider.ts b/extension/src-language-server/stpa/contextTable/context-dataProvider.ts index a6e4d23..3333dc8 100644 --- a/extension/src-language-server/stpa/contextTable/context-dataProvider.ts +++ b/extension/src-language-server/stpa/contextTable/context-dataProvider.ts @@ -104,9 +104,9 @@ export class ContextTableProvider { rule.contexts.forEach((context) => { // determine context variables const contextVariables: ContextTableVariable[] = []; - for (let i = 0; i < context.values.length; i++) { - if (context.vars[i].ref?.name) { - contextVariables.push({ name: context.vars[i].ref!.name, value: context.values[i] }); + for (const assignedValue of context.assignedValues) { + if (assignedValue.variable.ref?.name) { + contextVariables.push({ name: assignedValue.variable.ref.name, value: assignedValue.value.$refText }); } } //determine hazards diff --git a/extension/src-language-server/stpa/diagram/utils.ts b/extension/src-language-server/stpa/diagram/utils.ts index 5d94561..d870ede 100644 --- a/extension/src-language-server/stpa/diagram/utils.ts +++ b/extension/src-language-server/stpa/diagram/utils.ts @@ -252,9 +252,9 @@ export function createUCAContextDescription(uca: Context): string { break; } description += " in the context of "; - for (let i = 0; i < uca.vars.length; i++) { - description += uca.vars[i].$refText + "=" + uca.values[i]; - if (i < uca.vars.length - 1) { + for (let i = 0; i < uca.assignedValues.length; i++) { + description += uca.assignedValues[i].variable.$refText + "=" + uca.assignedValues[i].value.$refText; + if (i < uca.assignedValues.length - 1) { description += ", "; } } diff --git a/extension/src-language-server/stpa/modelChecking/model-checking.ts b/extension/src-language-server/stpa/modelChecking/model-checking.ts index c8f89b8..741841d 100644 --- a/extension/src-language-server/stpa/modelChecking/model-checking.ts +++ b/extension/src-language-server/stpa/modelChecking/model-checking.ts @@ -74,7 +74,7 @@ export async function generateLTLFormulae( let model = await getModel(uri, shared) as Model; // references are not found if the stpa file has not been opened since then the linter has not been activated yet - if (model.rules.length > 0 && model.rules[0]?.contexts[0]?.vars[0]?.ref === undefined) { + if (model.rules.length > 0 && model.rules[0]?.contexts[0]?.assignedValues[0]?.variable.ref === undefined) { // build document await shared.workspace.DocumentBuilder.update([URI.parse(uri)], []); // update the model @@ -124,9 +124,9 @@ async function translateRuleToLTLFormulas(rule: Rule | DCARule, map: Record { let contextText = ``; - for (let i = 0; i < context.values.length; i++) { - contextText += `${context.vars[i].$refText} = ${context.values[i]}`; - if (i !== context.values.length - 1) { + for (let i = 0; i < context.assignedValues.length; i++) { + contextText += `${context.assignedValues[i].variable.$refText} = ${context.assignedValues[i].value.$refText}`; + if (i !== context.assignedValues.length - 1) { contextText += ` and `; } } diff --git a/extension/src-language-server/stpa/stpa-scopeProvider.ts b/extension/src-language-server/stpa/stpa-scopeProvider.ts index 864a6d2..9fe6a34 100644 --- a/extension/src-language-server/stpa/stpa-scopeProvider.ts +++ b/extension/src-language-server/stpa/stpa-scopeProvider.ts @@ -29,9 +29,9 @@ import { } from "langium"; import { ActionUCAs, + AssignedValue, Command, Context, - DCAContext, DCARule, Graph, Hazard, @@ -43,11 +43,11 @@ import { SystemResponsibilities, UCA, Variable, + VariableValue, VerticalEdge, isActionUCAs, - isContext, + isAssignedValue, isControllerConstraint, - isDCAContext, isDCARule, isGraph, isHazardList, @@ -58,7 +58,7 @@ import { isSafetyConstraint, isSystemConstraint, isSystemResponsibilities, - isVerticalEdge, + isVerticalEdge } from "../generated/ast"; import { StpaServices } from "./stpa-module"; @@ -69,8 +69,9 @@ export class StpaScopeProvider extends DefaultScopeProvider { private SYS_CONSTRAINT_TYPE = SystemConstraint; private UCA_TYPE = UCA; private CONTEXT_TYPE = Context; - private VAR_TYPE = Variable; + private VARIABLE_TYPE = Variable; private NODE_TYPE = Node; + private VARIABLE_VALUE_TYPE = VariableValue; constructor(services: StpaServices) { super(services); @@ -79,6 +80,7 @@ export class StpaScopeProvider extends DefaultScopeProvider { getScope(context: ReferenceInfo): Scope { const referenceType = this.reflection.getReferenceType(context); const node = context.container; + const precomputed = getDocument(node).precomputedScopes; // get the root container which should be the Model let model = node.$container; @@ -105,8 +107,10 @@ export class StpaScopeProvider extends DefaultScopeProvider { return this.getHazards(model, precomputed); } else if ((isActionUCAs(node) || isRule(node) || isDCARule(node)) && referenceType === this.CA_TYPE) { return this.getCAs(node, precomputed); - } else if ((isContext(node) || isDCAContext(node)) && referenceType === this.VAR_TYPE) { - return this.getVars(node, precomputed); + } else if (isAssignedValue(node) && referenceType === this.VARIABLE_TYPE) { + return this.getVariables(node, precomputed); + } else if (isAssignedValue(node) && referenceType === this.VARIABLE_VALUE_TYPE) { + return this.getVariableValues(node, precomputed); } else if ( (isVerticalEdge(node) || isSystemResponsibilities(node) || @@ -230,24 +234,43 @@ export class StpaScopeProvider extends DefaultScopeProvider { /** * Creates scope containing the variables of the system component referenced by {@code node}. - * @param node Current Rule. + * @param node Current AssignedValue. * @param precomputed Precomputed Scope of the document. * @returns Scope containing all variables. */ - private getVars(node: Context | DCAContext, precomputed: PrecomputedScopes): Scope { + private getVariables(node: AssignedValue, precomputed: PrecomputedScopes): Scope { let allDescriptions: AstNodeDescription[] = []; - const varLists = node.$container.system.ref?.variables; + const varLists = node.$container.$container.system.ref?.variables; if (varLists) { for (const varList of varLists) { const currentNode: AstNode | undefined = varList; - const descs = this.getDescriptions(currentNode, this.VAR_TYPE, precomputed); + const descs = this.getDescriptions(currentNode, this.VARIABLE_TYPE, precomputed); allDescriptions = allDescriptions.concat(descs); } } return this.descriptionsToScope(allDescriptions); } + /** + * Creates scope containing the values of the variable referenced by {@code node}. + * @param node Current AssignedValue. + * @param precomputed Precomputed Scope of the document. + * @returns Scope containing all variable values. + */ + private getVariableValues( + node: AssignedValue, + precomputed: PrecomputedScopes + ): Scope { + let allDescriptions: AstNodeDescription[] = []; + const variable = node.variable.ref; + + const currentNode: AstNode | undefined = variable; + const descs = this.getDescriptions(currentNode, this.VARIABLE_VALUE_TYPE, precomputed); + allDescriptions = allDescriptions.concat(descs); + return this.descriptionsToScope(allDescriptions); + } + /** * Creates scope containing all hazards. * @param model Root of the STPA model. diff --git a/extension/src-language-server/stpa/stpa-validator.ts b/extension/src-language-server/stpa/stpa-validator.ts index aeecbea..a42f683 100644 --- a/extension/src-language-server/stpa/stpa-validator.ts +++ b/extension/src-language-server/stpa/stpa-validator.ts @@ -53,7 +53,6 @@ export class StpaValidationRegistry extends ValidationRegistry { ControllerConstraint: validator.checkControllerConstraints, HazardList: validator.checkHazardList, Node: validator.checkNode, - Context: validator.checkContext, Graph: validator.checkControlStructure, }; this.register(checks, validator); @@ -353,9 +352,9 @@ export class StpaValidator { protected isSameContext(context1: Context | DCAContext, context2: Context | DCAContext): boolean { let isSame = true; // check whether context1 is a subset of context2 - for (let i = 0; i < context1.vars.length; i++) { - const varIndex = context2.vars.findIndex(v => v.$refText === context1.vars[i].$refText); - if (varIndex === -1 || context2.values[varIndex] !== context1.values[i]) { + for (let i = 0; i < context1.assignedValues.length; i++) { + const varIndex = context2.assignedValues.findIndex(v => v.variable.$refText === context1.assignedValues[i].variable.$refText); + if (varIndex === -1 || context2.assignedValues[varIndex].value.$refText !== context1.assignedValues[i].value.$refText) { isSame = false; break; } @@ -363,9 +362,9 @@ export class StpaValidator { if (!isSame) { isSame = true; // check whether context2 is a subset of context1 - for (let i = 0; i < context2.vars.length; i++) { - const varIndex = context1.vars.findIndex(v => v.$refText === context2.vars[i].$refText); - if (varIndex === -1 || context1.values[varIndex] !== context2.values[i]) { + for (let i = 0; i < context2.assignedValues.length; i++) { + const varIndex = context1.assignedValues.findIndex(v => v.variable.$refText === context2.assignedValues[i].variable.$refText); + if (varIndex === -1 || context1.assignedValues[varIndex].value.$refText !== context2.assignedValues[i].value.$refText) { isSame = false; break; } @@ -374,25 +373,6 @@ export class StpaValidator { return isSame; } - /** - * Validates the variable values of {@code context}. - * @param context The Context to check. - * @param accept - */ - checkContext(context: Context, accept: ValidationAcceptor): void { - for (let i = 0; i < context.vars.length; i++) { - const variable = context.vars[i]; - const variableValues = variable.ref?.values.map(value => value.name); - // the value of the variable in the context should be one of the values that are stated in the definition of the variable - if (!variableValues?.includes(context.values[i])) { - accept("error", "This variable has an invalid value.", { - node: context, - range: variable.$refNode?.range, - }); - } - } - } - /** * Executes validation checks for a hazard. * @param hazard The Hazard to check. diff --git a/extension/src-language-server/stpa/stpa.langium b/extension/src-language-server/stpa/stpa.langium index 801a395..52015d1 100644 --- a/extension/src-language-server/stpa/stpa.langium +++ b/extension/src-language-server/stpa/stpa.langium @@ -39,7 +39,7 @@ Rule: '}'; Context: - name=ID '['vars+=[Variable] '=' values+=QualifiedName (',' vars+=[Variable] '=' values+=QualifiedName)*']' + name=ID '['assignedValues+=AssignedValue (',' assignedValues+=AssignedValue)*']' list=HazardList; Loss: @@ -122,7 +122,10 @@ DCARule: '}'; DCAContext: - name=ID '['vars+=[Variable] '=' values+=QualifiedName (',' vars+=[Variable] '=' values+=QualifiedName)*']'; + name=ID '[' assignedValues+=AssignedValue (',' assignedValues+=AssignedValue)*']'; + +AssignedValue: + variable=[Variable] '=' value=[VariableValue:QualifiedName]; ControllerConstraint: name=ID description=STRING '['refs+=[UCA] (',' refs+=[UCA])*']'; From 27cb8026dfad17117f2bf5d20cf687584eecac1c Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Tue, 25 Jun 2024 12:38:39 +0200 Subject: [PATCH 23/28] updates options for svg generation --- .../src-language-server/diagram-server.ts | 4 +++ .../stpa/result-report/svg-generator.ts | 34 +++++++++++++++++++ .../src-language-server/synthesis-options.ts | 32 ++++++++++++----- 3 files changed, 62 insertions(+), 8 deletions(-) diff --git a/extension/src-language-server/diagram-server.ts b/extension/src-language-server/diagram-server.ts index 2c26a61..a34c64e 100644 --- a/extension/src-language-server/diagram-server.ts +++ b/extension/src-language-server/diagram-server.ts @@ -38,6 +38,7 @@ import { SYSTEM_CONSTRAINT_PATH, resetOptions, saveOptions, + setAllScenariosOptions, setControlStructureOptions, setControllerConstraintWithFilteredUcaGraphOptions, setFilteredUcaGraphOptions, @@ -167,6 +168,9 @@ export class PastaDiagramServer extends SnippetDiagramServer { // scenario with hazard svg graph setScenarioWithNoUCAGraphOptions(this.synthesisOptions); await this.createSVG(setSynthesisOption, action.uri, SCENARIO_WITH_HAZARDS_PATH); + // all scenarios svg graph + setAllScenariosOptions(this.synthesisOptions); + await this.createSVG(setSynthesisOption, action.uri, FILTERED_SCENARIO_PATH("all UCAs")); // safety requirement svg graph setSafetyRequirementGraphOptions(this.synthesisOptions); diff --git a/extension/src-language-server/stpa/result-report/svg-generator.ts b/extension/src-language-server/stpa/result-report/svg-generator.ts index 0890657..3eb77a6 100644 --- a/extension/src-language-server/stpa/result-report/svg-generator.ts +++ b/extension/src-language-server/stpa/result-report/svg-generator.ts @@ -16,6 +16,7 @@ */ import { SetSynthesisOptionsAction } from "../../options/actions"; +import { labelManagementValue } from '../../synthesis-options'; import { StpaSynthesisOptions } from "../diagram/stpa-synthesis-options"; /* the paths for the several diagrams of the STPA aspects */ @@ -79,6 +80,7 @@ export function resetOptions(options: StpaSynthesisOptions): SetSynthesisOptions export function setControlStructureOptions(options: StpaSynthesisOptions): void { options.setShowRelationshipGraph(false); options.setShowControlStructure(true); + options.setLabelManagement(labelManagementValue.NO_LABELS); } /** @@ -94,7 +96,9 @@ export function setHazardGraphOptions(options: StpaSynthesisOptions): void { options.setShowUCAs(false); options.setShowContCons(false); options.setShowScenarios(false); + options.setShowScenariosWithHazard(false); options.setShowSafetyConstraints(false); + options.setLabelManagement(labelManagementValue.NO_LABELS); } /** @@ -110,7 +114,9 @@ export function setSystemConstraintGraphOptions(options: StpaSynthesisOptions): options.setShowUCAs(false); options.setShowContCons(false); options.setShowScenarios(false); + options.setShowScenariosWithHazard(false); options.setShowSafetyConstraints(false); + options.setLabelManagement(labelManagementValue.NO_LABELS); } /** @@ -126,7 +132,9 @@ export function setResponsibilityGraphOptions(options: StpaSynthesisOptions): vo options.setShowUCAs(false); options.setShowContCons(false); options.setShowScenarios(false); + options.setShowScenariosWithHazard(false); options.setShowSafetyConstraints(false); + options.setLabelManagement(labelManagementValue.NO_LABELS); } /** @@ -143,7 +151,9 @@ export function setFilteredUcaGraphOptions(options: StpaSynthesisOptions, value: options.setShowUCAs(true); options.setShowContCons(false); options.setShowScenarios(false); + options.setShowScenariosWithHazard(false); options.setShowSafetyConstraints(false); + options.setLabelManagement(labelManagementValue.NO_LABELS); } /** @@ -160,7 +170,9 @@ export function setControllerConstraintWithFilteredUcaGraphOptions(options: Stpa options.setShowUCAs(true); options.setShowContCons(true); options.setShowScenarios(false); + options.setShowScenariosWithHazard(false); options.setShowSafetyConstraints(false); + options.setLabelManagement(labelManagementValue.NO_LABELS); } /** @@ -179,6 +191,7 @@ export function setScenarioWithFilteredUCAGraphOptions(options: StpaSynthesisOpt options.setShowScenarios(true); options.setShowScenariosWithHazard(false); options.setShowSafetyConstraints(false); + options.setLabelManagement(labelManagementValue.NO_LABELS); } /** @@ -197,6 +210,25 @@ export function setScenarioWithNoUCAGraphOptions(options: StpaSynthesisOptions): options.setShowScenarios(true); options.setShowScenariosWithHazard(true); options.setShowSafetyConstraints(false); + options.setLabelManagement(labelManagementValue.NO_LABELS); +} + +/** + * Sets the values of {@code options} such that the relationship graph is reduced to the loss scenarios without system-level constraints. + * @param options The synthesis options. + */ +export function setAllScenariosOptions(options: StpaSynthesisOptions): void { + options.setShowRelationshipGraph(true); + options.setShowControlStructure(false); + options.setFilteringUCAs("all UCAs"); + options.setShowSysCons(false); + options.setShowResps(true); + options.setShowUCAs(true); + options.setShowContCons(false); + options.setShowScenarios(true); + options.setShowScenariosWithHazard(true); + options.setShowSafetyConstraints(false); + options.setLabelManagement(labelManagementValue.NO_LABELS); } /** @@ -214,6 +246,7 @@ export function setSafetyRequirementGraphOptions(options: StpaSynthesisOptions): options.setShowScenarios(true); options.setShowScenariosWithHazard(true); options.setShowSafetyConstraints(true); + options.setLabelManagement(labelManagementValue.NO_LABELS); } /** @@ -231,4 +264,5 @@ export function setRelationshipGraphOptions(options: StpaSynthesisOptions): void options.setShowScenarios(true); options.setShowScenariosWithHazard(true); options.setShowSafetyConstraints(true); + options.setLabelManagement(labelManagementValue.NO_LABELS); } diff --git a/extension/src-language-server/synthesis-options.ts b/extension/src-language-server/synthesis-options.ts index 4821279..0a125ca 100644 --- a/extension/src-language-server/synthesis-options.ts +++ b/extension/src-language-server/synthesis-options.ts @@ -63,7 +63,7 @@ const labelShorteningWidthOption: ValuedSynthesisOption = { /** * Option to determine the display of node labels. - * It can be original labels (whole label in one line), wrapping (label is wrapped into multiple lines), + * It can be original labels (whole label in one line), wrapping (label is wrapped into multiple lines), * truncate (label is truncated) or no labels. */ const labelManagementOption: ValuedSynthesisOption = { @@ -109,11 +109,7 @@ export class SynthesisOptions { protected options: ValuedSynthesisOption[]; constructor() { - this.options = [ - layoutCategoryOption, - labelManagementOption, - labelShorteningWidthOption, - modelOrderOption]; + this.options = [layoutCategoryOption, labelManagementOption, labelShorteningWidthOption, modelOrderOption]; } getSynthesisOptions(): ValuedSynthesisOption[] { @@ -121,12 +117,12 @@ export class SynthesisOptions { } protected getOption(id: string): ValuedSynthesisOption | undefined { - const option = this.options.find((option) => option.synthesisOption.id === id); + const option = this.options.find(option => option.synthesisOption.id === id); return option; } getLabelManagement(): labelManagementValue { - const option = this.options.find((option) => option.synthesisOption.id === labelManagementID); + const option = this.options.find(option => option.synthesisOption.id === labelManagementID); switch (option?.currentValue) { case "Original Labels": return labelManagementValue.ORIGINAL; @@ -140,6 +136,26 @@ export class SynthesisOptions { return option?.currentValue; } + setLabelManagement(value: labelManagementValue): void { + const option = this.options.find(option => option.synthesisOption.id === labelManagementID); + if (option) { + switch (value) { + case labelManagementValue.ORIGINAL: + option.currentValue = "Original Labels"; + break; + case labelManagementValue.WRAPPING: + option.currentValue = "Wrapping"; + break; + case labelManagementValue.TRUNCATE: + option.currentValue = "Truncate"; + break; + case labelManagementValue.NO_LABELS: + option.currentValue = "No Labels"; + break; + } + } + } + getLabelShorteningWidth(): number { return this.getOption(labelShorteningWidthID)?.currentValue; } From 322045e7c7cc47d39147ef7520e9dff729407644 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Tue, 25 Jun 2024 12:48:04 +0200 Subject: [PATCH 24/28] SVG Gen: fixed option + update view only when not triggered by svg gen --- extension/src-language-server/synthesis-options.ts | 1 + extension/src/extension.ts | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/extension/src-language-server/synthesis-options.ts b/extension/src-language-server/synthesis-options.ts index 0a125ca..624f5bf 100644 --- a/extension/src-language-server/synthesis-options.ts +++ b/extension/src-language-server/synthesis-options.ts @@ -153,6 +153,7 @@ export class SynthesisOptions { option.currentValue = "No Labels"; break; } + option.synthesisOption.currentValue = option.currentValue; } } diff --git a/extension/src/extension.ts b/extension/src/extension.ts index 583a4f4..dfb03b4 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -321,8 +321,14 @@ function createLanguageClient(context: vscode.ExtensionContext): LanguageClient function registerTextEditorSync(manager: StpaLspVscodeExtension, context: vscode.ExtensionContext): void { context.subscriptions.push( vscode.workspace.onDidChangeTextDocument(async changeEvent => { - const document = changeEvent.document; - updateViews(manager, document); + const svgGeneration = changeEvent.contentChanges[0].text.startsWith( + ' { manager.openDiagram(document.uri, { preserveFocus: true }); From 06e6940e92897ae23177e60756e1174559aae7d1 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Wed, 26 Jun 2024 11:09:42 +0200 Subject: [PATCH 25/28] nodes are not aligned based on out/in-coming edges --- extension/src-language-server/stpa/diagram/layout-config.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/extension/src-language-server/stpa/diagram/layout-config.ts b/extension/src-language-server/stpa/diagram/layout-config.ts index ce8376c..208836b 100644 --- a/extension/src-language-server/stpa/diagram/layout-config.ts +++ b/extension/src-language-server/stpa/diagram/layout-config.ts @@ -99,6 +99,7 @@ export class StpaLayoutConfigurator extends DefaultLayoutConfigurator { */ processModelParentNodeOptions(snode: SNode): LayoutOptions | undefined { return { + "org.eclipse.elk.alignment": "CENTER", "org.eclipse.elk.layered.considerModelOrder.strategy": "NODES_AND_EDGES", "org.eclipse.elk.layered.crossingMinimization.forceNodeModelOrder": "true", // TODO: wait for node size fix in elkjs @@ -111,6 +112,7 @@ export class StpaLayoutConfigurator extends DefaultLayoutConfigurator { */ protected invisibleSubcomponentOptions(snode: SNode): LayoutOptions | undefined { return { + "org.eclipse.elk.alignment": "CENTER", "org.eclipse.elk.partitioning.activate": "true", "org.eclipse.elk.direction": "DOWN", "org.eclipse.elk.portConstraints": "FIXED_SIDE", @@ -131,6 +133,7 @@ export class StpaLayoutConfigurator extends DefaultLayoutConfigurator { return this.parentSTPANodeOptions(node); } else { return { + "org.eclipse.elk.alignment": "CENTER", "org.eclipse.elk.nodeLabels.placement": "INSIDE V_CENTER H_CENTER", "org.eclipse.elk.partitioning.partition": "" + node.level, "org.eclipse.elk.portConstraints": "FIXED_SIDE", @@ -145,6 +148,7 @@ export class StpaLayoutConfigurator extends DefaultLayoutConfigurator { protected parentSTPANodeOptions(node: STPANode): LayoutOptions { // options for nodes in the STPA graphs that have children const options: LayoutOptions = { + "org.eclipse.elk.alignment": "CENTER", "org.eclipse.elk.direction": "UP", "org.eclipse.elk.nodeLabels.placement": "INSIDE V_TOP H_CENTER", "org.eclipse.elk.partitioning.partition": "" + node.level, @@ -171,6 +175,7 @@ export class StpaLayoutConfigurator extends DefaultLayoutConfigurator { */ protected csNodeOptions(node: CSNode): LayoutOptions { const options: LayoutOptions = { + "org.eclipse.elk.alignment": "CENTER", "org.eclipse.elk.partitioning.partition": "" + node.level, "org.eclipse.elk.nodeSize.constraints": "NODE_LABELS", // edges do no start at the border of the node From fffa74fcb57a821ace7bc7ebee98a29b1297ce3e Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Thu, 27 Jun 2024 09:23:35 +0200 Subject: [PATCH 26/28] ID enforcer expanded with IDs for DCAs and DCARules --- .../src-language-server/stpa/ID-enforcer.ts | 19 ++++++++++++++----- extension/src-language-server/stpa/utils.ts | 6 +++++- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/extension/src-language-server/stpa/ID-enforcer.ts b/extension/src-language-server/stpa/ID-enforcer.ts index 668c3dc..69a8e51 100644 --- a/extension/src-language-server/stpa/ID-enforcer.ts +++ b/extension/src-language-server/stpa/ID-enforcer.ts @@ -18,7 +18,7 @@ import { isCompositeCstNode, LangiumDocument } from "langium"; import { TextDocumentContentChangeEvent } from "vscode"; import { Range, RenameParams, TextEdit } from "vscode-languageserver"; -import { Hazard, isHazard, isSystemConstraint, LossScenario, Model, SystemConstraint } from "../generated/ast"; +import { Hazard, isHazard, isRule, isSystemConstraint, LossScenario, Model, SystemConstraint } from "../generated/ast"; import { StpaServices } from "./stpa-module"; import { collectElementsWithSubComps, elementWithName, elementWithRefs } from "./utils"; @@ -30,10 +30,13 @@ class IDPrefix { static Hazard = "H"; static SystemConstraint = "SC"; static Responsibility = "R"; + static Rule = "RL"; static UCA = "UCA"; static ControllerConstraint = "C"; static LossScenario = "Scenario"; static SafetyRequirement = "SR"; + static DCA = "DCA"; + static DCARule = "DRL"; } /** @@ -81,7 +84,7 @@ export class IDEnforcer { // rule IDs must be handled separately if (modifiedAspect.ruleElements.length !== 0) { - edits = edits.concat(await this.enforceIDsOnElements(modifiedAspect.ruleElements, "RL", change)); + edits = edits.concat(await this.enforceIDsOnElements(modifiedAspect.ruleElements, isRule(modifiedAspect.ruleElements[0]) ? IDPrefix.Rule : IDPrefix.DCARule, change)); } } } @@ -303,8 +306,9 @@ export class IDEnforcer { // offsets of the different aspects to determine the aspect for the given offset const subtractOffset = 5; + const dcaOffset = model.allDCAs.length !== 0 && model.allDCAs[0].$cstNode?.offset ? model.allDCAs[0].$cstNode.offset - subtractOffset : Number.MAX_VALUE; const safetyConsOffset = model.safetyCons.length !== 0 && model.safetyCons[0].$cstNode?.offset ? - model.safetyCons[0].$cstNode.offset - subtractOffset : Number.MAX_VALUE; + model.safetyCons[0].$cstNode.offset - subtractOffset : dcaOffset; const scenarioOffset = model.scenarios.length !== 0 && model.scenarios[0].$cstNode?.offset ? model.scenarios[0].$cstNode.offset - subtractOffset : safetyConsOffset; const ucaConstraintOffset = model.controllerConstraints.length !== 0 && model.controllerConstraints[0].$cstNode?.offset ? @@ -320,7 +324,7 @@ export class IDEnforcer { model.hazards[0].$cstNode.offset - subtractOffset : constraintOffset; // determine the aspect for the given offset - if (!hazardOffset || !constraintOffset || !responsibilitiesOffset || !ucaOffset || !ucaConstraintOffset || !scenarioOffset || !safetyConsOffset) { + if (!hazardOffset || !constraintOffset || !responsibilitiesOffset || !ucaOffset || !ucaConstraintOffset || !scenarioOffset || !safetyConsOffset || !dcaOffset) { console.log("Offset could not be determined for all aspects."); return undefined; } else if (offset < hazardOffset) { @@ -351,9 +355,14 @@ export class IDEnforcer { } else if (offset < safetyConsOffset && offset > scenarioOffset) { elements = model.scenarios; prefix = IDPrefix.LossScenario; - } else { + } else if (offset < dcaOffset && offset > safetyConsOffset) { elements = model.safetyCons; prefix = IDPrefix.SafetyRequirement; + } else { + elements = model.allDCAs.flatMap(dca => dca.contexts); + prefix = IDPrefix.DCA; + // rules must be handled separately since they are mixed with the DCAs + ruleElements = model.allDCAs; } return { elements, prefix, ruleElements }; diff --git a/extension/src-language-server/stpa/utils.ts b/extension/src-language-server/stpa/utils.ts index e79d4da..841a47b 100644 --- a/extension/src-language-server/stpa/utils.ts +++ b/extension/src-language-server/stpa/utils.ts @@ -22,6 +22,8 @@ import { Command, Context, ControllerConstraint, + DCAContext, + DCARule, Graph, Hazard, HazardList, @@ -62,7 +64,9 @@ export type elementWithName = | Graph | Command | Context - | Rule; + | Rule + | DCAContext + | DCARule; export type elementWithRefs = | Hazard | SystemConstraint From bbff8d2c243a6af0adbce0c5cf0b3379208530c0 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Thu, 27 Jun 2024 09:23:44 +0200 Subject: [PATCH 27/28] formatting --- .../src-language-server/stpa/ID-enforcer.ts | 148 +++++++++++++----- 1 file changed, 110 insertions(+), 38 deletions(-) diff --git a/extension/src-language-server/stpa/ID-enforcer.ts b/extension/src-language-server/stpa/ID-enforcer.ts index 69a8e51..a9766c7 100644 --- a/extension/src-language-server/stpa/ID-enforcer.ts +++ b/extension/src-language-server/stpa/ID-enforcer.ts @@ -43,7 +43,6 @@ class IDPrefix { * Contains methods to enforce correct IDs on STPA components. */ export class IDEnforcer { - /** langium services for the stpa DSL */ protected readonly stpaServices: StpaServices; @@ -65,10 +64,15 @@ export class IDEnforcer { async enforceIDs(changes: TextDocumentContentChangeEvent[], uri: string): Promise { // update current document information this.currentUri = uri; - this.currentDocument = this.stpaServices.shared.workspace.LangiumDocuments.getOrCreateDocument(uri as any) as LangiumDocument; + this.currentDocument = this.stpaServices.shared.workspace.LangiumDocuments.getOrCreateDocument( + uri as any + ) as LangiumDocument; // ID enforcement can only be done if the parser has no errors. Otherwise other syntax elements than IDs are interpreted as IDs. - if (this.currentDocument.parseResult.lexerErrors.length !== 0 || this.currentDocument.parseResult.parserErrors.length !== 0) { + if ( + this.currentDocument.parseResult.lexerErrors.length !== 0 || + this.currentDocument.parseResult.parserErrors.length !== 0 + ) { return []; } @@ -84,7 +88,13 @@ export class IDEnforcer { // rule IDs must be handled separately if (modifiedAspect.ruleElements.length !== 0) { - edits = edits.concat(await this.enforceIDsOnElements(modifiedAspect.ruleElements, isRule(modifiedAspect.ruleElements[0]) ? IDPrefix.Rule : IDPrefix.DCARule, change)); + edits = edits.concat( + await this.enforceIDsOnElements( + modifiedAspect.ruleElements, + isRule(modifiedAspect.ruleElements[0]) ? IDPrefix.Rule : IDPrefix.DCARule, + change + ) + ); } } } @@ -98,7 +108,11 @@ export class IDEnforcer { * @param change The text change that triggered the enforcing of IDs. Needed because only the elements below the modified one are checked. * @param edits The edits needed to enforce the calculated IDs. */ - protected async enforceIDsOnElements(elements: elementWithName[], prefix: string, change: TextDocumentContentChangeEvent): Promise { + protected async enforceIDsOnElements( + elements: elementWithName[], + prefix: string, + change: TextDocumentContentChangeEvent + ): Promise { let edits: TextEdit[] = []; // index of the modified element let index = elements.findIndex(element => element.$cstNode && element.$cstNode.offset >= change.rangeOffset); @@ -109,7 +123,7 @@ export class IDEnforcer { edits = edits.concat(this.deleteReferences(prefix + (index + 1))); } else { // compute edits for renaming the elements below the modified element - edits = await this.enforceIDsBelowModifiedElement(index, elements, prefix, change.text === ''); + edits = await this.enforceIDsBelowModifiedElement(index, elements, prefix, change.text === ""); } // create edit to rename the modified element @@ -146,11 +160,16 @@ export class IDEnforcer { * @param index The index of the first element that should be checked. * @param elements The elements which IDs should be checked and possibley updated. * @param prefix The prefix for the new ID of the elements. The new ID will be the prefix + index of the element. - * @param decrease Determines where to start. If "false", the last element is the start element. + * @param decrease Determines where to start. If "false", the last element is the start element. * Otherwise the first element after the modified one is the start element. * @returns The edits for renaming the elements. */ - protected async enforceIDsBelowModifiedElement(index: number, elements: elementWithName[], prefix: string, decrease: boolean): Promise { + protected async enforceIDsBelowModifiedElement( + index: number, + elements: elementWithName[], + prefix: string, + decrease: boolean + ): Promise { // guarantee that the index is not out of bounds if (index < 0) { index = 0; @@ -180,7 +199,7 @@ export class IDEnforcer { const renameEdits = await this.renameID(elementToRename, prefix, i + 1); edits = edits.concat(renameEdits); } else { - // if the element to rename has the same name as the modified element it must be renamed manually + // if the element to rename has the same name as the modified element it must be renamed manually // and the references are updated by calling the rename function with the modified element if (elementToRename.$cstNode) { // rename current element manually @@ -195,7 +214,13 @@ export class IDEnforcer { // delete the edit that renames the modified element (undo the renaming for this element) if (modifiedElement.$cstNode) { const range = modifiedElement.$cstNode.range; - renameEdits = renameEdits.filter(edit => !(edit.range.start.line === range.start.line && edit.range.start.character === range.start.character)); + renameEdits = renameEdits.filter( + edit => + !( + edit.range.start.line === range.start.line && + edit.range.start.character === range.start.character + ) + ); } // add the edits to the list edits = edits.concat(renameEdits); @@ -217,7 +242,11 @@ export class IDEnforcer { const hazards = collectElementsWithSubComps(model.hazards) as Hazard[]; const sysCons = collectElementsWithSubComps(model.systemLevelConstraints) as SystemConstraint[]; const responsibilities = model.responsibilities?.map(r => r.responsiblitiesForOneSystem).flat(1); - const ucas = model.allUCAs?.map(sysUCA => sysUCA.providingUcas.concat(sysUCA.notProvidingUcas, sysUCA.wrongTimingUcas, sysUCA.continousUcas)).flat(1); + const ucas = model.allUCAs + ?.map(sysUCA => + sysUCA.providingUcas.concat(sysUCA.notProvidingUcas, sysUCA.wrongTimingUcas, sysUCA.continousUcas) + ) + .flat(1); const contexts = model.rules?.map(rule => rule.contexts).flat(1); const scenarioHazards = model.scenarios.map(scenario => scenario.list); const scenarioUCAs = model.scenarios; @@ -232,7 +261,7 @@ export class IDEnforcer { ...model.controllerConstraints, ...scenarioHazards, ...scenarioUCAs, - ...model.safetyCons + ...model.safetyCons, ]; // compute edits to delete references to the given name @@ -272,7 +301,7 @@ export class IDEnforcer { const params: RenameParams = { textDocument: this.currentDocument.textDocument, position: element.$cstNode.range.start, - newName: prefix + counter + newName: prefix + counter, }; // compute the textedits for renaming const edit = await this.stpaServices.lsp.RenameProvider!.rename(this.currentDocument, params); @@ -297,34 +326,63 @@ export class IDEnforcer { * @param offset Offset in the file, for which the corresponding aspect should be determined. * @returns the elements and prefix of the STPA aspect corresponding to the given offset. */ - protected findModifiedAspect(offset: number): { elements: elementWithName[], prefix: string, ruleElements: elementWithName[]; } | undefined { + protected findModifiedAspect( + offset: number + ): { elements: elementWithName[]; prefix: string; ruleElements: elementWithName[] } | undefined { const model: Model = this.currentDocument.parseResult.value; let elements: elementWithName[] = []; let prefix = ""; let ruleElements: elementWithName[] = []; - // offsets of the different aspects to determine the aspect for the given offset + // offsets of the different aspects to determine the aspect for the given offset const subtractOffset = 5; - const dcaOffset = model.allDCAs.length !== 0 && model.allDCAs[0].$cstNode?.offset ? model.allDCAs[0].$cstNode.offset - subtractOffset : Number.MAX_VALUE; - const safetyConsOffset = model.safetyCons.length !== 0 && model.safetyCons[0].$cstNode?.offset ? - model.safetyCons[0].$cstNode.offset - subtractOffset : dcaOffset; - const scenarioOffset = model.scenarios.length !== 0 && model.scenarios[0].$cstNode?.offset ? - model.scenarios[0].$cstNode.offset - subtractOffset : safetyConsOffset; - const ucaConstraintOffset = model.controllerConstraints.length !== 0 && model.controllerConstraints[0].$cstNode?.offset ? - model.controllerConstraints[0].$cstNode.offset - subtractOffset : scenarioOffset; - const ucaOffset = model.rules.length !== 0 && model.rules[0].$cstNode?.offset ? - model.rules[0].$cstNode.offset - subtractOffset : (model.allUCAs.length !== 0 && model.allUCAs[0].$cstNode?.offset ? - model.allUCAs[0].$cstNode.offset - subtractOffset : ucaConstraintOffset); - const responsibilitiesOffset = model.responsibilities.length !== 0 && model.responsibilities[0].$cstNode?.offset ? - model.responsibilities[0].$cstNode.offset - subtractOffset : ucaOffset; - const constraintOffset = model.systemLevelConstraints.length !== 0 && model.systemLevelConstraints[0].$cstNode?.offset ? - model.systemLevelConstraints[0].$cstNode.offset - subtractOffset : responsibilitiesOffset; - const hazardOffset = model.hazards.length !== 0 && model.hazards[0].$cstNode?.offset ? - model.hazards[0].$cstNode.offset - subtractOffset : constraintOffset; + const dcaOffset = + model.allDCAs.length !== 0 && model.allDCAs[0].$cstNode?.offset + ? model.allDCAs[0].$cstNode.offset - subtractOffset + : Number.MAX_VALUE; + const safetyConsOffset = + model.safetyCons.length !== 0 && model.safetyCons[0].$cstNode?.offset + ? model.safetyCons[0].$cstNode.offset - subtractOffset + : dcaOffset; + const scenarioOffset = + model.scenarios.length !== 0 && model.scenarios[0].$cstNode?.offset + ? model.scenarios[0].$cstNode.offset - subtractOffset + : safetyConsOffset; + const ucaConstraintOffset = + model.controllerConstraints.length !== 0 && model.controllerConstraints[0].$cstNode?.offset + ? model.controllerConstraints[0].$cstNode.offset - subtractOffset + : scenarioOffset; + const ucaOffset = + model.rules.length !== 0 && model.rules[0].$cstNode?.offset + ? model.rules[0].$cstNode.offset - subtractOffset + : model.allUCAs.length !== 0 && model.allUCAs[0].$cstNode?.offset + ? model.allUCAs[0].$cstNode.offset - subtractOffset + : ucaConstraintOffset; + const responsibilitiesOffset = + model.responsibilities.length !== 0 && model.responsibilities[0].$cstNode?.offset + ? model.responsibilities[0].$cstNode.offset - subtractOffset + : ucaOffset; + const constraintOffset = + model.systemLevelConstraints.length !== 0 && model.systemLevelConstraints[0].$cstNode?.offset + ? model.systemLevelConstraints[0].$cstNode.offset - subtractOffset + : responsibilitiesOffset; + const hazardOffset = + model.hazards.length !== 0 && model.hazards[0].$cstNode?.offset + ? model.hazards[0].$cstNode.offset - subtractOffset + : constraintOffset; // determine the aspect for the given offset - if (!hazardOffset || !constraintOffset || !responsibilitiesOffset || !ucaOffset || !ucaConstraintOffset || !scenarioOffset || !safetyConsOffset || !dcaOffset) { + if ( + !hazardOffset || + !constraintOffset || + !responsibilitiesOffset || + !ucaOffset || + !ucaConstraintOffset || + !scenarioOffset || + !safetyConsOffset || + !dcaOffset + ) { console.log("Offset could not be determined for all aspects."); return undefined; } else if (offset < hazardOffset) { @@ -337,14 +395,20 @@ export class IDEnforcer { prefix = modified.prefix; } else if (offset < responsibilitiesOffset && offset > constraintOffset) { // sub-components must be considered when determining the affected elements - const modified = this.findAffectedSubComponents(model.systemLevelConstraints, IDPrefix.SystemConstraint, offset); + const modified = this.findAffectedSubComponents( + model.systemLevelConstraints, + IDPrefix.SystemConstraint, + offset + ); elements = modified.elements; prefix = modified.prefix; } else if (offset < ucaOffset && offset > responsibilitiesOffset) { elements = model.responsibilities.flatMap(resp => resp.responsiblitiesForOneSystem); prefix = IDPrefix.Responsibility; } else if (offset < ucaConstraintOffset && offset > ucaOffset) { - elements = model.allUCAs.flatMap(sysUCA => sysUCA.notProvidingUcas.concat(sysUCA.providingUcas, sysUCA.wrongTimingUcas, sysUCA.continousUcas)); + elements = model.allUCAs.flatMap(sysUCA => + sysUCA.notProvidingUcas.concat(sysUCA.providingUcas, sysUCA.wrongTimingUcas, sysUCA.continousUcas) + ); elements = elements.concat(model.rules.flatMap(rule => rule.contexts)); prefix = IDPrefix.UCA; // rules must be handled separately since they are mixed with the UCAs @@ -370,12 +434,16 @@ export class IDEnforcer { /** * Determines the elements affected by the given {@code offfset}. - * @param originalElements The top-level elements which may be affected by the offset. + * @param originalElements The top-level elements which may be affected by the offset. * @param originalPrefix The prefix for the top-level elements. * @param offset Offset in the file for which the affected components should be determined. * @returns the elements affected by the offset and their prefix. */ - protected findAffectedSubComponents(originalElements: Hazard[] | SystemConstraint[], originalPrefix: string, offset: number): { elements: Hazard[] | SystemConstraint[], prefix: string; } { + protected findAffectedSubComponents( + originalElements: Hazard[] | SystemConstraint[], + originalPrefix: string, + offset: number + ): { elements: Hazard[] | SystemConstraint[]; prefix: string } { let elements = originalElements; let prefix = originalPrefix; // index of the affected element @@ -389,11 +457,15 @@ export class IDEnforcer { // check whether the children are affected // if the children are affected, it must be checked whether they have again affected children // otherwise the current elements are the affected ones - if (element.subComponents.length !== 0 && element.subComponents[0].$cstNode && element.subComponents[0].$cstNode.offset <= offset) { + if ( + element.subComponents.length !== 0 && + element.subComponents[0].$cstNode && + element.subComponents[0].$cstNode.offset <= offset + ) { const modified = this.findAffectedSubComponents(element.subComponents, element.name + ".", offset); elements = modified.elements; prefix = modified.prefix; } return { elements, prefix }; } -} \ No newline at end of file +} From baa8768750e589ad32134df77885cb8d1039803d Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Fri, 28 Jun 2024 10:40:13 +0200 Subject: [PATCH 28/28] mka feedback --- extension/src-context-table/main.ts | 10 +++++++--- extension/src/extension.ts | 4 ++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/extension/src-context-table/main.ts b/extension/src-context-table/main.ts index e2a0c79..0ddef1d 100644 --- a/extension/src-context-table/main.ts +++ b/extension/src-context-table/main.ts @@ -47,6 +47,10 @@ export class ContextTable extends Table { protected typeSelectorId = "select_type"; protected tableId = "context_table"; + /** Offsets for the selectors */ + protected selectorControlActionOffset = { top: "13px", left: "170px" }; + protected selectorTypeOffset = { top: "43px", left: "115px" }; + // data of the table protected rules: ContextTableRule[] = []; protected controlActions: ContextTableControlAction[] = []; @@ -128,7 +132,7 @@ export class ContextTable extends Table { if (mainDiv) { // Create text and selector element for selecting a control action addText(mainDiv, "Choose a Control Action:"); - addSelector(mainDiv, this.actionSelectorId, 0, [], "13px", "170px"); + addSelector(mainDiv, this.actionSelectorId, 0, [], this.selectorControlActionOffset.top, this.selectorControlActionOffset.left); // Create text and selector element for selecting the action type addText(mainDiv, "Choose a Type:"); @@ -137,8 +141,8 @@ export class ContextTable extends Table { this.typeSelectorId, this.selectedType, ["provided", "not provided", "both"], - "43px", - "115px" + this.selectorTypeOffset.top, + this.selectorTypeOffset.left ); // add listener diff --git a/extension/src/extension.ts b/extension/src/extension.ts index dfb03b4..fc0cf54 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -347,13 +347,13 @@ async function updateViews(manager: StpaLspVscodeExtension, document: vscode.Tex } // update diagram without reseting viewport - const mes: ActionMessage = { + const message: ActionMessage = { clientId: manager.clientId!, action: { kind: UpdateDiagramAction.KIND, } as UpdateDiagramAction, }; - languageClient.sendNotification(acceptMessageType, mes); + languageClient.sendNotification(acceptMessageType, message); // update the context table if (manager.contextTable) {