diff --git a/.vscode/launch.json b/.vscode/launch.json index e4fc1001..0b6794ad 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -34,7 +34,7 @@ "webpack:///./~/*": "${workspaceFolder}/extension/node_modules/*", "webpack://?:*/*": "${workspaceFolder}/extension/*" }, - "type": "pwa-node" + "type": "node" } ] } \ No newline at end of file diff --git a/extension/package.json b/extension/package.json index 90f9bcc5..6fcab64d 100644 --- a/extension/package.json +++ b/extension/package.json @@ -150,6 +150,11 @@ "command": "stpa.SBM.generation", "title": "Generate Safe Behavioral Model (SBM)", "category": "STPA SBM" + }, + { + "command": "stpa.md.creation", + "title": "Create a Markdown file", + "category": "STPA PDF Creation" } ], "menus": { @@ -201,6 +206,10 @@ { "command": "stpa.SBM.generation", "when": "editorLangId == 'stpa'" + }, + { + "command": "stpa.md.creation", + "when": "editorLangId == 'stpa'" } ], "editor/context": [ @@ -217,12 +226,17 @@ { "submenu": "stpa.checks", "when": "editorLangId == 'stpa'", - "group": "checks" + "group": "stpa" }, { "command": "stpa.SBM.generation", "when": "editorLangId == 'stpa'", - "group": "navigation" + "group": "stpa" + }, + { + "command": "stpa.md.creation", + "when": "editorLangId == 'stpa'", + "group": "stpa" } ], "stpa.checks": [ @@ -339,7 +353,8 @@ "feather-icons": "^4.28.0", "sprotty-vscode-webview": "^0.5.0", "@kieler/table-webview": "^0.0.3", - "snabbdom": "^3.5.1" + "snabbdom": "^3.5.1", + "dayjs": "^1.11.8" }, "devDependencies": { "@types/node": "^12.12.6", @@ -365,7 +380,7 @@ "langium:generate": "langium generate", "langium:watch": "langium generate --watch", "lint": "eslint .", - "build": "yarn run langium:generate && webpack --mode development --devtool eval-source-map", + "build": "yarn run langium:generate && webpack --mode development", "watch": "webpack --watch", "package": "vsce package --yarn -o pasta.vsix", "predistribute": "yarn run package", diff --git a/extension/src-language-server/handler.ts b/extension/src-language-server/handler.ts index bfd8201d..014e3f39 100644 --- a/extension/src-language-server/handler.ts +++ b/extension/src-language-server/handler.ts @@ -28,22 +28,27 @@ import { elementWithName } from "./stpa/utils"; */ export function addNotificationHandler(connection: Connection, shared: LangiumSprottySharedServices): void { // diagram - connection.onNotification('diagram/selected', (msg: {label: string, uri: string}) => { + connection.onNotification("diagram/selected", async (msg: { label: string; uri: string }) => { // get the current model - const model = getModel(msg.uri, shared); + const model = await getModel(msg.uri, shared); // determine the range in the editor of the component identified by "label" const range = getRangeOfNode(model, msg.label); if (range) { // notify extension to highlight the range in the editor - connection.sendNotification('editor/highlight', ({ startLine: range.start.line, startChar: range.start.character, endLine: range.end.line, endChar: range.end.character, uri: msg.uri })); + connection.sendNotification("editor/highlight", { + startLine: range.start.line, + startChar: range.start.character, + endLine: range.end.line, + endChar: range.end.character, + uri: msg.uri, + }); } else { console.log("The selected UCA could not be found in the editor."); } }); } - /** * Determines the range of the component identified by {@code label} in the editor, * @param model The current STPA model. @@ -52,16 +57,29 @@ export function addNotificationHandler(connection: Connection, shared: LangiumSp */ function getRangeOfNode(model: Model, label: string): Range | undefined { let range: Range | undefined = undefined; - const elements: elementWithName[] = [...model.losses, ...model.hazards, ...model.hazards.flatMap(hazard => hazard.subComps), ...model.systemLevelConstraints, ...model.systemLevelConstraints.flatMap(constraint => constraint.subComps), ...model.responsibilities.flatMap(resp => resp.responsiblitiesForOneSystem), - ...model.allUCAs.flatMap(ucas => ucas.ucas), ...model.rules.flatMap(rule => rule.contexts), ...model.controllerConstraints, ...model.scenarios, ...model.safetyCons]; + const elements: elementWithName[] = [ + ...model.losses, + ...model.hazards, + ...model.hazards.flatMap((hazard) => hazard.subComponents), + ...model.systemLevelConstraints, + ...model.systemLevelConstraints.flatMap((constraint) => constraint.subComponents), + ...model.responsibilities.flatMap((resp) => resp.responsiblitiesForOneSystem), + ...model.allUCAs.flatMap((ucas) => + ucas.providingUcas.concat(ucas.notProvidingUcas, ucas.wrongTimingUcas, ucas.continousUcas) + ), + ...model.rules.flatMap((rule) => rule.contexts), + ...model.controllerConstraints, + ...model.scenarios, + ...model.safetyCons, + ]; if (model.controlStructure) { elements.push(...model.controlStructure.nodes); } - elements.forEach(component => { + elements.forEach((component) => { if (component.name === label) { range = component.$cstNode?.range; return; } }); return range; -} \ No newline at end of file +} diff --git a/extension/src-language-server/stpa.langium b/extension/src-language-server/stpa.langium index 55e91866..63a8c5c5 100644 --- a/extension/src-language-server/stpa.langium +++ b/extension/src-language-server/stpa.langium @@ -22,10 +22,10 @@ entry Model: ('Hazards' hazards+=Hazard*)? ('SystemConstraints' systemLevelConstraints+=SystemConstraint*)? ('ControlStructure' controlStructure=Graph)? - ('Responsibilities' responsibilities+=Resps*)? + ('Responsibilities' responsibilities+=SystemResponsibilities*)? (('UCAs' allUCAs+=ActionUCAs*) | ('Context-Table' rules+=Rule*))? ('DCAs' allDCAs+=DCARule*)? - ('ControllerConstraints' controllerConstraints+=ContConstraint*)? + ('ControllerConstraints' controllerConstraints+=ControllerConstraint*)? ('LossScenarios' scenarios+=LossScenario*)? ('SafetyRequirements' safetyCons+=SafetyConstraint*)?; @@ -48,14 +48,14 @@ Loss: Hazard: name=SubID description=STRING ('['refs+=[Loss] (',' refs+=[Loss])*']')? - ('{' (header=STRING? subComps+=Hazard+)*'}')?; + ('{' (header=STRING? subComponents+=Hazard+)*'}')?; SystemConstraint: name=SubID description=STRING '[' refs+=[Hazard:SubID] (',' refs+=[Hazard:SubID])* ']' - ('{' (header=STRING? subComps+=SystemConstraint+)*'}')?; + ('{' (header=STRING? subComponents+=SystemConstraint+)*'}')?; Graph: name=ID '{'(nodes+=Node /* | edges+=Edge */)*'}'; @@ -67,8 +67,8 @@ Node: ('processModel' '{'variables+=Variable*'}')? ('input' '[' inputs+=Command (',' inputs+=Command)* ']')? ('output' '[' outputs+=Command (',' outputs+=Command)* ']')? - ('controlActions' '{'actions+=VE*'}')? - ('feedback' '{'feedbacks+=VE*'}')? + ('controlActions' '{'actions+=VerticalEdge*'}')? + ('feedback' '{'feedbacks+=VerticalEdge*'}')? '}'; /* Edge: @@ -85,13 +85,13 @@ VariableValue: firstValue=(QualifiedName | INT | 'MIN' | 'true' | 'false') (',' secondValue=(QualifiedName | INT | 'MAX'))? secondParenthesis=(']'|')'))?; -VE: +VerticalEdge: '[' comms+=Command (',' comms+=Command)* ']' '->' target=[Node]; Command: name=ID label=STRING; -Resps: +SystemResponsibilities: system=[Node] '{'responsiblitiesForOneSystem+=Responsibility*'}'; Responsibility: @@ -102,10 +102,10 @@ Responsibility: ActionUCAs: system=[Node]'.' action=[Command] '{' - 'notProviding' '{'ucas+=UCA*'}' - 'providing' '{'ucas+=UCA*'}' - 'tooEarly/Late' '{'ucas+=UCA*'}' - 'stoppedTooSoon' '{'ucas+=UCA*'}' + 'notProviding' '{'notProvidingUcas+=UCA*'}' + 'providing' '{'providingUcas+=UCA*'}' + 'tooEarly/Late' '{'wrongTimingUcas+=UCA*'}' + 'stoppedTooSoon' '{'continousUcas+=UCA*'}' '}'; UCA: @@ -123,7 +123,7 @@ DCARule: DCAContext: name=ID '['vars+=[Variable] '=' values+=QualifiedName (',' vars+=[Variable] '=' values+=QualifiedName)*']'; -ContConstraint: +ControllerConstraint: name=ID description=STRING '['refs+=[UCA] (',' refs+=[UCA])*']'; LossScenario: diff --git a/extension/src-language-server/stpa/ID-enforcer.ts b/extension/src-language-server/stpa/ID-enforcer.ts index 7b7d23bf..ed379138 100644 --- a/extension/src-language-server/stpa/ID-enforcer.ts +++ b/extension/src-language-server/stpa/ID-enforcer.ts @@ -214,7 +214,7 @@ 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.ucas).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; @@ -277,9 +277,9 @@ export class IDEnforcer { edits = edits.concat(edit.changes[this.currentUri]); } // rename children - if ((isHazard(element) || isSystemConstraint(element)) && element.subComps.length !== 0) { + if ((isHazard(element) || isSystemConstraint(element)) && element.subComponents.length !== 0) { let index = 1; - for (const child of element.subComps) { + for (const child of element.subComponents) { edits = edits.concat(await this.renameID(child, prefix + counter + ".", index)); index++; } @@ -340,7 +340,7 @@ export class IDEnforcer { elements = model.responsibilities.flatMap(resp => resp.responsiblitiesForOneSystem); prefix = IDPrefix.Responsibility; } else if (offset < ucaConstraintOffset && offset > ucaOffset) { - elements = model.allUCAs.flatMap(uca => uca.ucas); + elements = model.allUCAs.flatMap(sysUCA => sysUCA.providingUcas.concat(sysUCA.notProvidingUcas, 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 @@ -380,8 +380,8 @@ 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.subComps.length !== 0 && element.subComps[0].$cstNode && element.subComps[0].$cstNode.offset <= offset) { - const modified = this.findAffectedSubComponents(element.subComps, element.name + ".", 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; } diff --git a/extension/src-language-server/stpa/actions.ts b/extension/src-language-server/stpa/actions.ts new file mode 100644 index 00000000..ffb03775 --- /dev/null +++ b/extension/src-language-server/stpa/actions.ts @@ -0,0 +1,81 @@ +/* + * KIELER - Kiel Integrated Environment for Layout Eclipse RichClient + * + * http://rtsys.informatik.uni-kiel.de/kieler + * + * Copyright 2023 by + * + Kiel University + * + Department of Computer Science + * + Real-Time and Embedded Systems Group + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { Action, JsonMap, RequestAction, generateRequestId, ResponseAction } from "sprotty-protocol"; + +/** Send to server to generate SVGs for the STPA result report */ +export interface GenerateSVGsAction extends Action { + kind: typeof GenerateSVGsAction.KIND + uri: string + options?: JsonMap; +} + +export namespace GenerateSVGsAction { + export const KIND = "generateSVGs"; + + + export function create( + uri: string, + options?: JsonMap + ): GenerateSVGsAction { + return { + kind: KIND, + uri, + options + }; + } + + export function isThisAction(action: Action): action is GenerateSVGsAction { + return action.kind === GenerateSVGsAction.KIND; + } +} + +/** Requests the current SVG from the client. */ +export interface RequestSvgAction extends RequestAction { + kind: typeof RequestSvgAction.KIND +} + +export namespace RequestSvgAction { + export const KIND = 'requestSvg'; + + export function create(): RequestSvgAction { + return { + kind: KIND, + requestId: generateRequestId() + }; + } +} + +/** Send from client to server containing the requested SVG and its width. */ +export interface SvgAction extends ResponseAction { + kind: typeof SvgAction.KIND; + svg: string + width: number + responseId: string +} +export namespace SvgAction { + export const KIND = 'svg'; + + export function create(svg: string, width: number, requestId: string): SvgAction { + return { + kind: KIND, + svg, + width, + responseId: requestId + }; + } +} \ No newline at end of file diff --git a/extension/src-language-server/stpa/contextTable/context-dataProvider.ts b/extension/src-language-server/stpa/contextTable/context-dataProvider.ts index ad5e6ec5..0fb9dcba 100644 --- a/extension/src-language-server/stpa/contextTable/context-dataProvider.ts +++ b/extension/src-language-server/stpa/contextTable/context-dataProvider.ts @@ -16,11 +16,18 @@ */ import { LangiumDocument } from "langium"; -import { StpaServices } from "../stpa-module"; import { Range, URI } from "vscode-languageserver"; -import { ContextTableData, ContextTableControlAction, ContextTableRule, ContextTableSystemVariables, ContextTableVariable, ContextTableVariableValues } from "../../../src-context-table/utils"; +import { + ContextTableControlAction, + ContextTableData, + ContextTableRule, + ContextTableSystemVariables, + ContextTableVariable, + ContextTableVariableValues, +} from "../../../src-context-table/utils"; import { Model } from "../../generated/ast"; import { getModel } from "../../utils"; +import { StpaServices } from "../stpa-module"; export class ContextTableProvider { protected services: StpaServices; @@ -42,12 +49,14 @@ export class ContextTableProvider { const model: Model = currentDoc.parseResult.value; let range: Range | undefined = undefined; - model.rules.forEach(rule => rule.contexts.forEach(uca => { - if (uca.name === ucaName) { - range = uca.$cstNode?.range; - return; - } - })); + model.rules.forEach((rule) => + rule.contexts.forEach((uca) => { + if (uca.name === ucaName) { + range = uca.$cstNode?.range; + return; + } + }) + ); return range; } @@ -55,32 +64,32 @@ export class ContextTableProvider { * Collects all the data needed for constructing the context table. * @returns The data in a set of arrays. */ - getData(uri: URI): ContextTableData { + async getData(uri: URI): Promise { // get the current model - const model = getModel(uri, this.services.shared); + const model = await getModel(uri, this.services.shared); const actions: ContextTableControlAction[] = []; const variables: ContextTableSystemVariables[] = []; const rules: ContextTableRule[] = []; // collect control actions and variables - model.controlStructure?.nodes.forEach(systemComponent => { + model.controlStructure?.nodes.forEach((systemComponent) => { // control actions of the current system component - systemComponent.actions.forEach(action => { - action.comms.forEach(command => { + 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) }); + systemComponent.variables.forEach((variable) => { + variableValues.push({ name: variable.name, values: variable.values.map((value) => value.name) }); }); variables.push({ system: systemComponent.name, variables: variableValues }); }); // collect rules - model.rules.forEach(rule => { - rule.contexts.forEach(context => { + model.rules.forEach((rule) => { + rule.contexts.forEach((context) => { // determine context variables const contextVariables: ContextTableVariable[] = []; for (let i = 0; i < context.values.length; i++) { @@ -91,7 +100,7 @@ export class ContextTableProvider { //determine hazards const hazards: string[] = []; const hazardList = context.list.refs; - hazardList.forEach(hazard => { + hazardList.forEach((hazard) => { if (hazard.ref?.name) { hazards.push(hazard.ref.name); } @@ -99,15 +108,15 @@ export class ContextTableProvider { // create rule/uca if (rule.action.ref?.name && rule.system.ref?.name) { rules.push({ - id: context.name, controlAction: { controller: rule.system.ref!.name, action: rule.action.ref!.name }, type: rule.type, - variables: contextVariables, hazards: hazards + id: context.name, + controlAction: { controller: rule.system.ref!.name, action: rule.action.ref!.name }, + type: rule.type, + variables: contextVariables, + hazards: hazards, }); } }); - - - }); return { rules: rules, actions: actions, systemVariables: variables }; } -} \ No newline at end of file +} diff --git a/extension/src-language-server/stpa/diagram/diagram-generator.ts b/extension/src-language-server/stpa/diagram/diagram-generator.ts index 10cac924..eb3aadaa 100644 --- a/extension/src-language-server/stpa/diagram/diagram-generator.ts +++ b/extension/src-language-server/stpa/diagram/diagram-generator.ts @@ -15,21 +15,43 @@ * SPDX-License-Identifier: EPL-2.0 */ -import { AstNode } from 'langium'; -import { GeneratorContext, IdCache, LangiumDiagramGenerator } from 'langium-sprotty'; -import { SLabel, SModelElement, SModelRoot, SNode } from 'sprotty-protocol'; - -import { Command, Hazard, Model, Node, SystemConstraint, VE, isContext, isHazard, isSystemConstraint, isUCA } from '../../generated/ast'; -import { StpaServices } from '../stpa-module'; -import { collectElementsWithSubComps, getAspect, leafElement } from '../utils'; -import { filterModel } from './filtering'; -import { CSEdge, CSNode, ParentNode, STPAEdge, STPANode, STPAPort } from './stpa-interfaces'; -import { CS_EDGE_TYPE, CS_NODE_TYPE, DUMMY_NODE_TYPE, EdgeType, PARENT_TYPE, PortSide, STPAAspect, STPA_EDGE_TYPE, STPA_INTERMEDIATE_EDGE_TYPE, STPA_NODE_TYPE, STPA_PORT_TYPE } from './stpa-model'; -import { StpaSynthesisOptions, labelManagementValue, showLabelsValue } from './synthesis-options'; -import { createUCAContextDescription, getTargets, setLevelOfCSNodes, setLevelsForSTPANodes } from './utils'; +import { AstNode } from "langium"; +import { GeneratorContext, IdCache, LangiumDiagramGenerator } from "langium-sprotty"; +import { SLabel, SModelElement, SModelRoot, SNode } from "sprotty-protocol"; + +import { + Command, + Hazard, + Model, + Node, + SystemConstraint, + VerticalEdge, + isContext, + isHazard, + isSystemConstraint, + isUCA, +} from "../../generated/ast"; +import { StpaServices } from "../stpa-module"; +import { collectElementsWithSubComps, leafElement } from "../utils"; +import { filterModel } from "./filtering"; +import { CSEdge, CSNode, ParentNode, STPAEdge, STPANode, STPAPort } from "./stpa-interfaces"; +import { + CS_EDGE_TYPE, + CS_NODE_TYPE, + DUMMY_NODE_TYPE, + EdgeType, + PARENT_TYPE, + PortSide, + STPAAspect, + STPA_EDGE_TYPE, + STPA_INTERMEDIATE_EDGE_TYPE, + STPA_NODE_TYPE, + STPA_PORT_TYPE, +} from "./stpa-model"; +import { StpaSynthesisOptions, labelManagementValue, showLabelsValue } from "./synthesis-options"; +import { createUCAContextDescription, getAspect, getTargets, setLevelOfCSNodes, setLevelsForSTPANodes } from "./utils"; export class StpaDiagramGenerator extends LangiumDiagramGenerator { - protected readonly options: StpaSynthesisOptions; /** Saves the Ids of the generated SNodes */ @@ -56,34 +78,125 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { // 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 - let stpaChildren: SModelElement[] = filteredModel.losses?.map(l => this.generateSTPANode(l, showLabels === showLabelsValue.ALL || showLabels === showLabelsValue.LOSSES, args)); + let stpaChildren: SModelElement[] = filteredModel.losses?.map((l) => + this.generateSTPANode(l, showLabels === showLabelsValue.ALL || showLabels === showLabelsValue.LOSSES, args) + ); // the hierarchy option determines whether subcomponents are contained in ther parent or not if (!this.options.getHierarchy()) { // subcomponents have edges to the parent const hazards = collectElementsWithSubComps(filteredModel.hazards); const sysCons = collectElementsWithSubComps(filteredModel.systemLevelConstraints); stpaChildren = stpaChildren.concat([ - ...hazards.map(sh => this.generateAspectWithEdges(sh, showLabels === showLabelsValue.ALL || showLabels === showLabelsValue.HAZARDS, args)).flat(1), - ...sysCons.map(ssc => this.generateAspectWithEdges(ssc, showLabels === showLabelsValue.ALL || showLabels === showLabelsValue.SYSTEM_CONSTRAINTS, args)).flat(1) + ...hazards + .map((sh) => + this.generateAspectWithEdges( + sh, + showLabels === showLabelsValue.ALL || showLabels === showLabelsValue.HAZARDS, + args + ) + ) + .flat(1), + ...sysCons + .map((ssc) => + this.generateAspectWithEdges( + ssc, + showLabels === showLabelsValue.ALL || showLabels === showLabelsValue.SYSTEM_CONSTRAINTS, + args + ) + ) + .flat(1), ]); } else { // subcomponents are contained in the parent stpaChildren = stpaChildren.concat([ - ...filteredModel.hazards?.map(h => this.generateAspectWithEdges(h, showLabels === showLabelsValue.ALL || showLabels === showLabelsValue.HAZARDS, args)).flat(1), - ...filteredModel.systemLevelConstraints?.map(sc => this.generateAspectWithEdges(sc, showLabels === showLabelsValue.ALL || showLabels === showLabelsValue.SYSTEM_CONSTRAINTS, args)).flat(1), - ...filteredModel.systemLevelConstraints?.map(sc => sc.subComps?.map(ssc => this.generateEdgesForSTPANode(ssc, args))).flat(2) + ...filteredModel.hazards + ?.map((h) => + this.generateAspectWithEdges( + h, + showLabels === showLabelsValue.ALL || showLabels === showLabelsValue.HAZARDS, + args + ) + ) + .flat(1), + ...filteredModel.systemLevelConstraints + ?.map((sc) => + this.generateAspectWithEdges( + sc, + showLabels === showLabelsValue.ALL || showLabels === showLabelsValue.SYSTEM_CONSTRAINTS, + args + ) + ) + .flat(1), + ...filteredModel.systemLevelConstraints + ?.map((sc) => sc.subComponents?.map((ssc) => this.generateEdgesForSTPANode(ssc, args))) + .flat(2), ]); } stpaChildren = stpaChildren.concat([ - ...filteredModel.responsibilities?.map(r => r.responsiblitiesForOneSystem.map(resp => this.generateAspectWithEdges(resp, showLabels === showLabelsValue.ALL || showLabels === showLabelsValue.RESPONSIBILITIES, args))).flat(2), - ...filteredModel.allUCAs?.map(allUCA => allUCA.ucas.map(uca => this.generateAspectWithEdges(uca, showLabels === showLabelsValue.ALL || showLabels === showLabelsValue.UCAS, args))).flat(2), - ...filteredModel.rules?.map(rule => rule.contexts.map(context => this.generateAspectWithEdges(context, showLabels === showLabelsValue.ALL || showLabels === showLabelsValue.UCAS, args))).flat(2), - ...filteredModel.controllerConstraints?.map(c => this.generateAspectWithEdges(c, showLabels === showLabelsValue.ALL || showLabels === showLabelsValue.CONTROLLER_CONSTRAINTS, args)).flat(1), - ...filteredModel.scenarios?.map(s => this.generateAspectWithEdges(s, showLabels === showLabelsValue.ALL || showLabels === showLabelsValue.SCENARIOS, args)).flat(1), - ...filteredModel.safetyCons?.map(sr => this.generateAspectWithEdges(sr, showLabels === showLabelsValue.ALL || showLabels === showLabelsValue.SAFETY_CONSTRAINTS, args)).flat(1) + ...filteredModel.responsibilities + ?.map((r) => + r.responsiblitiesForOneSystem.map((resp) => + this.generateAspectWithEdges( + resp, + showLabels === showLabelsValue.ALL || showLabels === showLabelsValue.RESPONSIBILITIES, + args + ) + ) + ) + .flat(2), + ...filteredModel.allUCAs + ?.map((sysUCA) => + sysUCA.providingUcas + .concat(sysUCA.notProvidingUcas, sysUCA.wrongTimingUcas, sysUCA.continousUcas) + .map((uca) => + this.generateAspectWithEdges( + uca, + showLabels === showLabelsValue.ALL || showLabels === showLabelsValue.UCAS, + args + ) + ) + ) + .flat(2), + ...filteredModel.rules + ?.map((rule) => + rule.contexts.map((context) => + this.generateAspectWithEdges( + context, + showLabels === showLabelsValue.ALL || showLabels === showLabelsValue.UCAS, + args + ) + ) + ) + .flat(2), + ...filteredModel.controllerConstraints + ?.map((c) => + this.generateAspectWithEdges( + c, + showLabels === showLabelsValue.ALL || showLabels === showLabelsValue.CONTROLLER_CONSTRAINTS, + args + ) + ) + .flat(1), + ...filteredModel.scenarios + ?.map((s) => + this.generateAspectWithEdges( + s, + showLabels === showLabelsValue.ALL || showLabels === showLabelsValue.SCENARIOS, + args + ) + ) + .flat(1), + ...filteredModel.safetyCons + ?.map((sr) => + this.generateAspectWithEdges( + sr, + showLabels === showLabelsValue.ALL || showLabels === showLabelsValue.SAFETY_CONSTRAINTS, + args + ) + ) + .flat(1), ]); - // filtering the nodes of the STPA graph const stpaNodes: STPANode[] = []; for (const node of stpaChildren) { @@ -98,7 +211,7 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { if (filteredModel.controlStructure) { setLevelOfCSNodes(filteredModel.controlStructure?.nodes); // determine the nodes of the control structure graph - const csNodes = filteredModel.controlStructure?.nodes.map(n => this.createControlStructureNode(n, args)); + const csNodes = filteredModel.controlStructure?.nodes.map((n) => this.createControlStructureNode(n, args)); // children (nodes and edges) of the control structure const CSChildren = [ ...csNodes, @@ -108,25 +221,23 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { // add control structure to roots children rootChildren.push({ type: PARENT_TYPE, - id: 'controlStructure', + id: "controlStructure", children: CSChildren, - modelOrder: this.options.getModelOrder() + modelOrder: this.options.getModelOrder(), } as ParentNode); } // add relationship graph to roots children - rootChildren.push( - { - type: PARENT_TYPE, - id: 'relationships', - children: stpaChildren, - modelOrder: this.options.getModelOrder() - } as ParentNode - ); + rootChildren.push({ + type: PARENT_TYPE, + id: "relationships", + children: stpaChildren, + modelOrder: this.options.getModelOrder(), + } as ParentNode); // return root return { - type: 'graph', - id: 'root', - children: rootChildren + type: "graph", + id: "root", + children: rootChildren, }; } @@ -144,13 +255,13 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { id: nodeId, level: node.level, children: this.createLabel([label], nodeId, idCache), - layout: 'stack', + layout: "stack", layoutOptions: { paddingTop: 10.0, paddingBottom: 10.0, paddingLeft: 10.0, - paddingRight: 10.0 - } + paddingRight: 10.0, + }, }; this.idToSNode.set(nodeId, csNode); return csNode; @@ -185,7 +296,7 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { * @param args GeneratorContext of the STPA model. * @returns A list of edges representing the commands. */ - protected translateCommandsToEdges(commands: VE[], edgetype: EdgeType, args: GeneratorContext): CSEdge[] { + protected translateCommandsToEdges(commands: VerticalEdge[], edgetype: EdgeType, args: GeneratorContext): CSEdge[] { const idCache = args.idCache; const edges: CSEdge[] = []; for (const edge of commands) { @@ -198,12 +309,23 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { const com = edge.comms[i]; label.push(com.label); } - const portIds = this.createPortsForEdge(sourceId ?? "", edgetype === EdgeType.CONTROL_ACTION ? - PortSide.SOUTH : PortSide.NORTH, targetId ?? "", edgetype === EdgeType.CONTROL_ACTION ? - PortSide.NORTH : PortSide.SOUTH, edgeId, idCache); - - const e = this.createControlStructureEdge(edgeId, portIds.sourcePortId, portIds.targetPortId, - label, edgetype, args); + const portIds = this.createPortsForEdge( + sourceId ?? "", + edgetype === EdgeType.CONTROL_ACTION ? PortSide.SOUTH : PortSide.NORTH, + targetId ?? "", + edgetype === EdgeType.CONTROL_ACTION ? PortSide.NORTH : PortSide.SOUTH, + edgeId, + idCache + ); + + const e = this.createControlStructureEdge( + edgeId, + portIds.sourcePortId, + portIds.targetPortId, + label, + edgetype, + args + ); edges.push(e); } return edges; @@ -219,15 +341,21 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { * @param idCache The id cache of the STPA model. * @returns the ids of the source and target port the edge should be connected to. */ - protected createPortsForEdge(sourceId: string, sourceSide: PortSide, targetId: string, - targetSide: PortSide, edgeId: string, idCache: IdCache): { sourcePortId: string, targetPortId: string; } { + protected createPortsForEdge( + sourceId: string, + sourceSide: PortSide, + targetId: string, + targetSide: PortSide, + edgeId: string, + idCache: IdCache + ): { sourcePortId: string; targetPortId: string } { // add ports for source and target const sourceNode = this.idToSNode.get(sourceId); - const sourcePortId = idCache.uniqueId(edgeId + '_newTransition'); + const sourcePortId = idCache.uniqueId(edgeId + "_newTransition"); sourceNode?.children?.push(this.createSTPAPort(sourcePortId, sourceSide)); const targetNode = this.idToSNode.get(targetId!); - const targetPortId = idCache.uniqueId(edgeId + '_newTransition'); + const targetPortId = idCache.uniqueId(edgeId + "_newTransition"); targetNode?.children?.push(this.createSTPAPort(targetPortId, targetSide)); return { sourcePortId, targetPortId }; @@ -241,7 +369,12 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { * @param args GeneratorContext of the STPA model. * @returns a list of edges representing the inputs or outputs. */ - protected translateIOToEdgeAndNode(io: Command[], node: Node, edgetype: EdgeType, args: GeneratorContext): (CSNode | CSEdge)[] { + protected translateIOToEdgeAndNode( + io: Command[], + node: Node, + edgetype: EdgeType, + args: GeneratorContext + ): (CSNode | CSEdge)[] { if (io.length !== 0) { const idCache = args.idCache; const nodeId = idCache.getId(node); @@ -257,18 +390,38 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { switch (edgetype) { case EdgeType.INPUT: // create dummy node for the input - const inputDummyNode = this.createDummyNode("input" + node.name, node.level ? node.level - 1 : undefined, idCache); + const inputDummyNode = this.createDummyNode( + "input" + node.name, + node.level ? node.level - 1 : undefined, + idCache + ); // create edge for the input - const inputEdge = this.createControlStructureEdge(idCache.uniqueId(`${inputDummyNode.id}_input_${nodeId}`), inputDummyNode.id ? inputDummyNode.id : '', nodeId ? nodeId : '', - label, edgetype, args); + const inputEdge = this.createControlStructureEdge( + idCache.uniqueId(`${inputDummyNode.id}_input_${nodeId}`), + inputDummyNode.id ? inputDummyNode.id : "", + nodeId ? nodeId : "", + label, + edgetype, + args + ); graphComponents = [inputEdge, inputDummyNode]; break; case EdgeType.OUTPUT: // create dummy node for the output - const outputDummyNode = this.createDummyNode("output" + node.name, node.level ? node.level + 1 : undefined, idCache); + const outputDummyNode = this.createDummyNode( + "output" + node.name, + node.level ? node.level + 1 : undefined, + idCache + ); // create edge for the output - const outputEdge = this.createControlStructureEdge(idCache.uniqueId(`${nodeId}_output_${outputDummyNode.id}`), nodeId ? nodeId : '', outputDummyNode.id ? outputDummyNode.id : '', - label, edgetype, args); + const outputEdge = this.createControlStructureEdge( + idCache.uniqueId(`${nodeId}_output_${outputDummyNode.id}`), + nodeId ? nodeId : "", + outputDummyNode.id ? outputDummyNode.id : "", + label, + edgetype, + args + ); graphComponents = [outputEdge, outputDummyNode]; break; default: @@ -301,7 +454,11 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { * @param args GeneratorContext of the STPA model. * @returns A node representing {@code node} and edges representing the references {@code node} contains. */ - protected generateAspectWithEdges(node: leafElement, showDescription: boolean, args: GeneratorContext): SModelElement[] { + protected generateAspectWithEdges( + node: leafElement, + showDescription: boolean, + args: GeneratorContext + ): SModelElement[] { // node must be created first in order to access the id when creating the edges const stpaNode = this.generateSTPANode(node, showDescription, args); // uca nodes need to save their control action in order to be able to group them by the actions @@ -330,15 +487,25 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { container = container.$container; } - let children: SModelElement[] = this.generateDescriptionLabels(showDescription, nodeId, node.name, args.idCache, isContext(node) ? createUCAContextDescription(node) : node.description); + let children: SModelElement[] = this.generateDescriptionLabels( + showDescription, + nodeId, + node.name, + args.idCache, + isContext(node) ? createUCAContextDescription(node) : node.description + ); // if the hierarchy option is true, the subcomponents are added as children to the parent - if (this.options.getHierarchy() && (isHazard(node) && node.subComps.length !== 0)) { + if (this.options.getHierarchy() && isHazard(node) && node.subComponents.length !== 0) { // adds subhazards - children = children.concat(node.subComps?.map((sc: Hazard) => this.generateSTPANode(sc, showDescription, args))); + children = children.concat( + node.subComponents?.map((sc: Hazard) => this.generateSTPANode(sc, showDescription, args)) + ); } - if (this.options.getHierarchy() && isSystemConstraint(node) && node.subComps.length !== 0) { + if (this.options.getHierarchy() && isSystemConstraint(node) && node.subComponents.length !== 0) { // adds subconstraints - children = children.concat(node.subComps?.map((sc: SystemConstraint) => this.generateSTPANode(sc, showDescription, args))); + children = children.concat( + node.subComponents?.map((sc: SystemConstraint) => this.generateSTPANode(sc, showDescription, args)) + ); } if (isContext(node)) { @@ -365,7 +532,7 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { // if hierarchy option is false, edges from subcomponents to parents are created too const targets = getTargets(node, this.options.getHierarchy()); for (const target of targets) { - const edge = this.generateSTPAEdge(node, target, '', args); + const edge = this.generateSTPAEdge(node, target, "", args); if (edge) { elements.push(edge); } @@ -381,7 +548,12 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { * @param param4 GeneratorContext of the STPA model. * @returns An STPAEdge. */ - protected generateSTPAEdge(source: AstNode, target: AstNode, label: string, { idCache }: GeneratorContext): STPAEdge | undefined { + protected generateSTPAEdge( + source: AstNode, + target: AstNode, + label: string, + { idCache }: GeneratorContext + ): STPAEdge | undefined { // get the IDs const targetId = idCache.getId(target); const sourceId = idCache.getId(source); @@ -390,19 +562,33 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { if (sourceId && targetId) { // create the label of the edge let children: SModelElement[] = []; - if (label !== '') { + if (label !== "") { children = this.createLabel([label], edgeId, idCache); } - if ((isHazard(target) || isSystemConstraint(target)) && target.$container?.$type !== 'Model') { + if ((isHazard(target) || isSystemConstraint(target)) && target.$container?.$type !== "Model") { // if the target is a subcomponent we need to add several ports and edges through the hierarchical structure return this.generateIntermediateIncomingEdges(target, source, sourceId, edgeId, children, idCache); } else { // otherwise it is sufficient to add ports for source and target - const portIds = this.createPortsForEdge(sourceId, PortSide.NORTH, targetId, PortSide.SOUTH, edgeId, idCache); + const portIds = this.createPortsForEdge( + sourceId, + PortSide.NORTH, + targetId, + PortSide.SOUTH, + edgeId, + idCache + ); // add edge between the two ports - return this.createSTPAEdge(edgeId, portIds.sourcePortId, portIds.targetPortId, children, STPA_EDGE_TYPE, getAspect(source)); + return this.createSTPAEdge( + edgeId, + portIds.sourcePortId, + portIds.targetPortId, + children, + STPA_EDGE_TYPE, + getAspect(source) + ); } } } @@ -417,30 +603,59 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { * @param idCache The ID cache of the STPA model. * @returns an STPAEdge to connect the {@code source} (or its top parent) with the top parent of the {@code target}. */ - protected generateIntermediateIncomingEdges(target: AstNode, source: AstNode, sourceId: string, edgeId: string, children: SModelElement[], idCache: IdCache): STPAEdge { + protected generateIntermediateIncomingEdges( + target: AstNode, + source: AstNode, + sourceId: string, + edgeId: string, + children: SModelElement[], + idCache: IdCache + ): STPAEdge { // add ports to the target and its (grand)parents const targetPortIds = this.generatePortsForHierarchy(target, edgeId, PortSide.SOUTH, idCache); // add edges between the ports let current: AstNode | undefined = target; - for (let i = 0; current && current?.$type !== 'Model'; i++) { + for (let i = 0; current && current?.$type !== "Model"; i++) { const currentNode = this.idToSNode.get(idCache.getId(current.$container)!); const edgeType = i === 0 ? STPA_EDGE_TYPE : STPA_INTERMEDIATE_EDGE_TYPE; - currentNode?.children?.push(this.createSTPAEdge(idCache.uniqueId(edgeId), targetPortIds[i + 1], targetPortIds[i], children, edgeType, getAspect(source))); + currentNode?.children?.push( + this.createSTPAEdge( + idCache.uniqueId(edgeId), + targetPortIds[i + 1], + targetPortIds[i], + children, + edgeType, + getAspect(source) + ) + ); current = current?.$container; } - if (isSystemConstraint(source) && source.$container?.$type !== 'Model') { + if (isSystemConstraint(source) && source.$container?.$type !== "Model") { // if the source is a sub-sytemconstraint we also need intermediate edges to the top system constraint - return this.generateIntermediateOutgoingEdges(source, edgeId, children, targetPortIds[targetPortIds.length - 1], idCache); + return this.generateIntermediateOutgoingEdges( + source, + edgeId, + children, + targetPortIds[targetPortIds.length - 1], + idCache + ); } else { // add port for source node const sourceNode = this.idToSNode.get(sourceId); - const sourcePortId = idCache.uniqueId(edgeId + '_newTransition'); + const sourcePortId = idCache.uniqueId(edgeId + "_newTransition"); sourceNode?.children?.push(this.createSTPAPort(sourcePortId, PortSide.NORTH)); // add edge from source to top parent of the target - return this.createSTPAEdge(edgeId, sourcePortId, targetPortIds[targetPortIds.length - 1], children, STPA_INTERMEDIATE_EDGE_TYPE, getAspect(source)); + return this.createSTPAEdge( + edgeId, + sourcePortId, + targetPortIds[targetPortIds.length - 1], + children, + STPA_INTERMEDIATE_EDGE_TYPE, + getAspect(source) + ); } } @@ -453,19 +668,41 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { * @param idCache The ID cache of the STPA model. * @returns the STPAEdge to connect the top parent of the {@code source} with the {@code targetPortId}. */ - protected generateIntermediateOutgoingEdges(source: AstNode, edgeId: string, children: SModelElement[], targetPortId: string, idCache: IdCache): STPAEdge { + protected generateIntermediateOutgoingEdges( + source: AstNode, + edgeId: string, + children: SModelElement[], + targetPortId: string, + idCache: IdCache + ): STPAEdge { // add ports to the source and its (grand)parents const sourceIds = this.generatePortsForHierarchy(source, edgeId, PortSide.NORTH, idCache); // add edges between the ports let current: AstNode | undefined = source; - for (let i = 0; current && current?.$type !== 'Model'; i++) { + for (let i = 0; current && current?.$type !== "Model"; i++) { const currentNode = this.idToSNode.get(idCache.getId(current.$container)!); - currentNode?.children?.push(this.createSTPAEdge(idCache.uniqueId(edgeId), sourceIds[i], sourceIds[i + 1], children, STPA_INTERMEDIATE_EDGE_TYPE, getAspect(source))); + currentNode?.children?.push( + this.createSTPAEdge( + idCache.uniqueId(edgeId), + sourceIds[i], + sourceIds[i + 1], + children, + STPA_INTERMEDIATE_EDGE_TYPE, + getAspect(source) + ) + ); current = current?.$container; } - return this.createSTPAEdge(edgeId, sourceIds[sourceIds.length - 1], targetPortId, children, STPA_INTERMEDIATE_EDGE_TYPE, getAspect(source)); + return this.createSTPAEdge( + edgeId, + sourceIds[sourceIds.length - 1], + targetPortId, + children, + STPA_INTERMEDIATE_EDGE_TYPE, + getAspect(source) + ); } /** @@ -476,12 +713,17 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { * @param idCache The ID cache of the STPA model. * @returns the IDs of the created ports. */ - protected generatePortsForHierarchy(current: AstNode | undefined, edgeId: string, side: PortSide, idCache: IdCache): string[] { + protected generatePortsForHierarchy( + current: AstNode | undefined, + edgeId: string, + side: PortSide, + idCache: IdCache + ): string[] { const ids: string[] = []; - while (current && current?.$type !== 'Model') { + while (current && current?.$type !== "Model") { const currentId = idCache.getId(current); const currentNode = this.idToSNode.get(currentId!); - const portId = idCache.uniqueId(edgeId + '_newTransition'); + const portId = idCache.uniqueId(edgeId + "_newTransition"); currentNode?.children?.push(this.createSTPAPort(portId, side)); ids.push(portId); current = current?.$container; @@ -497,7 +739,13 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { * @param children The children of the STPANode. * @returns an STPANode. */ - protected createSTPANode(node: AstNode, nodeId: string, lvl: number, description: string, children: SModelElement[]): STPANode { + protected createSTPANode( + node: AstNode, + nodeId: string, + lvl: number, + description: string, + children: SModelElement[] + ): STPANode { return { type: STPA_NODE_TYPE, id: nodeId, @@ -505,14 +753,14 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { description: description, hierarchyLvl: lvl, children: children, - layout: 'stack', + layout: "stack", layoutOptions: { paddingTop: 10.0, paddingBottom: 10.0, paddingLeft: 10.0, - paddingRight: 10.0 + paddingRight: 10.0, }, - modelOrder: this.options.getModelOrder() + modelOrder: this.options.getModelOrder(), }; } @@ -526,7 +774,7 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { return { type: STPA_PORT_TYPE, id: id, - side: side + side: side, }; } @@ -540,14 +788,21 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { * @param aspect The aspect of the edge. * @returns an STPAEdge. */ - protected createSTPAEdge(id: string, sourceId: string, targetId: string, children: SModelElement[], type: string, aspect: STPAAspect): STPAEdge { + protected createSTPAEdge( + id: string, + sourceId: string, + targetId: string, + children: SModelElement[], + type: string, + aspect: STPAAspect + ): STPAEdge { return { type: type, id: id, sourceId: sourceId, targetId: targetId, children: children, - aspect: aspect + aspect: aspect, }; } @@ -561,14 +816,21 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { * @param param5 GeneratorContext of the STPA model. * @returns A control structure edge. */ - protected createControlStructureEdge(edgeId: string, sourceId: string, targetId: string, label: string[], edgeType: EdgeType, args: GeneratorContext): CSEdge { + protected createControlStructureEdge( + edgeId: string, + sourceId: string, + targetId: string, + label: string[], + edgeType: EdgeType, + args: GeneratorContext + ): CSEdge { return { type: CS_EDGE_TYPE, id: edgeId, sourceId: sourceId!, targetId: targetId!, edgeType: edgeType, - children: this.createLabel(label, edgeId, args.idCache) + children: this.createLabel(label, edgeId, args.idCache), }; } @@ -580,22 +842,21 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { */ protected createLabel(label: string[], id: string, idCache: IdCache): SLabel[] { const children: SLabel[] = []; - if (label.find(l => l !== '')) { - label.forEach(l => { + if (label.find((l) => l !== "")) { + label.forEach((l) => { children.push({ - type: 'label:xref', - id: idCache.uniqueId(id + '_label'), - text: l + type: "label:xref", + id: idCache.uniqueId(id + "_label"), + text: l, } as SLabel); }); } else { // needed for correct layout children.push({ - type: 'label:xref', - id: idCache.uniqueId(id + '_label'), - text: ' ' + type: "label:xref", + id: idCache.uniqueId(id + "_label"), + text: " ", } as SLabel); - } return children; } @@ -609,14 +870,14 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { protected createDummyNode(name: string, level: number | undefined, idCache: IdCache): CSNode { const dummyNode: CSNode = { type: DUMMY_NODE_TYPE, - id: idCache.uniqueId('dummy' + name), - layout: 'stack', + id: idCache.uniqueId("dummy" + name), + layout: "stack", layoutOptions: { paddingTop: 10.0, paddingBottom: 10.0, paddngLeft: 10.0, - paddingRight: 10.0 - } + paddingRight: 10.0, + }, }; if (level) { dummyNode.level = level; @@ -633,14 +894,20 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { * @param nodeDescription The description of the node for which the labels should be generated. * @returns the labels for the given node. */ - protected generateDescriptionLabels(showDescription: boolean, nodeId: string, nodeName: string, idCache: IdCache, nodeDescription?: string): SModelElement[] { + protected generateDescriptionLabels( + showDescription: boolean, + nodeId: string, + nodeName: string, + idCache: IdCache, + nodeDescription?: string + ): SModelElement[] { const labelManagement = this.options.getLabelManagement(); const children: SModelElement[] = []; //TODO: automatic label selection if (nodeDescription && showDescription) { const width = this.options.getLabelShorteningWidth(); - const words = nodeDescription.split(' '); + const words = nodeDescription.split(" "); let current = ""; switch (labelManagement) { case labelManagementValue.NO_LABELS: @@ -648,9 +915,9 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { case labelManagementValue.ORIGINAL: // show complete description in one line children.push({ - type: 'label', - id: idCache.uniqueId(nodeId + '.label'), - text: nodeDescription + type: "label", + id: idCache.uniqueId(nodeId + ".label"), + text: nodeDescription, }); break; case labelManagementValue.TRUNCATE: @@ -658,12 +925,12 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { if (words.length > 0) { current = words[0]; for (let i = 1; i < words.length && current.length + words[i].length <= width; i++) { - current += ' ' + words[i]; + current += " " + words[i]; } children.push({ - type: 'label', - id: idCache.uniqueId(nodeId + '.label'), - text: current + "..." + type: "label", + id: idCache.uniqueId(nodeId + ".label"), + text: current + "...", }); } break; @@ -675,15 +942,15 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { descriptions.push(current); current = word; } else { - current += ' ' + word; + current += " " + word; } } descriptions.push(current); for (let i = descriptions.length - 1; i >= 0; i--) { children.push({ - type: 'label', - id: idCache.uniqueId(nodeId + '.label'), - text: descriptions[i] + type: "label", + id: idCache.uniqueId(nodeId + ".label"), + text: descriptions[i], }); } break; @@ -691,14 +958,11 @@ export class StpaDiagramGenerator extends LangiumDiagramGenerator { } // show the name in the top line - children.push( - { - type: 'label', - id: idCache.uniqueId(nodeId + '.label'), - text: nodeName - } - ); + children.push({ + type: "label", + id: idCache.uniqueId(nodeId + ".label"), + text: nodeName, + }); return children; } - } diff --git a/extension/src-language-server/stpa/diagram/filtering.ts b/extension/src-language-server/stpa/diagram/filtering.ts index e1f5f94f..8bbad680 100644 --- a/extension/src-language-server/stpa/diagram/filtering.ts +++ b/extension/src-language-server/stpa/diagram/filtering.ts @@ -15,23 +15,35 @@ * SPDX-License-Identifier: EPL-2.0 */ -import { ActionUCAs, ContConstraint, Graph, Hazard, Loss, LossScenario, Model, Resps, Rule, SafetyConstraint, SystemConstraint } from "../../generated/ast"; +import { + ActionUCAs, + ControllerConstraint, + Graph, + Hazard, + Loss, + LossScenario, + Model, + SystemResponsibilities, + Rule, + SafetyConstraint, + SystemConstraint, +} from "../../generated/ast"; import { StpaSynthesisOptions } from "./synthesis-options"; /** * Needed to work on a filtered model without changing the original model. */ export class CustomModel { - losses: Loss[]; - hazards: Hazard[]; - systemLevelConstraints: SystemConstraint[]; - responsibilities: Resps[]; - allUCAs: ActionUCAs[]; - controllerConstraints: ContConstraint[]; - scenarios: LossScenario[]; - safetyCons: SafetyConstraint[]; + losses: Loss[] = []; + hazards: Hazard[] = []; + systemLevelConstraints: SystemConstraint[] = []; + responsibilities: SystemResponsibilities[] = []; + allUCAs: ActionUCAs[] = []; + controllerConstraints: ControllerConstraint[] = []; + scenarios: LossScenario[] = []; + safetyCons: SafetyConstraint[] = []; controlStructure?: Graph; - rules: Rule[]; + rules: Rule[] = []; } /** @@ -44,44 +56,72 @@ export function filterModel(model: Model, options: StpaSynthesisOptions): Custom // updates the control actions that can be used to filter the UCAs setFilterUCAOption(model.allUCAs, model.rules, options); const newModel = new CustomModel(); - // aspects for which no filter exists are just copied - newModel.losses = model.losses; - newModel.hazards = model.hazards; - newModel.controlStructure = model.controlStructure; + if (options.getShowControlStructure()) { + newModel.controlStructure = model.controlStructure; + } + if (options.getShowRelationshipGraph()) { + // aspects for which no filter exists are just copied + newModel.losses = model.losses; + newModel.hazards = model.hazards; - newModel.systemLevelConstraints = options.getHideSysCons() ? [] : model.systemLevelConstraints; - newModel.responsibilities = options.getHideSysCons() || options.getHideRespsCons() ? [] : model.responsibilities; + newModel.systemLevelConstraints = options.getHideSysCons() ? [] : model.systemLevelConstraints; + newModel.responsibilities = + options.getHideSysCons() || options.getHideRespsCons() ? [] : model.responsibilities; - // filter UCAs by the filteringUCA option - newModel.allUCAs = model.allUCAs?.filter(allUCA => - (allUCA.system.ref?.name + "." + allUCA.action.ref?.name) === options.getFilteringUCAs() - || options.getFilteringUCAs() === "all UCAs"); - newModel.rules = model.rules?.filter(rule => - (rule.system.ref?.name + "." + rule.action.ref?.name) === options.getFilteringUCAs() - || options.getFilteringUCAs() === "all UCAs"); - newModel.controllerConstraints = options.getHideContCons() ? [] : - model.controllerConstraints?.filter(cons => - (cons.refs[0].ref?.$container.system.ref?.name + "." - + cons.refs[0].ref?.$container.action.ref?.name) === options.getFilteringUCAs() - || options.getFilteringUCAs() === "all UCAs"); - - // remaining scenarios must be saved to filter safety constraints - const remainingScenarios = new Set(); - newModel.scenarios = options.getHideScenarios() ? [] : - model.scenarios?.filter(scenario => { - if ((!scenario.uca || scenario.uca?.ref?.$container.system.ref?.name + "." - + scenario.uca?.ref?.$container.action.ref?.name) === options.getFilteringUCAs() - || options.getFilteringUCAs() === "all UCAs") { - remainingScenarios.add(scenario.name); - return true; - }; - }); - // filter safety constraints by the remaining scenarios - newModel.safetyCons = options.getHideScenarios() ? [] : - model.safetyCons?.filter(safetyCons => - (safetyCons.refs.filter(ref => remainingScenarios.has(ref.$refText)).length !== 0) - || options.getFilteringUCAs() === "all UCAs"); + // filter UCAs by the filteringUCA option + newModel.allUCAs = options.getHideUCAs() + ? [] + : model.allUCAs?.filter( + (allUCA) => + allUCA.system.ref?.name + "." + allUCA.action.ref?.name === options.getFilteringUCAs() || + options.getFilteringUCAs() === "all UCAs" + ); + newModel.rules = options.getHideUCAs() + ? [] + : model.rules?.filter( + (rule) => + rule.system.ref?.name + "." + rule.action.ref?.name === options.getFilteringUCAs() || + options.getFilteringUCAs() === "all UCAs" + ); + newModel.controllerConstraints = + options.getHideUCAs() || options.getHideContCons() + ? [] + : model.controllerConstraints?.filter( + (cons) => + cons.refs[0].ref?.$container.system.ref?.name + + "." + + cons.refs[0].ref?.$container.action.ref?.name === + options.getFilteringUCAs() || options.getFilteringUCAs() === "all UCAs" + ); + // remaining scenarios must be saved to filter safety constraints + const remainingScenarios = new Set(); + newModel.scenarios = options.getHideScenarios() + ? [] + : model.scenarios?.filter((scenario) => { + if ( + (!scenario.uca && !options.getHideScenariosWithHazard()) || + (scenario.uca && !options.getHideUCAs() && + (scenario.uca?.ref?.$container.system.ref?.name + + "." + + scenario.uca?.ref?.$container.action.ref?.name === + options.getFilteringUCAs() || + options.getFilteringUCAs() === "all UCAs")) + ) { + remainingScenarios.add(scenario.name); + return true; + } + }); + // filter safety constraints by the remaining scenarios + newModel.safetyCons = + options.getHideSafetyConstraints() || options.getHideScenarios() + ? [] + : model.safetyCons?.filter( + (safetyCons) => + safetyCons.refs.filter((ref) => remainingScenarios.has(ref.$refText)).length !== 0 || + options.getFilteringUCAs() === "all UCAs" + ); + } return newModel; } @@ -95,19 +135,19 @@ function setFilterUCAOption(allUCAs: ActionUCAs[], rules: Rule[], options: StpaS const set = new Set(); set.add("all UCAs"); // collect all available control actions - allUCAs.forEach(uca => { + allUCAs.forEach((uca) => { if (!set.has(uca.system.ref?.name + "." + uca.action.ref?.name)) { set.add(uca.system.ref?.name + "." + uca.action.ref?.name); } }); - rules.forEach(rule => { + rules.forEach((rule) => { if (!set.has(rule.system.ref?.name + "." + rule.action.ref?.name)) { set.add(rule.system.ref?.name + "." + rule.action.ref?.name); } }); // generate the options for the UCAs - const list: { displayName: string; id: string; }[] = []; - set.forEach(entry => list.push({ displayName: entry, id: entry })); + const list: { displayName: string; id: string }[] = []; + set.forEach((entry) => list.push({ displayName: entry, id: entry })); // update the option options.updateFilterUCAsOption(list); } diff --git a/extension/src-language-server/stpa/diagram/stpa-diagramServer.ts b/extension/src-language-server/stpa/diagram/stpa-diagramServer.ts index 2bca6d34..b29c3306 100644 --- a/extension/src-language-server/stpa/diagram/stpa-diagramServer.ts +++ b/extension/src-language-server/stpa/diagram/stpa-diagramServer.ts @@ -15,22 +15,61 @@ * SPDX-License-Identifier: EPL-2.0 */ -import { Action, DiagramServer, DiagramServices, JsonMap, RequestAction, RequestModelAction, ResponseAction } from 'sprotty-protocol'; - -import { StpaSynthesisOptions } from './synthesis-options'; -import { SetSynthesisOptionsAction, UpdateOptionsAction } from '../../options/actions'; -import { DropDownOption } from '../../options/option-models'; +import { + Action, + DiagramServer, + DiagramServices, + JsonMap, + RequestAction, + RequestModelAction, + ResponseAction, +} from "sprotty-protocol"; +import { Connection } from "vscode-languageserver"; +import { SetSynthesisOptionsAction, UpdateOptionsAction } from "../../options/actions"; +import { DropDownOption } from "../../options/option-models"; +import { GenerateSVGsAction, RequestSvgAction, SvgAction } from "../actions"; +import { + COMPLETE_GRAPH_PATH, + CONTROL_STRUCTURE_PATH, + FILTERED_CONTROLLER_CONSTRAINT_PATH, + FILTERED_SCENARIO_PATH, + FILTERED_UCA_PATH, + HAZARD_PATH, + RESPONSIBILITY_PATH, + SAFETY_REQUIREMENT_PATH, + SCENARIO_WITH_HAZARDS_PATH, + SYSTEM_CONSTRAINT_PATH, + resetOptions, + saveOptions, + setControlStructureOptions, + setControllerConstraintWithFilteredUcaGraphOptions, + setFilteredUcaGraphOptions, + setHazardGraphOptions, + setRelationshipGraphOptions, + setResponsibilityGraphOptions, + setSafetyRequirementGraphOptions, + setScenarioWithFilteredUCAGraphOptions, + setScenarioWithNoUCAGraphOptions, + setSystemConstraintGraphOptions +} from "../result-report/svg-generator"; +import { StpaSynthesisOptions, filteringUCAsID } from "./synthesis-options"; export class StpaDiagramServer extends DiagramServer { - protected stpaOptions: StpaSynthesisOptions; clientId: string; + protected connection: Connection | undefined; - constructor(dispatch: (action: A) => Promise, - services: DiagramServices, synthesisOptions: StpaSynthesisOptions, clientId: string) { + constructor( + dispatch: (action: A) => Promise, + services: DiagramServices, + synthesisOptions: StpaSynthesisOptions, + clientId: string, + connection: Connection | undefined + ) { super(dispatch, services); this.stpaOptions = synthesisOptions; this.clientId = clientId; + this.connection = connection; } accept(action: Action): Promise { @@ -47,19 +86,113 @@ export class StpaDiagramServer extends DiagramServer { switch (action.kind) { case SetSynthesisOptionsAction.KIND: return this.handleSetSynthesisOption(action as SetSynthesisOptionsAction); + case GenerateSVGsAction.KIND: + return this.handleGenerateSVGDiagrams(action as GenerateSVGsAction); } return super.handleAction(action); } + /** + * Generates the diagrams for the STPA results by setting the synthesis options + * accordingly and requesting the SVG from the client. + * + * @param action The action that triggered this method. + * @returns + */ + async handleGenerateSVGDiagrams(action: GenerateSVGsAction): Promise { + diagramSizes = {}; + const setSynthesisOption = { + kind: SetSynthesisOptionsAction.KIND, + options: this.stpaOptions.getSynthesisOptions().map((option) => option.synthesisOption), + } as SetSynthesisOptionsAction; + // save current option values + saveOptions(this.stpaOptions); + // control structure svg + setControlStructureOptions(this.stpaOptions); + await this.createSVG(setSynthesisOption, action.uri, CONTROL_STRUCTURE_PATH); + // hazard graph svg + setHazardGraphOptions(this.stpaOptions); + await this.createSVG(setSynthesisOption, action.uri, HAZARD_PATH); + // system constraint graph svg + setSystemConstraintGraphOptions(this.stpaOptions); + await this.createSVG(setSynthesisOption, action.uri, SYSTEM_CONSTRAINT_PATH); + // responsibility graph svg + setResponsibilityGraphOptions(this.stpaOptions); + await this.createSVG(setSynthesisOption, action.uri, RESPONSIBILITY_PATH); + + // filtered uca graph svg + const filteringUcaOption = this.stpaOptions + .getSynthesisOptions() + .find((option) => option.synthesisOption.id === filteringUCAsID); + for (const value of (filteringUcaOption?.synthesisOption as DropDownOption).availableValues) { + setFilteredUcaGraphOptions(this.stpaOptions, value.id); + await this.createSVG(setSynthesisOption, action.uri, FILTERED_UCA_PATH(value.id)); + } + + // filtered controller constraint graph svg + for (const value of (filteringUcaOption?.synthesisOption as DropDownOption).availableValues) { + setControllerConstraintWithFilteredUcaGraphOptions(this.stpaOptions, value.id); + await this.createSVG(setSynthesisOption, action.uri, FILTERED_CONTROLLER_CONSTRAINT_PATH(value.id)); + } + + // filtered scenario graph svg + for (const value of (filteringUcaOption?.synthesisOption as DropDownOption).availableValues) { + setScenarioWithFilteredUCAGraphOptions(this.stpaOptions, value.id); + await this.createSVG(setSynthesisOption, action.uri, FILTERED_SCENARIO_PATH(value.id)); + } + // scenario with hazard svg graph + setScenarioWithNoUCAGraphOptions(this.stpaOptions); + await this.createSVG(setSynthesisOption, action.uri, SCENARIO_WITH_HAZARDS_PATH); + + // safety requirement svg graph + setSafetyRequirementGraphOptions(this.stpaOptions); + await this.createSVG(setSynthesisOption, action.uri, SAFETY_REQUIREMENT_PATH); + // complete graph svg + setRelationshipGraphOptions(this.stpaOptions); + await this.createSVG(setSynthesisOption, action.uri, COMPLETE_GRAPH_PATH); + // reset options + resetOptions(this.stpaOptions); + await this.handleSetSynthesisOption(setSynthesisOption); + + return Promise.resolve(); + } + + /** + * Creates an SVG by sending the {@code action} to the client and then requesting the SVG. The SVG is then send to the extension with the uri where to save it. + * @param action The action to set the synthesis options for the wanted diagram. + * @param uri The uri of the folder where the SVG should be saved. + * @param id The name of the SVG. + */ + protected async createSVG(action: SetSynthesisOptionsAction | undefined, uri: string, id: string): Promise { + if (action) { + // wait for client to apply the new synthesis option values + await this.handleSetSynthesisOption(action); + // request SVG + const request = RequestSvgAction.create(); + const response = await this.request(request); + // save the width of the SVG + diagramSizes[id] = response.width; + // send SVG to the extension + this.connection?.sendNotification("svg", { uri: uri + id, svg: response.svg }); + } + } + protected async handleSetSynthesisOption(action: SetSynthesisOptionsAction): Promise { for (const option of action.options) { - const opt = this.stpaOptions.getSynthesisOptions().find(synOpt => synOpt.synthesisOption.id === option.id); + const opt = this.stpaOptions + .getSynthesisOptions() + .find((synOpt) => synOpt.synthesisOption.id === option.id); if (opt) { opt.currentValue = option.currentValue; + opt.synthesisOption.currentValue = option.currentValue; // for dropdown menu options more must be done if ((opt.synthesisOption as DropDownOption).currentId) { (opt.synthesisOption as DropDownOption).currentId = option.currentValue; - this.dispatch({ kind: UpdateOptionsAction.KIND, valuedSynthesisOptions: this.stpaOptions.getSynthesisOptions(), clientId: this.clientId }); + this.dispatch({ + kind: UpdateOptionsAction.KIND, + valuedSynthesisOptions: this.stpaOptions.getSynthesisOptions(), + clientId: this.clientId, + }); } } } @@ -72,22 +205,31 @@ export class StpaDiagramServer extends DiagramServer { try { const newRoot = await this.diagramGenerator.generate({ options: this.state.options ?? {}, - state: this.state + 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.stpaOptions.getSynthesisOptions(), clientId: this.clientId }); + this.dispatch({ + kind: UpdateOptionsAction.KIND, + valuedSynthesisOptions: this.stpaOptions.getSynthesisOptions(), + clientId: this.clientId, + }); } catch (err) { this.rejectRemoteRequest(undefined, err as Error); - console.error('Failed to generate diagram:', err); + console.error("Failed to generate diagram:", err); } } protected async handleRequestModel(action: RequestModelAction): Promise { await super.handleRequestModel(action); - this.dispatch({ kind: UpdateOptionsAction.KIND, valuedSynthesisOptions: this.stpaOptions.getSynthesisOptions(), clientId: this.clientId }); + this.dispatch({ + kind: UpdateOptionsAction.KIND, + valuedSynthesisOptions: this.stpaOptions.getSynthesisOptions(), + clientId: this.clientId, + }); } +} -} \ No newline at end of file +export let diagramSizes: Record = {}; diff --git a/extension/src-language-server/stpa/diagram/synthesis-options.ts b/extension/src-language-server/stpa/diagram/synthesis-options.ts index c1b2dead..824d037f 100644 --- a/extension/src-language-server/stpa/diagram/synthesis-options.ts +++ b/extension/src-language-server/stpa/diagram/synthesis-options.ts @@ -15,17 +15,26 @@ * SPDX-License-Identifier: EPL-2.0 */ -import { SynthesisOption, TransformationOptionType, ValuedSynthesisOption, DropDownOption, RangeOption } from "../../options/option-models"; +import { + DropDownOption, + RangeOption, + SynthesisOption, + TransformationOptionType, + ValuedSynthesisOption, +} from "../../options/option-models"; const hierarchyID = "hierarchy"; const modelOrderID = "modelOrder"; const groupingUCAsID = "groupingUCAs"; -const filteringUCAsID = "filteringUCAs"; +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 showLabelsID = "showLabels"; const labelManagementID = "labelManagement"; @@ -34,6 +43,9 @@ const labelShorteningWidthID = "labelShorteningWidth"; const layoutCategoryID = "layoutCategory"; const filterCategoryID = "filterCategory"; +const showControlStructureID = "showControlStructure"; +const showRelationshipGraphID = "showRelationshipGraph"; + /** * Category for layout options. */ @@ -43,7 +55,7 @@ const layoutCategory: SynthesisOption = { type: TransformationOptionType.CATEGORY, initialValue: 0, currentValue: 0, - values: [] + values: [], }; /** @@ -51,7 +63,7 @@ const layoutCategory: SynthesisOption = { */ const layoutCategoryOption: ValuedSynthesisOption = { synthesisOption: layoutCategory, - currentValue: 0 + currentValue: 0, }; /** @@ -63,7 +75,7 @@ const filterCategory: SynthesisOption = { type: TransformationOptionType.CATEGORY, initialValue: 0, currentValue: 0, - values: [] + values: [], }; /** @@ -71,7 +83,71 @@ const filterCategory: SynthesisOption = { */ const filterCategoryOption: ValuedSynthesisOption = { synthesisOption: filterCategory, - currentValue: 0 + currentValue: 0, +}; + +/** + * Boolean option to toggle the visualization of the control structure. + */ +const showControlStructureOption: ValuedSynthesisOption = { + synthesisOption: { + id: showControlStructureID, + name: "Show Control Structure", + type: TransformationOptionType.CHECK, + initialValue: true, + currentValue: true, + values: [true, false], + category: filterCategory, + }, + currentValue: true, +}; + +/** + * Boolean option to toggle the visualization of the relationship graph. + */ +const showRelationshipGraphOption: ValuedSynthesisOption = { + synthesisOption: { + id: showRelationshipGraphID, + name: "Show Relationship Graph", + type: TransformationOptionType.CHECK, + initialValue: true, + currentValue: true, + values: [true, false], + category: filterCategory, + }, + currentValue: true, +}; + +/** + * Boolean option to toggle the visualization of UCAs. + */ +const hideUCAsOption: ValuedSynthesisOption = { + synthesisOption: { + id: hideUCAsID, + name: "Hide UCAs", + type: TransformationOptionType.CHECK, + initialValue: false, + currentValue: false, + values: [true, false], + category: filterCategory, + }, + currentValue: false, +}; + +/** + * Boolean option to toggle the visualization of safety constraints. + */ +const hideSafetyConstraintsOption: ValuedSynthesisOption = { + synthesisOption: { + id: hideSafetyConstraintsID, + name: "Hide Safety Constraints", + type: TransformationOptionType.CHECK, + initialValue: false, + currentValue: false, + values: [true, false], + category: filterCategory, + }, + currentValue: false, }; /** @@ -85,9 +161,9 @@ const hierarchicalGraphOption: ValuedSynthesisOption = { initialValue: true, currentValue: true, values: [true, false], - category: layoutCategory + category: layoutCategory, }, - currentValue: true + currentValue: true, }; /** @@ -101,9 +177,9 @@ const modelOrderOption: ValuedSynthesisOption = { initialValue: true, currentValue: true, values: [true, false], - category: layoutCategory + category: layoutCategory, }, - currentValue: true + currentValue: true, }; /** @@ -112,7 +188,7 @@ const modelOrderOption: ValuedSynthesisOption = { export enum groupValue { NO_GROUPING, CONTROL_ACTION, - SYSTEM_COMPONENT + SYSTEM_COMPONENT, } /** @@ -124,12 +200,12 @@ const groupingOfUCAs: ValuedSynthesisOption = { id: groupingUCAsID, name: "Group UCAs", type: TransformationOptionType.CHOICE, - initialValue: "No grouping", + initialValue: "No Grouping", currentValue: "No grouping", - values: ["No grouping", "Group by Control Action", "Group by System Component"], - category: layoutCategory + values: ["No Grouping", "Group by Control Action", "Group by System Component"], + category: layoutCategory, }, - currentValue: "No grouping" + currentValue: "No Grouping", }; /** @@ -145,9 +221,9 @@ const filteringOfUCAs: ValuedSynthesisOption = { initialValue: "all UCAs", currentValue: "all UCAs", values: [], - category: filterCategory + category: filterCategory, } as DropDownOption, - currentValue: "all UCAs" + currentValue: "all UCAs", }; /** @@ -156,14 +232,14 @@ const filteringOfUCAs: ValuedSynthesisOption = { const hideSysConsOption: ValuedSynthesisOption = { synthesisOption: { id: hideSysConsID, - name: "Hide system-level constraints", + name: "Hide System-level Constraints", type: TransformationOptionType.CHECK, initialValue: false, currentValue: false, values: [true, false], - category: filterCategory + category: filterCategory, }, - currentValue: false + currentValue: false, }; /** @@ -172,14 +248,14 @@ const hideSysConsOption: ValuedSynthesisOption = { const hideRespsOption: ValuedSynthesisOption = { synthesisOption: { id: hideRespsID, - name: "Hide responsibilities", + name: "Hide Responsibilities", type: TransformationOptionType.CHECK, initialValue: false, currentValue: false, values: [true, false], - category: filterCategory + category: filterCategory, }, - currentValue: false + currentValue: false, }; /** @@ -188,14 +264,14 @@ const hideRespsOption: ValuedSynthesisOption = { const hideContConsOption: ValuedSynthesisOption = { synthesisOption: { id: hideContConsID, - name: "Hide controller constraints", + name: "Hide Controller Constraints", type: TransformationOptionType.CHECK, initialValue: false, currentValue: false, values: [true, false], - category: filterCategory + category: filterCategory, }, - currentValue: false + currentValue: false, }; /** @@ -204,14 +280,30 @@ const hideContConsOption: ValuedSynthesisOption = { const hideScenariosOption: ValuedSynthesisOption = { synthesisOption: { id: hideScenariosID, - name: "Hide loss scenarios", + name: "Hide Loss Scenarios", type: TransformationOptionType.CHECK, initialValue: false, currentValue: false, values: [true, false], - category: filterCategory + category: filterCategory, }, - currentValue: false + currentValue: false, +}; + +/** + * Boolean option to toggle the visualization of loss scenarios that are not associated with a UCA. + */ +const hideScenariosWithHazardsOption: ValuedSynthesisOption = { + synthesisOption: { + id: hideScenariosWithHazardID, + name: "Hide Loss Scenarios Without UCAs", + type: TransformationOptionType.CHECK, + initialValue: false, + currentValue: false, + values: [true, false], + category: filterCategory, + }, + currentValue: false, }; /** @@ -227,14 +319,15 @@ const labelShorteningWidthOption: ValuedSynthesisOption = { range: { first: 0, second: 100 }, stepSize: 1, values: [], - category: layoutCategory + category: layoutCategory, } as RangeOption, - currentValue: 30 + currentValue: 30, }; /** * 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), truncate (label is truncated) or no labels. + * 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 = { synthesisOption: { @@ -244,9 +337,9 @@ const labelManagementOption: ValuedSynthesisOption = { initialValue: "Wrapping", currentValue: "Wrapping", values: ["Original Labels", "Wrapping", "Truncate", "No Labels"], - category: layoutCategory + category: layoutCategory, }, - currentValue: "Wrapping" + currentValue: "Wrapping", }; /** @@ -258,22 +351,24 @@ const showLabelsOption: ValuedSynthesisOption = { name: "Show Labels of", type: TransformationOptionType.DROPDOWN, currentId: "losses", - availableValues: [{ displayName: "All", id: "all" }, - { displayName: "Losses", id: "losses" }, - { displayName: "Hazards", id: "hazards" }, - { displayName: "System Constraints", id: "systemConstraints" }, - { displayName: "Responsibilities", id: "responsibilities" }, - { displayName: "UCAs", id: "ucas" }, - { displayName: "Controller Constraints", id: "controllerConstraints" }, - { displayName: "Scenarios", id: "scenarios" }, - { displayName: "Safety Constraints", id: "safetyConstraints" }, - { displayName: "Automatic", id: "automatic" }], + availableValues: [ + { displayName: "All", id: "all" }, + { displayName: "Losses", id: "losses" }, + { displayName: "Hazards", id: "hazards" }, + { displayName: "System Constraints", id: "systemConstraints" }, + { displayName: "Responsibilities", id: "responsibilities" }, + { displayName: "UCAs", id: "ucas" }, + { displayName: "Controller Constraints", id: "controllerConstraints" }, + { displayName: "Scenarios", id: "scenarios" }, + { displayName: "Safety Constraints", id: "safetyConstraints" }, + { displayName: "Automatic", id: "automatic" }, + ], initialValue: "losses", currentValue: "losses", values: [], - category: layoutCategory + category: layoutCategory, } as DropDownOption, - currentValue: "losses" + currentValue: "losses", }; /** @@ -283,7 +378,7 @@ export enum labelManagementValue { ORIGINAL, WRAPPING, TRUNCATE, - NO_LABELS + NO_LABELS, } /** @@ -299,19 +394,32 @@ export enum showLabelsValue { CONTROLLER_CONSTRAINTS, SCENARIOS, SAFETY_CONSTRAINTS, - AUTOMATIC + AUTOMATIC, } export class StpaSynthesisOptions { - private options: ValuedSynthesisOption[]; constructor() { this.options = [ - layoutCategoryOption, filterCategoryOption, - hierarchicalGraphOption, modelOrderOption, groupingOfUCAs, filteringOfUCAs, - hideSysConsOption, hideRespsOption, hideContConsOption, hideScenariosOption, - labelManagementOption, labelShorteningWidthOption, showLabelsOption + layoutCategoryOption, + filterCategoryOption, + hierarchicalGraphOption, + modelOrderOption, + groupingOfUCAs, + filteringOfUCAs, + hideSysConsOption, + hideRespsOption, + hideUCAsOption, + hideContConsOption, + hideScenariosOption, + hideScenariosWithHazardsOption, + hideSafetyConstraintsOption, + labelManagementOption, + labelShorteningWidthOption, + showLabelsOption, + showControlStructureOption, + showRelationshipGraphOption, ]; } @@ -320,94 +428,190 @@ export class StpaSynthesisOptions { } getModelOrder(): boolean { - const option = this.options.find(option => option.synthesisOption.id === modelOrderID); - return option?.currentValue; + return this.getOption(modelOrderID)?.currentValue; } getShowLabels(): showLabelsValue { - const option = this.options.find(option => option.synthesisOption.id === showLabelsID); + const option = this.getOption(showLabelsID); switch (option?.currentValue) { - case "all": return showLabelsValue.ALL; - case "losses": return showLabelsValue.LOSSES; - case "hazards": return showLabelsValue.HAZARDS; - case "systemConstraints": return showLabelsValue.SYSTEM_CONSTRAINTS; - case "responsibilities": return showLabelsValue.RESPONSIBILITIES; - case "ucas": return showLabelsValue.UCAS; - case "controllerConstraints": return showLabelsValue.CONTROLLER_CONSTRAINTS; - case "scenarios": return showLabelsValue.SCENARIOS; - case "safetyConstraints": return showLabelsValue.SAFETY_CONSTRAINTS; - case "automatic": return showLabelsValue.AUTOMATIC; + case "all": + return showLabelsValue.ALL; + case "losses": + return showLabelsValue.LOSSES; + case "hazards": + return showLabelsValue.HAZARDS; + case "systemConstraints": + return showLabelsValue.SYSTEM_CONSTRAINTS; + case "responsibilities": + return showLabelsValue.RESPONSIBILITIES; + case "ucas": + return showLabelsValue.UCAS; + case "controllerConstraints": + return showLabelsValue.CONTROLLER_CONSTRAINTS; + case "scenarios": + return showLabelsValue.SCENARIOS; + case "safetyConstraints": + return showLabelsValue.SAFETY_CONSTRAINTS; + case "automatic": + return showLabelsValue.AUTOMATIC; } return option?.currentValue; } 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; - case "Wrapping": return labelManagementValue.WRAPPING; - case "Truncate": return labelManagementValue.TRUNCATE; - case "No Labels": return labelManagementValue.NO_LABELS; + case "Original Labels": + return labelManagementValue.ORIGINAL; + case "Wrapping": + return labelManagementValue.WRAPPING; + case "Truncate": + return labelManagementValue.TRUNCATE; + case "No Labels": + return labelManagementValue.NO_LABELS; } return option?.currentValue; } getLabelShorteningWidth(): number { - const option = this.options.find(option => option.synthesisOption.id === labelShorteningWidthID); - return option?.currentValue; + return this.getOption(labelShorteningWidthID)?.currentValue; + } + + setShowRelationshipGraph(value: boolean): void { + this.setOption(showRelationshipGraphID, value); + } + + getShowRelationshipGraph(): boolean { + return this.getOption(showRelationshipGraphID)?.currentValue; + } + + setShowControlStructure(value: boolean): void { + this.setOption(showControlStructureID, value); + } + + getShowControlStructure(): boolean { + return this.getOption(showControlStructureID)?.currentValue; + } + + setHierarchy(value: boolean): void { + this.setOption(hierarchyID, value); } getHierarchy(): boolean { - const option = this.options.find(option => option.synthesisOption.id === hierarchyID); - return option?.currentValue; + return this.getOption(hierarchyID)?.currentValue; + } + + setGroupingUCAs(value: groupValue): void { + const option = this.options.find((option) => option.synthesisOption.id === groupingUCAsID); + if (option) { + switch (value) { + case groupValue.NO_GROUPING: + option.currentValue = "No Grouping"; + break; + case groupValue.CONTROL_ACTION: + option.currentValue = "Group by Control Action"; + break; + case groupValue.SYSTEM_COMPONENT: + option.currentValue = "Group by System Component"; + break; + } + option.synthesisOption.currentValue = option.currentValue; + } } getGroupingUCAs(): groupValue { - const option = this.options.find(option => option.synthesisOption.id === groupingUCAsID); + const option = this.getOption(groupingUCAsID); switch (option?.currentValue) { - case "No grouping": return groupValue.NO_GROUPING; - case "Group by Control Action": return groupValue.CONTROL_ACTION; - case "Group by System Component": return groupValue.SYSTEM_COMPONENT; + case "No Grouping": + return groupValue.NO_GROUPING; + case "Group by Control Action": + return groupValue.CONTROL_ACTION; + case "Group by System Component": + return groupValue.SYSTEM_COMPONENT; } return option?.currentValue; } + setFilteringUCAs(value: string): void { + const option = this.options.find((option) => option.synthesisOption.id === filteringUCAsID); + if (option) { + option.currentValue = value; + option.synthesisOption.currentValue = value; + (option.synthesisOption as DropDownOption).currentId = value; + } + } + getFilteringUCAs(): string { - const option = this.options.find(option => option.synthesisOption.id === filteringUCAsID); - return option?.currentValue; + return this.getOption(filteringUCAsID)?.currentValue; + } + + setHideSysCons(value: boolean): void { + this.setOption(hideSysConsID, value); } getHideSysCons(): boolean { - const option = this.options.find(option => option.synthesisOption.id === hideSysConsID); - return option?.currentValue; + return this.getOption(hideSysConsID)?.currentValue; + } + + setHideResps(value: boolean): void { + this.setOption(hideRespsID, value); } getHideRespsCons(): boolean { - const option = this.options.find(option => option.synthesisOption.id === hideRespsID); - return option?.currentValue; + return this.getOption(hideRespsID)?.currentValue; + } + + setHideUCAs(value: boolean): void { + this.setOption(hideUCAsID, value); + } + + getHideUCAs(): boolean { + return this.getOption(hideUCAsID)?.currentValue; + } + + setHideContCons(value: boolean): void { + this.setOption(hideContConsID, value); } getHideContCons(): boolean { - const option = this.options.find(option => option.synthesisOption.id === hideContConsID); - return option?.currentValue; + return this.getOption(hideContConsID)?.currentValue; + } + + setHideScenarios(value: boolean): void { + this.setOption(hideScenariosID, value); } getHideScenarios(): boolean { - const option = this.options.find(option => option.synthesisOption.id === hideScenariosID); - return option?.currentValue; + return this.getOption(hideScenariosID)?.currentValue; + } + + setHideScenariosWithHazard(value: boolean): void { + this.setOption(hideScenariosWithHazardID, value); + } + + getHideScenariosWithHazard(): boolean { + return this.getOption(hideScenariosWithHazardID)?.currentValue; + } + + setHideSafetyConstraints(value: boolean): void { + this.setOption(hideSafetyConstraintsID, value); + } + + getHideSafetyConstraints(): boolean { + return this.getOption(hideSafetyConstraintsID)?.currentValue; } /** * Updates the filterUCAs option with the availabe cotrol actions. * @param values The currently avaiable control actions. */ - updateFilterUCAsOption(values: { displayName: string; id: string; }[]): void { - const option = this.options.find(option => option.synthesisOption.id === filteringUCAsID); + updateFilterUCAsOption(values: { displayName: string; id: string }[]): void { + const option = this.getOption(filteringUCAsID); if (option) { (option.synthesisOption as DropDownOption).availableValues = values; - // if the last selected control action is not available anymore, + // if the last selected control action is not available anymore, // set the option to the first control action of the new list - if (!values.find(val => val.id === (option.synthesisOption as DropDownOption).currentId)) { + if (!values.find((val) => val.id === (option.synthesisOption as DropDownOption).currentId)) { (option.synthesisOption as DropDownOption).currentId = values[0].id; option.synthesisOption.currentValue = values[0].id; option.synthesisOption.initialValue = values[0].id; @@ -415,4 +619,17 @@ export class StpaSynthesisOptions { } } } + + protected setOption(id: string, value: any): void { + const option = this.getOption(id); + if (option) { + option.currentValue = value; + option.synthesisOption.currentValue = value; + } + } + + protected getOption(id: string): ValuedSynthesisOption | undefined { + const option = this.options.find((option) => option.synthesisOption.id === id); + return option; + } } diff --git a/extension/src-language-server/stpa/diagram/utils.ts b/extension/src-language-server/stpa/diagram/utils.ts index 67b7939c..ae4fa590 100644 --- a/extension/src-language-server/stpa/diagram/utils.ts +++ b/extension/src-language-server/stpa/diagram/utils.ts @@ -19,9 +19,10 @@ import { AstNode } from "langium"; import { Context, Node, - isContConstraint, + isControllerConstraint, isContext, isHazard, + isLoss, isLossScenario, isResponsibility, isSafetyConstraint, @@ -41,7 +42,7 @@ import { groupValue } from "./synthesis-options"; export function getTargets(node: AstNode, hierarchy: boolean): AstNode[] { if (node) { const targets: AstNode[] = []; - if (isHazard(node) || isResponsibility(node) || isSystemConstraint(node) || isContConstraint(node) || isSafetyConstraint(node)) { + if (isHazard(node) || isResponsibility(node) || isSystemConstraint(node) || isControllerConstraint(node) || isSafetyConstraint(node)) { for (const ref of node.refs) { if (ref?.ref) { targets.push(ref.ref); } } @@ -220,4 +221,30 @@ export function createUCAContextDescription(uca: Context): string { } return description; +} + +/** + * Getter for the aspect of a STPA component. + * @param node AstNode which aspect should determined. + * @returns the aspect of {@code node}. + */ +export function getAspect(node: AstNode): STPAAspect { + if (isLoss(node)) { + return STPAAspect.LOSS; + } else if (isHazard(node)) { + return STPAAspect.HAZARD; + } else if (isSystemConstraint(node)) { + return STPAAspect.SYSTEMCONSTRAINT; + } else if (isUCA(node) || isContext(node)) { + return STPAAspect.UCA; + } else if (isResponsibility(node)) { + return STPAAspect.RESPONSIBILITY; + } else if (isControllerConstraint(node)) { + return STPAAspect.CONTROLLERCONSTRAINT; + } else if (isLossScenario(node)) { + return STPAAspect.SCENARIO; + } else if (isSafetyConstraint(node)) { + return STPAAspect.SAFETYREQUIREMENT; + } + return STPAAspect.UNDEFINED; } \ No newline at end of file diff --git a/extension/src-language-server/stpa/message-handler.ts b/extension/src-language-server/stpa/message-handler.ts index c639e96c..5044284f 100644 --- a/extension/src-language-server/stpa/message-handler.ts +++ b/extension/src-language-server/stpa/message-handler.ts @@ -15,13 +15,15 @@ * SPDX-License-Identifier: EPL-2.0 */ -import { DocumentState } from 'langium'; +import { DocumentState } from "langium"; import { LangiumSprottySharedServices } from "langium-sprotty"; -import { TextDocumentContentChangeEvent } from 'vscode'; +import { TextDocumentContentChangeEvent } from "vscode"; import { Connection, URI } from "vscode-languageserver"; -import { generateLTLFormulae } from './modelChecking/model-checking'; +import { diagramSizes } from "./diagram/stpa-diagramServer"; +import { generateLTLFormulae } from "./modelChecking/model-checking"; +import { createResultData } from "./result-report/result-generator"; import { StpaServices } from "./stpa-module"; -import { getControlActions } from './utils'; +import { getControlActions } from "./utils"; let lastUri: URI; @@ -32,36 +34,48 @@ let changeUri: string; /** * Adds handlers for notifications regarding stpa. - * @param connection - * @param stpaServices + * @param connection + * @param stpaServices */ -export function addSTPANotificationHandler(connection: Connection, stpaServices: StpaServices, sharedServices: LangiumSprottySharedServices): void { +export function addSTPANotificationHandler( + connection: Connection, + stpaServices: StpaServices, + sharedServices: LangiumSprottySharedServices +): void { addContextTableHandler(connection, stpaServices); addTextChangeHandler(connection, stpaServices, sharedServices); addVerificationHandler(connection, sharedServices); + addResultHandler(connection, sharedServices); } /** * Adds handlers for notifications regarding the context table. - * @param connection - * @param stpaServices + * @param connection + * @param stpaServices */ function addContextTableHandler(connection: Connection, stpaServices: StpaServices): void { // the data needed to create the context table is requested - connection.onNotification('contextTable/getData', uri => { + connection.onNotification("contextTable/getData", async (uri) => { // data is computed and send back to the extension lastUri = uri; const contextTable = stpaServices.contextTable.ContextTableProvider; - connection.sendNotification('contextTable/data', contextTable.getData(uri)); + const data = await contextTable.getData(uri); + connection.sendNotification("contextTable/data", data); }); // a cell in the context table is selected - connection.onNotification('contextTable/selected', text => { + connection.onNotification("contextTable/selected", (text) => { // compute the range of the textual definition of the selected UCA const contextTable = stpaServices.contextTable.ContextTableProvider; const range = contextTable.getRangeOfUCA(lastUri, text); if (range) { // highlight the textual definition in the editor - connection.sendNotification('editor/highlight', ({ startLine: range.start.line, startChar: range.start.character, endLine: range.end.line, endChar: range.end.character, uri: lastUri })); + connection.sendNotification("editor/highlight", { + startLine: range.start.line, + startChar: range.start.character, + endLine: range.end.line, + endChar: range.end.character, + uri: lastUri, + }); } else { console.log("The selected UCA could not be found in the editor."); } @@ -70,13 +84,17 @@ function addContextTableHandler(connection: Connection, stpaServices: StpaServic /** * Adds handlers for notifications regarding changes in the editor. - * @param connection - * @param stpaServices - * @param sharedServices + * @param connection + * @param stpaServices + * @param sharedServices */ -function addTextChangeHandler(connection: Connection, stpaServices: StpaServices, sharedServices: LangiumSprottySharedServices): void { +function addTextChangeHandler( + connection: Connection, + stpaServices: StpaServices, + sharedServices: LangiumSprottySharedServices +): void { // text in the editor changed - connection.onNotification('editor/textChange', async ({ changes, uri }) => { + connection.onNotification("editor/textChange", async ({ changes, uri }) => { // save the changes and the uri of the file. Before we can do something we have to wait until the document is built (see below). textChange = true; textChanges = changes; @@ -89,7 +107,7 @@ function addTextChangeHandler(connection: Connection, stpaServices: StpaServices // enforce correct IDs for the STPA components by sending the computed edits to the extension const edits = await stpaServices.utility.IDEnforcer.enforceIDs(textChanges, changeUri); if (edits.length !== 0) { - connection.sendNotification('editor/workspaceedit', ({ edits, uri: changeUri })); + connection.sendNotification("editor/workspaceedit", { edits, uri: changeUri }); } // reset saved changes textChange = false; @@ -100,19 +118,38 @@ function addTextChangeHandler(connection: Connection, stpaServices: StpaServices /** * Adds handlers for verification. - * @param connection - * @param sharedServices + * @param connection + * @param sharedServices */ function addVerificationHandler(connection: Connection, sharedServices: LangiumSprottySharedServices): void { // LTL generation - connection.onRequest('verification/generateLTL', async (uri: string) => { + connection.onRequest("verification/generateLTL", async (uri: string) => { // generate and send back the LTL formula based on the STPA UCAs const formulas = await generateLTLFormulae(uri, sharedServices); return formulas; }); // get the control actions - connection.onRequest('verification/getControlActions', async (uri: string) => { - const controlActions = getControlActions(uri, sharedServices); + connection.onRequest("verification/getControlActions", async (uri: string) => { + const controlActions = await getControlActions(uri, sharedServices); return controlActions; }); } + +/** + * Adds handlers for notifications regarding the STPA result. + * @param connection + * @param sharedServices + */ +function addResultHandler(connection: Connection, sharedServices: LangiumSprottySharedServices): void { + // creates and send back the STPA result data + connection.onRequest("result/getData", async (uri: string) => { + const data = await createResultData(uri, sharedServices); + return data; + }); + // create the diagrams needed for the STPA result report and send back the widths of them. + connection.onRequest("result/createDiagrams", async (msg) => { + const diagramServerManager = sharedServices.diagram.DiagramServerManager; + await diagramServerManager.acceptAction(msg); + return diagramSizes; + }); +} diff --git a/extension/src-language-server/stpa/modelChecking/model-checking.ts b/extension/src-language-server/stpa/modelChecking/model-checking.ts index e4f04496..1b99ab2e 100644 --- a/extension/src-language-server/stpa/modelChecking/model-checking.ts +++ b/extension/src-language-server/stpa/modelChecking/model-checking.ts @@ -15,10 +15,10 @@ * SPDX-License-Identifier: EPL-2.0 */ -import { DCARule, Model, Rule, Variable, VariableValue, isRule } from "../../generated/ast"; -import { LangiumSprottySharedServices } from "langium-sprotty"; import { Reference } from "langium"; +import { LangiumSprottySharedServices } from "langium-sprotty"; import { URI } from "vscode-uri"; +import { DCARule, Model, Rule, Variable, VariableValue, isRule } from "../../generated/ast"; import { getModel } from "../../utils"; /** @@ -71,14 +71,14 @@ export async function generateLTLFormulae( shared: LangiumSprottySharedServices ): Promise> { // get the current model - let model = getModel(uri, shared); + let model = await getModel(uri, shared); // 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) { // build document await shared.workspace.DocumentBuilder.update([URI.parse(uri)], []); // update the model - model = getModel(uri, shared); + model = await getModel(uri, shared); } // ltl formulas are saved per controller const map: Record = {}; diff --git a/extension/src-language-server/stpa/result-report/result-generator.ts b/extension/src-language-server/stpa/result-report/result-generator.ts new file mode 100644 index 00000000..e25d2258 --- /dev/null +++ b/extension/src-language-server/stpa/result-report/result-generator.ts @@ -0,0 +1,300 @@ +/* + * KIELER - Kiel Integrated Environment for Layout Eclipse RichClient + * + * http://rtsys.informatik.uni-kiel.de/kieler + * + * Copyright 2023 by + * + Kiel University + * + Department of Computer Science + * + Real-Time and Embedded Systems Group + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { AstNode, Reference } from "langium"; +import { LangiumSprottySharedServices } from "langium-sprotty"; +import { + ActionUCAs, + ControllerConstraint, + Hazard, + LossScenario, + Responsibility, + Rule, + SafetyConstraint, + SystemConstraint, + UCA +} from "../../generated/ast"; +import { getModel } from "../../utils"; +import { StpaComponent, StpaResult, UCA_TYPE } from "../utils"; + +/** + * Creates the STPA result data. + * @param uri The uri of the model for which the result data should be created. + * @param shared The shared services of sprotty and langium. + * @returns the STPA result data for the model with the given {@code uri}. + */ +export async function createResultData(uri: string, shared: LangiumSprottySharedServices): Promise { + const result: StpaResult = new StpaResult(); + // get the current model + const model = await getModel(uri, shared); + + // losses + const resultLosses: { id: string; description: string }[] = []; + model.losses.forEach((component) => { + resultLosses.push({ id: component.name, description: component.description }); + }); + result.losses = resultLosses; + + // TODO: consider subhazard headers + result.hazards = createHazardOrSystemConstraintComponents(model.hazards); + result.systemLevelConstraints = createHazardOrSystemConstraintComponents(model.systemLevelConstraints); + + // controller constraints sorted by control action + model.controllerConstraints.forEach((component) => { + const resultComponent = createSingleComponent(component); + const controlAction = createControlActionText(component.refs[0].ref?.$container); + if (result.controllerConstraints[controlAction] === undefined) { + result.controllerConstraints[controlAction] = [resultComponent]; + } else { + result.controllerConstraints[controlAction].push(resultComponent); + } + + }); + + // safety constraints + result.safetyConstraints = createResultComponents(model.safetyCons); + + // responsibilities + model.responsibilities.forEach((component) => { + const responsibilities = createResultComponents(component.responsiblitiesForOneSystem); + // responsibilities are grouped by their system component + result.responsibilities[component.system.$refText] = responsibilities; + }); + + // loss scenarios + model.scenarios.forEach((component) => { + createScenarioResult(component, result); + }); + + //UCAs + model.allUCAs.forEach((component) => { + const ucaResult = createUCAResult(component); + result.ucas[ucaResult.controlAction] = ucaResult.ucas; + }); + model.rules.forEach((component) => { + addRuleUCA(component, result); + }); + + // title for the result report + result.title = model.controlStructure?.name ?? "..."; + + return result; +} + +/** + * Creates the result components list for loss scenarios and UCAs. + * @param components The scenarios/UCAs for which the result components should be created. + * @returns the result components list for loss scenarios/UCAs. + */ +function createResultListComponents(components: LossScenario[] | UCA[]): StpaComponent[] { + const resultList: StpaComponent[] = []; + components.forEach((component) => { + resultList.push(createSingleListComponent(component)); + }); + return resultList; +} + +/** + * Translates a scenarios/UCA to a result component. + * @param component The component to translate. + * @returns a scenarios/UCA result component. + */ +function createSingleListComponent(component: LossScenario | UCA): StpaComponent { + const id = component.name; + const description = component.description; + const references = component.list + ? component.list.refs.map((ref: Reference) => ref.$refText).join(", ") + : undefined; + return { id, description, references }; +} + +/** + * Creates the result components for the given {@code components}. + * @param components The STPA components to translate. + * @returns the result components for the given {@code components}. + */ +function createResultComponents( + components: Hazard[] | SystemConstraint[] | ControllerConstraint[] | SafetyConstraint[] | Responsibility[] +): StpaComponent[] { + const resultList: StpaComponent[] = []; + components.forEach((component) => { + resultList.push(createSingleComponent(component)); + }); + return resultList; +} + +/** + * Creates the result components for the given {@code components} including their subcomponents. + * @param components The Hazards/System-level constraints to translate. + * @returns the result components for the given {@code components} including their subcomponents. + */ +function createHazardOrSystemConstraintComponents(components: Hazard[] | SystemConstraint[]): StpaComponent[] { + const resultList: StpaComponent[] = []; + components.forEach((component) => { + const resultComponent = createSingleComponent(component); + resultComponent.subComponents = createHazardOrSystemConstraintComponents(component.subComponents); + resultList.push(resultComponent); + }); + return resultList; +} + +/** + * Translates the given {@code component} to a result component. + * @param component the component to translate. + * @returns the result component for the given {@code component}. + */ +function createSingleComponent( + component: Hazard | SystemConstraint | ControllerConstraint | SafetyConstraint | Responsibility +): StpaComponent { + return { + id: component.name, + description: component.description, + references: component.refs.map((ref: Reference) => ref.$refText).join(", "), + }; +} + +/** + * Creates the result for UCAs grouped by the UCA types. + * @param component The UCAs for which the result should be created. + * @returns the result for UCAs grouped by the UCA types. + */ +function createUCAResult(component: ActionUCAs): { controlAction: string; ucas: Record } { + const controlAction = createControlActionText(component); + const ucas: Record = {}; + ucas[UCA_TYPE.NOT_PROVIDED] = createResultListComponents(component.notProvidingUcas); + ucas[UCA_TYPE.PROVIDED] = createResultListComponents(component.providingUcas); + ucas[UCA_TYPE.WRONG_TIME] = createResultListComponents(component.wrongTimingUcas); + ucas[UCA_TYPE.CONTINUOUS] = createResultListComponents(component.continousUcas); + return { controlAction, ucas }; +} + +function createControlActionText(component: ActionUCAs | Rule | undefined): string { + if (component === undefined) { + return ""; + } + return component.system.$refText + "." + component.action.$refText; +} + +/** + */ +function addRuleUCA(rule: Rule, result: StpaResult): void { + const controlAction = createControlActionText(rule); + if (result.ucas[controlAction] === undefined) { + result.ucas[controlAction] = {}; + } + const ucas = translateRuleToUCAs(rule); + switch (rule.type) { + case "provided": + if (result.ucas[controlAction][UCA_TYPE.PROVIDED] === undefined) { + result.ucas[controlAction][UCA_TYPE.PROVIDED] = ucas; + } else { + result.ucas[controlAction][UCA_TYPE.PROVIDED].concat(ucas); + } + break; + case "not-provided": + if (result.ucas[controlAction][UCA_TYPE.NOT_PROVIDED] === undefined) { + result.ucas[controlAction][UCA_TYPE.NOT_PROVIDED] = ucas; + } else { + result.ucas[controlAction][UCA_TYPE.NOT_PROVIDED].concat(ucas); + } + break; + case "wrong-time": + case "too-early": + case "too-late": + if (result.ucas[controlAction][UCA_TYPE.WRONG_TIME] === undefined) { + result.ucas[controlAction][UCA_TYPE.WRONG_TIME] = ucas; + } else { + result.ucas[controlAction][UCA_TYPE.WRONG_TIME].concat(ucas); + } + break; + case "applied-too-long": + case "stopped-too-soon": + if (result.ucas[controlAction][UCA_TYPE.CONTINUOUS] === undefined) { + result.ucas[controlAction][UCA_TYPE.CONTINUOUS] = ucas; + } else { + result.ucas[controlAction][UCA_TYPE.CONTINUOUS].concat(ucas); + } + break; + default: + break; + } +} + +function translateRuleToUCAs(rule: Rule): StpaComponent[] { + const ucas: StpaComponent[] = []; + let type = ``; + switch (rule.type) { + case "provided": + case "not-provided": + type = `${rule.type} the control action ${rule.action.$refText}`; + break; + case "wrong-time": + type = `provided the control action ${rule.action.$refText} at the wrong time`; + case "too-early": + case "too-late": + type = `provided the control action ${rule.action.$refText} ${rule.type}`; + break; + case "applied-too-long": + type = `applied the control action ${rule.action.$refText} too long`; + break; + case "stopped-too-soon": + type = `stopped the control action ${rule.action.$refText} too soon`; + break; + } + + rule.contexts.forEach((context) => { + 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) { + contextText += ` and `; + } + } + const description = `${rule.system.$refText} ${type}, when ${contextText}.`; + ucas.push({ + id: context.name, + description, + references: context.list.refs.map((ref: Reference) => ref.$refText).join(", "), + }); + }); + return ucas; +} + +/** + * Creates ands adds the scenario results. + * @param component The scenarios for which the result should be created and added. + * @param result The STPA result to which the created results should be added. + */ +function createScenarioResult(component: LossScenario, result: StpaResult): void { + if (component.uca) { + // translates scenario with a reference to an UCA + const scenario = createSingleListComponent(component); + const controlAction = createControlActionText(component.uca.ref?.$container); + if (result.ucaScenarios[controlAction] === undefined) { + result.ucaScenarios[controlAction] = {}; + } + const scenarioList = result.ucaScenarios[controlAction][component.uca.$refText]; + if (scenarioList !== undefined) { + scenarioList.push(scenario); + } else { + result.ucaScenarios[controlAction][component.uca.$refText] = [scenario]; + } + } else { + // translates scenario without a reference to an UCA + result.scenarios.push(createSingleListComponent(component)); + } +} diff --git a/extension/src-language-server/stpa/result-report/svg-generator.ts b/extension/src-language-server/stpa/result-report/svg-generator.ts new file mode 100644 index 00000000..803b8496 --- /dev/null +++ b/extension/src-language-server/stpa/result-report/svg-generator.ts @@ -0,0 +1,234 @@ +/* + * KIELER - Kiel Integrated Environment for Layout Eclipse RichClient + * + * http://rtsys.informatik.uni-kiel.de/kieler + * + * Copyright 2023 by + * + Kiel University + * + Department of Computer Science + * + Real-Time and Embedded Systems Group + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { SetSynthesisOptionsAction } from "../../options/actions"; +import { StpaSynthesisOptions } from "../diagram/synthesis-options"; + +/* the paths for the several diagrams of the STPA aspects */ +export const SVG_PATH = "/images"; +export const CONTROL_STRUCTURE_PATH = "/control-structure.svg"; +export const HAZARD_PATH = "/hazard.svg"; +export const SYSTEM_CONSTRAINT_PATH = "/system-constraint.svg"; +export const RESPONSIBILITY_PATH = "/responsibility.svg"; +export const SAFETY_REQUIREMENT_PATH = "/safety-requirement.svg"; +export const COMPLETE_GRAPH_PATH = "/complete-graph.svg"; +export const FILTERED_UCA_PATH = (controlAction: string): string => { + return "/ucas/" + controlAction.replace(".", "-").replace(" ", "-") + ".svg"; +}; +export const FILTERED_CONTROLLER_CONSTRAINT_PATH = (controlAction: string): string => { + return "/controller-constraints/" + controlAction.replace(".", "-").replace(" ", "-") + ".svg"; +}; +export const FILTERED_SCENARIO_PATH = (controlAction: string): string => { + return "/scenarios/" + controlAction.replace(".", "-").replace(" ", "-") + ".svg"; +}; +export const SCENARIO_WITH_HAZARDS_PATH = FILTERED_SCENARIO_PATH("no-UCAs"); + +/* used to reset the options after diagrams were created */ +const savedOptions: Map = new Map(); + +/** + * Saves the current values of the {@code options}. + * @param options The synthesis options. + */ +export function saveOptions(options: StpaSynthesisOptions): void { + options.getSynthesisOptions().forEach((option) => { + savedOptions.set(option.synthesisOption.id, option.currentValue); + }); +} + +/** + * Sates the values of {@code options} to the ones saved in savedOptions. + * @param options The synthesis options. + * @returns an action to send the new values to the client. + */ +export function resetOptions(options: StpaSynthesisOptions): SetSynthesisOptionsAction { + // set the values to the saved ones + options.getSynthesisOptions().forEach((option) => { + const savedValue = savedOptions.get(option.synthesisOption.id); + if (savedValue) { + option.currentValue = savedValue === "true" || savedValue === "false" ? savedValue === "true" : savedValue; + } + option.synthesisOption.currentValue = option.currentValue; + }); + // create an action to set the options on the client + const setSynthesisOption = { + kind: SetSynthesisOptionsAction.KIND, + options: options.getSynthesisOptions().map((option) => option.synthesisOption), + } as SetSynthesisOptionsAction; + return setSynthesisOption; +} + +/** + * Sets the values of {@code options} such that only the control structure is shown. + * @param options The synthesis options. + */ +export function setControlStructureOptions(options: StpaSynthesisOptions): void { + options.setShowRelationshipGraph(false); + options.setShowControlStructure(true); +} + +/** + * Sets the values of {@code options} such that the relationship graph is reduced to the hazards. + * @param options The synthesis options. + */ +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); +} + +/** + * Sets the values of {@code options} such that the relationship graph is reduced to the system-level constraints. + * @param options The synthesis options. + */ +export function setSystemConstraintGraphOptions(options: StpaSynthesisOptions): void { + 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); +} + +/** + * Sets the values of {@code options} such that the relationship graph is reduced to the responsibilities. + * @param options The synthesis options. + */ +export function setResponsibilityGraphOptions(options: StpaSynthesisOptions): void { + 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); +} + +/** + * Sets the values of {@code options} such that the relationship graph is filtered based on {@code value}. + * @param options The synthesis options. + * @param value The value the "filter UCA" option should be set to. + */ +export function setFilteredUcaGraphOptions(options: StpaSynthesisOptions, value: string): void { + 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); +} + +/** + * Sets the values of {@code options} such that the relationship graph is reduced to the controller constraints without + * system-level constraints. The relationship graph is filtered based on {@code value} for the UCA filter. + * @param options The synthesis options. + */ +export function setControllerConstraintWithFilteredUcaGraphOptions(options: StpaSynthesisOptions, value: string): void { + 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); +} + +/** + * Sets the values of {@code options} such that the relationship graph is reduced to the loss scenarios without + * system-level constraints. The relationship graph is filtered based on {@code value} for the UCA filter. + * @param options The synthesis options. + */ +export function setScenarioWithFilteredUCAGraphOptions(options: StpaSynthesisOptions, value: string): void { + 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); +} + +/** + * Sets the values of {@code options} such that the relationship graph is reduced to the loss scenarios that are not + * connected to a UCA without system-level constraints. + * @param options The synthesis options. + */ +export function setScenarioWithNoUCAGraphOptions(options: StpaSynthesisOptions): void { + 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); +} + +/** + * Sets the values of {@code options} such that the relationship graph is reduced to the safety constraints without system-level constraints. + * @param options The synthesis options. + */ +export function setSafetyRequirementGraphOptions(options: StpaSynthesisOptions): void { + 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); +} + +/** + * Sets the values of {@code options} such that the whole relationship graph is shown and the control structure is hidden. + * @param options The synthesis options. + */ +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); +} diff --git a/extension/src-language-server/stpa/stpa-module.ts b/extension/src-language-server/stpa/stpa-module.ts index da3cd8e1..a5461564 100644 --- a/extension/src-language-server/stpa/stpa-module.ts +++ b/extension/src-language-server/stpa/stpa-module.ts @@ -15,22 +15,41 @@ * SPDX-License-Identifier: EPL-2.0 */ -import ElkConstructor from 'elkjs/lib/elk.bundled'; -import { createDefaultModule, createDefaultSharedModule, DefaultSharedModuleContext, inject, Module, PartialLangiumServices } from 'langium'; -import { DefaultDiagramServerManager, DiagramActionNotification, LangiumSprottyServices, LangiumSprottySharedServices, SprottyDiagramServices, SprottySharedServices } from 'langium-sprotty'; -import { DefaultElementFilter, ElkFactory, ElkLayoutEngine, IElementFilter, ILayoutConfigurator } from 'sprotty-elk/lib/elk-layout'; -import { DiagramOptions } from 'sprotty-protocol'; -import { URI } from 'vscode-uri'; -import { StpaGeneratedModule, StpaGeneratedSharedModule } from '../generated/module'; -import { ContextTableProvider } from './contextTable/context-dataProvider'; -import { StpaDiagramGenerator } from './diagram/diagram-generator'; -import { StpaLayoutConfigurator } from './diagram/layout-config'; -import { StpaDiagramServer } from './diagram/stpa-diagramServer'; -import { StpaSynthesisOptions } from './diagram/synthesis-options'; -import { IDEnforcer } from './ID-enforcer'; -import { StpaScopeProvider } from './stpa-scopeProvider'; -import { StpaValidationRegistry, StpaValidator } from './stpa-validator'; - +import ElkConstructor from "elkjs/lib/elk.bundled"; +import { + createDefaultModule, + createDefaultSharedModule, + DefaultSharedModuleContext, + inject, + Module, + PartialLangiumServices, +} from "langium"; +import { + DefaultDiagramServerManager, + DiagramActionNotification, + LangiumSprottyServices, + LangiumSprottySharedServices, + SprottyDiagramServices, + SprottySharedServices, +} from "langium-sprotty"; +import { + DefaultElementFilter, + ElkFactory, + ElkLayoutEngine, + IElementFilter, + ILayoutConfigurator, +} from "sprotty-elk/lib/elk-layout"; +import { DiagramOptions } from "sprotty-protocol"; +import { URI } from "vscode-uri"; +import { StpaGeneratedModule, StpaGeneratedSharedModule } from "../generated/module"; +import { ContextTableProvider } from "./contextTable/context-dataProvider"; +import { StpaDiagramGenerator } from "./diagram/diagram-generator"; +import { StpaLayoutConfigurator } from "./diagram/layout-config"; +import { StpaDiagramServer } from "./diagram/stpa-diagramServer"; +import { StpaSynthesisOptions } from "./diagram/synthesis-options"; +import { IDEnforcer } from "./ID-enforcer"; +import { StpaScopeProvider } from "./stpa-scopeProvider"; +import { StpaValidationRegistry, StpaValidator } from "./stpa-validator"; /** * Declaration of custom services - add your own service classes here. @@ -38,24 +57,24 @@ import { StpaValidationRegistry, StpaValidator } from './stpa-validator'; export type StpaAddedServices = { references: { StpaScopeProvider: StpaScopeProvider; - }, + }; validation: { StpaValidator: StpaValidator; - }, + }; layout: { - ElkFactory: ElkFactory, - ElementFilter: IElementFilter, + ElkFactory: ElkFactory; + ElementFilter: IElementFilter; LayoutConfigurator: ILayoutConfigurator; - }, + }; options: { - StpaSynthesisOptions: StpaSynthesisOptions - }, + StpaSynthesisOptions: StpaSynthesisOptions; + }; contextTable: { - ContextTableProvider: ContextTableProvider - }, + ContextTableProvider: ContextTableProvider; + }; utility: { - IDEnforcer: IDEnforcer - } + IDEnforcer: IDEnforcer; + }; }; /** @@ -71,51 +90,63 @@ export type StpaServices = LangiumSprottyServices & StpaAddedServices; */ export const STPAModule: Module = { diagram: { - DiagramGenerator: services => new StpaDiagramGenerator(services), - ModelLayoutEngine: services => new ElkLayoutEngine(services.layout.ElkFactory, services.layout.ElementFilter, services.layout.LayoutConfigurator) as any + DiagramGenerator: (services) => new StpaDiagramGenerator(services), + ModelLayoutEngine: (services) => + new ElkLayoutEngine( + services.layout.ElkFactory, + services.layout.ElementFilter, + services.layout.LayoutConfigurator + ) as any, }, references: { - ScopeProvider: services => new StpaScopeProvider(services), - StpaScopeProvider: services => new StpaScopeProvider(services) + ScopeProvider: (services) => new StpaScopeProvider(services), + StpaScopeProvider: (services) => new StpaScopeProvider(services), }, validation: { - ValidationRegistry: services => new StpaValidationRegistry(services), - StpaValidator: () => new StpaValidator() + ValidationRegistry: (services) => new StpaValidationRegistry(services), + StpaValidator: () => new StpaValidator(), }, layout: { - ElkFactory: () => () => new ElkConstructor({ algorithms: ['layered'] }), - ElementFilter: () => new DefaultElementFilter, - LayoutConfigurator: () => new StpaLayoutConfigurator + ElkFactory: () => () => new ElkConstructor({ algorithms: ["layered"] }), + ElementFilter: () => new DefaultElementFilter(), + LayoutConfigurator: () => new StpaLayoutConfigurator(), }, options: { - StpaSynthesisOptions: () => new StpaSynthesisOptions() + StpaSynthesisOptions: () => new StpaSynthesisOptions(), }, contextTable: { - ContextTableProvider: services => new ContextTableProvider(services) + ContextTableProvider: (services) => new ContextTableProvider(services), }, utility: { - IDEnforcer: services => new IDEnforcer(services) - } + IDEnforcer: (services) => new IDEnforcer(services), + }, }; -export const stpaDiagramServerFactory = - (services: LangiumSprottySharedServices): ((clientId: string, options?: DiagramOptions) => StpaDiagramServer) => { - const connection = services.lsp.Connection; - const serviceRegistry = services.ServiceRegistry; - return (clientId, options) => { - const sourceUri = options?.sourceUri; - if (!sourceUri) { - throw new Error("Missing 'sourceUri' option in request."); - } - const language = serviceRegistry.getServices(URI.parse(sourceUri as string)) as StpaServices; - if (!language.diagram) { - throw new Error(`The '${language.LanguageMetaData.languageId}' language does not support diagrams.`); - } - return new StpaDiagramServer(async action => { +export const stpaDiagramServerFactory = ( + services: LangiumSprottySharedServices +): ((clientId: string, options?: DiagramOptions) => StpaDiagramServer) => { + const connection = services.lsp.Connection; + const serviceRegistry = services.ServiceRegistry; + return (clientId, options) => { + const sourceUri = options?.sourceUri; + if (!sourceUri) { + throw new Error("Missing 'sourceUri' option in request."); + } + const language = serviceRegistry.getServices(URI.parse(sourceUri as string)) as StpaServices; + if (!language.diagram) { + throw new Error(`The '${language.LanguageMetaData.languageId}' language does not support diagrams.`); + } + return new StpaDiagramServer( + async (action) => { connection?.sendNotification(DiagramActionNotification.type, { clientId, action }); - }, language.diagram, language.options.StpaSynthesisOptions, clientId); - }; + }, + language.diagram, + language.options.StpaSynthesisOptions, + clientId, + connection + ); }; +}; /** * instead of the default diagram server the stpa-diagram server is sued @@ -123,12 +154,10 @@ export const stpaDiagramServerFactory = export const StpaSprottySharedModule: Module = { diagram: { diagramServerFactory: stpaDiagramServerFactory, - DiagramServerManager: services => new DefaultDiagramServerManager(services) - } + DiagramServerManager: (services) => new DefaultDiagramServerManager(services), + }, }; - - /** * Create the full set of services required by Langium. * @@ -144,17 +173,12 @@ export const StpaSprottySharedModule: Module r.responsiblitiesForOneSystem).flat(1); - const ucas = model.allUCAs?.map(sysUCA => sysUCA.ucas).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); // collect all elements that have a reference list @@ -176,9 +176,9 @@ export class StpaValidator { * @param accept */ checkHazard(hazard: Hazard, accept: ValidationAcceptor): void { - if (hazard.subComps) { - this.checkPrefixOfSubElements(hazard.name, hazard.subComps, accept); - this.checkReferencedLossesOfSubHazard(hazard.refs, hazard.subComps, accept); + if (hazard.subComponents) { + this.checkPrefixOfSubElements(hazard.name, hazard.subComponents, accept); + this.checkReferencedLossesOfSubHazard(hazard.refs, hazard.subComponents, accept); } this.checkReferenceListForDuplicates(hazard, hazard.refs, accept); // a top-level hazard should reference loss(es) @@ -197,8 +197,8 @@ export class StpaValidator { * @param accept */ checkSystemConstraint(sysCons: SystemConstraint, accept: ValidationAcceptor): void { - if (sysCons.subComps) { - this.checkPrefixOfSubElements(sysCons.name, sysCons.subComps, accept); + if (sysCons.subComponents) { + this.checkPrefixOfSubElements(sysCons.name, sysCons.subComponents, accept); } this.checkReferenceListForDuplicates(sysCons, sysCons.refs, accept); } @@ -228,7 +228,7 @@ export class StpaValidator { * @param contCons The ContConstraint to check. * @param accept */ - checkControllerConstraints(contCons: ContConstraint, accept: ValidationAcceptor): void { + checkControllerConstraints(contCons: ControllerConstraint, accept: ValidationAcceptor): void { this.checkReferenceListForDuplicates(contCons, contCons.refs, accept); } diff --git a/extension/src-language-server/stpa/utils.ts b/extension/src-language-server/stpa/utils.ts index 4e79feaf..bb1287d2 100644 --- a/extension/src-language-server/stpa/utils.ts +++ b/extension/src-language-server/stpa/utils.ts @@ -15,11 +15,11 @@ * SPDX-License-Identifier: EPL-2.0 */ -import { AstNode, LangiumSharedServices } from "langium"; +import { LangiumSharedServices } from "langium"; import { LangiumSprottySharedServices } from "langium-sprotty"; import { Command, - ContConstraint, + ControllerConstraint, Context, Graph, Hazard, @@ -33,18 +33,8 @@ import { SystemConstraint, UCA, Variable, - isContConstraint, - isContext, - isHazard, - isLoss, - isLossScenario, - isResponsibility, - isSafetyConstraint, - isSystemConstraint, - isUCA, } from "../generated/ast"; import { getModel } from "../utils"; -import { STPAAspect } from "./diagram/stpa-model"; export type leafElement = | Loss @@ -52,7 +42,7 @@ export type leafElement = | SystemConstraint | Responsibility | UCA - | ContConstraint + | ControllerConstraint | LossScenario | SafetyConstraint | Context; @@ -62,7 +52,7 @@ export type elementWithName = | SystemConstraint | Responsibility | UCA - | ContConstraint + | ControllerConstraint | LossScenario | SafetyConstraint | Node @@ -76,7 +66,7 @@ export type elementWithRefs = | SystemConstraint | Responsibility | HazardList - | ContConstraint + | ControllerConstraint | SafetyConstraint; /** @@ -85,13 +75,13 @@ export type elementWithRefs = * @param shared The shared services of Langium. * @returns the control actions that are defined in the file determined by the {@code uri}. */ -export function getControlActions( +export async function getControlActions( uri: string, shared: LangiumSprottySharedServices | LangiumSharedServices -): Record { +): Promise> { const controlActionsMap: Record = {}; // get the model from the file determined by the uri - const model = getModel(uri, shared); + const model = await getModel(uri, shared); // collect control actions grouped by their controller model.controlStructure?.nodes.forEach((systemComponent) => { systemComponent.actions.forEach((action) => { @@ -108,32 +98,6 @@ export function getControlActions( return controlActionsMap; } -/** - * Getter for the aspect of a STPA component. - * @param node AstNode which aspect should determined. - * @returns the aspect of {@code node}. - */ -export function getAspect(node: AstNode): STPAAspect { - if (isLoss(node)) { - return STPAAspect.LOSS; - } else if (isHazard(node)) { - return STPAAspect.HAZARD; - } else if (isSystemConstraint(node)) { - return STPAAspect.SYSTEMCONSTRAINT; - } else if (isUCA(node) || isContext(node)) { - return STPAAspect.UCA; - } else if (isResponsibility(node)) { - return STPAAspect.RESPONSIBILITY; - } else if (isContConstraint(node)) { - return STPAAspect.CONTROLLERCONSTRAINT; - } else if (isLossScenario(node)) { - return STPAAspect.SCENARIO; - } else if (isSafetyConstraint(node)) { - return STPAAspect.SAFETYREQUIREMENT; - } - return STPAAspect.UNDEFINED; -} - /** * Collects the {@code topElements}, their children, their children's children and so on. * @param topElements The top elements that possbible have children. @@ -144,10 +108,49 @@ export function collectElementsWithSubComps(topElements: (Hazard | SystemConstra let todo = topElements; for (let i = 0; i < todo.length; i++) { const current = todo[i]; - if (current.subComps) { - result = result.concat(current.subComps); - todo = todo.concat(current.subComps); + if (current.subComponents) { + result = result.concat(current.subComponents); + todo = todo.concat(current.subComponents); } } return result; } + +export class StpaResult { + title: string; + losses: StpaComponent[] = []; + hazards: StpaComponent[] = []; + systemLevelConstraints: StpaComponent[] = []; + // sorted by system components + responsibilities: Record = {}; + // sorted first by control action, then by uca type + ucas: Record> = {}; + // sorted by control action + controllerConstraints: Record = {}; + // sorted by control action and by ucas + ucaScenarios: Record> = {}; + scenarios: StpaComponent[] = []; + safetyConstraints: StpaComponent[] = []; +} + +export class StpaComponent { + id: string; + description: string; + references?: string; + subComponents?: StpaComponent[]; +} + +/** + * Provides the different UCA types. + */ +export class UCA_TYPE { + static NOT_PROVIDED = "not-provided"; + static PROVIDED = "provided"; + static TOO_EARLY = "too-early"; + static TOO_LATE = "too-late"; + static APPLIED_TOO_LONG = "applied-too-long"; + static STOPPED_TOO_SOON = "stopped-too-soon"; + static WRONG_TIME = "wrong-time"; + static CONTINUOUS = "continuous-problem"; + static UNDEFINED = "undefined"; +} diff --git a/extension/src-language-server/utils.ts b/extension/src-language-server/utils.ts index 37b413c4..d520ed15 100644 --- a/extension/src-language-server/utils.ts +++ b/extension/src-language-server/utils.ts @@ -26,8 +26,15 @@ import { LangiumDocument, LangiumSharedServices } from "langium"; * @param shared The shared service. * @returns the model for the given uri. */ -export function getModel(uri: string, shared: LangiumSprottySharedServices | LangiumSharedServices): Model { +export async function getModel(uri: string, shared: LangiumSprottySharedServices | LangiumSharedServices): Promise { const textDocuments = shared.workspace.LangiumDocuments; const currentDoc = textDocuments.getOrCreateDocument(URI.parse(uri)) as LangiumDocument; - return currentDoc.parseResult.value; + let currentModel = currentDoc.parseResult.value; + if (currentModel.rules.length !== 0 && currentModel.rules[0]?.contexts[0]?.vars[0]?.ref === undefined) { + // build document + await shared.workspace.DocumentBuilder.update([URI.parse(uri)], []); + // update the model + currentModel = currentDoc.parseResult.value; + } + return currentModel; } \ No newline at end of file diff --git a/extension/src-webview/actions.ts b/extension/src-webview/actions.ts new file mode 100644 index 00000000..747d5306 --- /dev/null +++ b/extension/src-webview/actions.ts @@ -0,0 +1,94 @@ +/* + * KIELER - Kiel Integrated Environment for Layout Eclipse RichClient + * + * http://rtsys.informatik.uni-kiel.de/kieler + * + * Copyright 2023 by + * + Kiel University + * + Department of Computer Science + * + Real-Time and Embedded Systems Group + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { inject } from "inversify"; +import { CommandExecutionContext, CommandResult, HiddenCommand, TYPES, isExportable, isHoverable, isSelectable, isViewport } from "sprotty"; +import { RequestAction, ResponseAction, generateRequestId } from "sprotty-protocol"; + + +/** Requests the current SVG from the client. */ +export interface RequestSvgAction extends RequestAction { + kind: typeof RequestSvgAction.KIND +} + +export namespace RequestSvgAction { + export const KIND = 'requestSvg'; + + export function create(): RequestSvgAction { + return { + kind: KIND, + requestId: generateRequestId() + }; + } +} + +/** Send from client to server containing the requested SVG and its width. */ +export interface SvgAction extends ResponseAction { + kind: typeof SvgAction.KIND; + svg: string + width: number + responseId: string +} +export namespace SvgAction { + export const KIND = 'svg'; + + export function create(svg: string, width: number, requestId: string): SvgAction { + return { + kind: KIND, + svg, + width, + responseId: requestId + }; + } +} + +/** Command that is executed when SVG is requested by the server. */ +export class SvgCommand extends HiddenCommand { + static readonly KIND = RequestSvgAction.KIND; + + constructor(@inject(TYPES.Action) protected action: RequestSvgAction) { + super(); + } + + /** Same functionality as for the default SVGCommand provided by Sprotty. */ + execute(context: CommandExecutionContext): CommandResult { + if (isExportable(context.root)) { + const root = context.modelFactory.createRoot(context.root); + if (isExportable(root)) { + if (isViewport(root)) { + root.zoom = 1; + root.scroll = { x: 0, y: 0 }; + } + root.index.all().forEach(element => { + if (isSelectable(element) && element.selected) + {element.selected = false;} + if (isHoverable(element) && element.hoverFeedback) + {element.hoverFeedback = false;} + }); + return { + model: root, + modelChanged: true, + cause: this.action + }; + } + } + return { + model: context.root, + modelChanged: false + }; + } +} \ No newline at end of file diff --git a/extension/src-webview/di.config.ts b/extension/src-webview/di.config.ts index c1bded01..c82cea46 100644 --- a/extension/src-webview/di.config.ts +++ b/extension/src-webview/di.config.ts @@ -15,30 +15,58 @@ * SPDX-License-Identifier: EPL-2.0 */ -import 'sprotty/css/sprotty.css'; -import './css/diagram.css'; +import "sprotty/css/sprotty.css"; +import "./css/diagram.css"; -import { Container, ContainerModule } from 'inversify'; +import { Container, ContainerModule } from "inversify"; import { - ConsoleLogger, HtmlRoot, - HtmlRootView, LogLevel, + ConsoleLogger, + HtmlRoot, + HtmlRootView, + LogLevel, ModelViewer, PreRenderedElement, PreRenderedView, - SGraph, SLabel, + SGraph, + SLabel, SLabelView, SNode, TYPES, + configureCommand, configureModelElement, loadDefaultModules, - overrideViewerOptions -} from 'sprotty'; -import { StpaModelViewer } from './model-viewer'; -import { optionsModule } from './options/options-module'; -import { sidebarModule } from './sidebar'; -import { CSEdge, CSNode, CS_EDGE_TYPE, CS_NODE_TYPE, DUMMY_NODE_TYPE, PARENT_TYPE, STPAEdge, STPANode, STPAPort, STPA_EDGE_TYPE, STPA_INTERMEDIATE_EDGE_TYPE, STPA_NODE_TYPE, STPA_PORT_TYPE } from './stpa-model'; -import { StpaMouseListener } from './stpa-mouselistener'; -import { CSNodeView, IntermediateEdgeView, PolylineArrowEdgeView, PortView, STPAGraphView, STPANodeView } from './views'; + overrideViewerOptions, +} from "sprotty"; +import { SvgCommand } from "./actions"; +import { SvgPostprocessor } from "./exportPostProcessor"; +import { CustomSvgExporter } from "./exporter"; +import { StpaModelViewer } from "./model-viewer"; +import { optionsModule } from "./options/options-module"; +import { sidebarModule } from "./sidebar"; +import { + CSEdge, + CSNode, + CS_EDGE_TYPE, + CS_NODE_TYPE, + DUMMY_NODE_TYPE, + PARENT_TYPE, + STPAEdge, + STPANode, + STPAPort, + STPA_EDGE_TYPE, + STPA_INTERMEDIATE_EDGE_TYPE, + STPA_NODE_TYPE, + STPA_PORT_TYPE, +} from "./stpa-model"; +import { StpaMouseListener } from "./stpa-mouselistener"; +import { + CSNodeView, + IntermediateEdgeView, + PolylineArrowEdgeView, + PortView, + STPAGraphView, + STPANodeView, +} from "./views"; const stpaDiagramModule = new ContainerModule((bind, unbind, isBound, rebind) => { rebind(TYPES.ILogger).to(ConsoleLogger).inSingletonScope(); @@ -46,26 +74,30 @@ const stpaDiagramModule = new ContainerModule((bind, unbind, isBound, rebind) => rebind(TYPES.CommandStackOptions).toConstantValue({ // Override the default animation speed to be 700 ms, as the default value is too quick. defaultDuration: 700, - undoHistoryLimit: 50 + undoHistoryLimit: 50, }); bind(TYPES.MouseListener).to(StpaMouseListener).inSingletonScope(); rebind(ModelViewer).to(StpaModelViewer).inSingletonScope(); + rebind(TYPES.SvgExporter).to(CustomSvgExporter).inSingletonScope(); + bind(SvgPostprocessor).toSelf().inSingletonScope(); + bind(TYPES.HiddenVNodePostprocessor).toService(SvgPostprocessor); + configureCommand({ bind, isBound }, SvgCommand); // configure the diagram elements const context = { bind, unbind, isBound, rebind }; - configureModelElement(context, 'graph', SGraph, STPAGraphView); + configureModelElement(context, "graph", SGraph, STPAGraphView); configureModelElement(context, DUMMY_NODE_TYPE, CSNode, CSNodeView); configureModelElement(context, CS_NODE_TYPE, CSNode, CSNodeView); configureModelElement(context, STPA_NODE_TYPE, STPANode, STPANodeView); configureModelElement(context, PARENT_TYPE, SNode, CSNodeView); - configureModelElement(context, 'label', SLabel, SLabelView); - configureModelElement(context, 'label:xref', SLabel, SLabelView); + configureModelElement(context, "label", SLabel, SLabelView); + configureModelElement(context, "label:xref", SLabel, SLabelView); configureModelElement(context, STPA_EDGE_TYPE, STPAEdge, PolylineArrowEdgeView); configureModelElement(context, STPA_INTERMEDIATE_EDGE_TYPE, STPAEdge, IntermediateEdgeView); configureModelElement(context, CS_EDGE_TYPE, CSEdge, PolylineArrowEdgeView); configureModelElement(context, STPA_PORT_TYPE, STPAPort, PortView); - configureModelElement(context, 'html', HtmlRoot, HtmlRootView); - configureModelElement(context, 'pre-rendered', PreRenderedElement, PreRenderedView); + configureModelElement(context, "html", HtmlRoot, HtmlRootView); + configureModelElement(context, "pre-rendered", PreRenderedElement, PreRenderedView); }); export function createSTPADiagramContainer(widgetId: string): Container { @@ -76,7 +108,7 @@ export function createSTPADiagramContainer(widgetId: string): Container { needsClientLayout: true, needsServerLayout: true, baseDiv: widgetId, - hiddenDiv: widgetId + '_hidden' + hiddenDiv: widgetId + "_hidden", }); return container; } diff --git a/extension/src-webview/diagram-server.ts b/extension/src-webview/diagram-server.ts index 37d57b4d..bf07fd58 100644 --- a/extension/src-webview/diagram-server.ts +++ b/extension/src-webview/diagram-server.ts @@ -16,8 +16,10 @@ */ import { injectable } from "inversify"; -import { ActionMessage } from "sprotty-protocol"; +import { ActionHandlerRegistry } from "sprotty"; +import { Action, ActionMessage } from "sprotty-protocol"; import { VscodeLspEditDiagramServer } from "sprotty-vscode-webview/lib/lsp/editing"; +import { SvgAction } from "./actions"; @injectable() export class StpaDiagramServer extends VscodeLspEditDiagramServer { @@ -27,4 +29,27 @@ export class StpaDiagramServer extends VscodeLspEditDiagramServer { super.sendMessage(message); } + initialize(registry: ActionHandlerRegistry): void { + super.initialize(registry); + registry.register(SvgAction.KIND, this); + } + + handleLocally(action: Action): boolean { + switch (action.kind) { + case SvgAction.KIND: + this.handleSvgAction(action as SvgAction); + } + return super.handleLocally(action); + } + + /** + * Forwards the {@code action} to the server. + * @param action The SVGAction. + * @returns + */ + handleSvgAction(action: SvgAction): boolean { + this.forwardToServer(action); + return false; + } + } \ No newline at end of file diff --git a/extension/src-webview/exportPostProcessor.ts b/extension/src-webview/exportPostProcessor.ts new file mode 100644 index 00000000..7db3ae38 --- /dev/null +++ b/extension/src-webview/exportPostProcessor.ts @@ -0,0 +1,44 @@ +/* + * KIELER - Kiel Integrated Environment for Layout Eclipse RichClient + * + * http://rtsys.informatik.uni-kiel.de/kieler + * + * Copyright 2023 by + * + Kiel University + * + Department of Computer Science + * + Real-Time and Embedded Systems Group + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { inject, injectable } from "inversify"; +import { VNode } from "snabbdom"; +import { IVNodePostprocessor, SModelElement, SModelRoot, TYPES } from "sprotty"; +import { Action } from "sprotty-protocol"; +import { RequestSvgAction } from "./actions"; +import { CustomSvgExporter } from "./exporter"; + +/** Replaces the default SvgPostprocessor to use the custom svg exporter. */ +@injectable() +export class SvgPostprocessor implements IVNodePostprocessor { + + root: SModelRoot; + + @inject(TYPES.SvgExporter) protected svgExporter: CustomSvgExporter; + + decorate(vnode: VNode, element: SModelElement): VNode { + if (element instanceof SModelRoot) { this.root = element; } + return vnode; + } + + postUpdate(cause?: Action): void { + // triggers an internal export + if (this.root && cause !== undefined && cause.kind === RequestSvgAction.KIND) { + this.svgExporter.internalExport(this.root, cause as RequestSvgAction); + } + } +} \ No newline at end of file diff --git a/extension/src-webview/exporter.ts b/extension/src-webview/exporter.ts new file mode 100644 index 00000000..9b45d62a --- /dev/null +++ b/extension/src-webview/exporter.ts @@ -0,0 +1,44 @@ +/* + * KIELER - Kiel Integrated Environment for Layout Eclipse RichClient + * + * http://rtsys.informatik.uni-kiel.de/kieler + * + * Copyright 2023 by + * + Kiel University + * + Department of Computer Science + * + Real-Time and Embedded Systems Group + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { injectable } from "inversify"; +import { SModelRoot, SNode, SvgExporter } from "sprotty"; +import { RequestAction } from "sprotty-protocol"; +import { SvgAction } from "./actions"; + +@injectable() +export class CustomSvgExporter extends SvgExporter { + /** + * Generates an SVG and dispatches an SVGAction. + * @param root The root of the model. + * @param request The request action that triggered this method. + */ + internalExport(root: SModelRoot, request?: RequestAction): void { + if (typeof document !== "undefined") { + const div = document.getElementById(this.options.hiddenDiv); + if (div !== null && div.firstElementChild && div.firstElementChild.tagName === "svg") { + const svgElement = div.firstElementChild as SVGSVGElement; + const svg = this.createSvg(svgElement, root); + const width = + root.children.length > 1 + ? Math.max((root.children[0] as SNode).bounds.width, (root.children[1] as SNode).bounds.width) + : (root.children[0] as SNode).bounds.width; + this.actionDispatcher.dispatch(SvgAction.create(svg, width, request ? request.requestId : "")); + } + } + } +} diff --git a/extension/src-webview/options/options-renderer.tsx b/extension/src-webview/options/options-renderer.tsx index 9acec8db..d1c5d3f1 100644 --- a/extension/src-webview/options/options-renderer.tsx +++ b/extension/src-webview/options/options-renderer.tsx @@ -145,7 +145,7 @@ export class OptionsRenderer { - ) + ); default: console.error("Unsupported option type for option:", option.name); return ""; diff --git a/extension/src/actions.ts b/extension/src/actions.ts index a4fcaab3..b44da5b1 100644 --- a/extension/src/actions.ts +++ b/extension/src/actions.ts @@ -15,25 +15,48 @@ * SPDX-License-Identifier: EPL-2.0 */ -import { Action } from "sprotty-protocol"; +import { Action, JsonMap } from "sprotty-protocol"; /** Contains config option values */ export interface SendConfigAction extends Action { kind: typeof SendConfigAction.KIND; - options: { id: string, value: any; }[]; + options: { id: string; value: any }[]; } export namespace SendConfigAction { export const KIND = "sendConfig"; - export function create(options: { id: string, value: any; }[]): SendConfigAction { + export function create(options: { id: string; value: any }[]): SendConfigAction { return { kind: KIND, - options + options, }; } export function isThisAction(action: Action): action is SendConfigAction { return action.kind === SendConfigAction.KIND; } -} \ No newline at end of file +} + +/** Send to server to generate SVGs for the STPA result report */ +export interface GenerateSVGsAction extends Action { + kind: typeof GenerateSVGsAction.KIND; + uri: string; + options?: JsonMap; +} + +export namespace GenerateSVGsAction { + export const KIND = "generateSVGs"; + + export function create(uri: string, options?: JsonMap): GenerateSVGsAction { + return { + kind: KIND, + uri, + options, + }; + } + + export function isThisAction(action: Action): action is GenerateSVGsAction { + return action.kind === GenerateSVGsAction.KIND; + } +} diff --git a/extension/src/constants.ts b/extension/src/constants.ts index 42c5acb2..b2d5042c 100644 --- a/extension/src/constants.ts +++ b/extension/src/constants.ts @@ -25,4 +25,3 @@ export const command = { getLTLFormula: withPrefix("getLTLFormula"), // sendModelCheckerResult: withPrefix("sendModelCheckerResult"), }; - diff --git a/extension/src/context-table-panel.ts b/extension/src/context-table-panel.ts index 6b4dfc49..c1798114 100644 --- a/extension/src/context-table-panel.ts +++ b/extension/src/context-table-panel.ts @@ -15,20 +15,18 @@ * SPDX-License-Identifier: EPL-2.0 */ +import { TableWebview } from "@kieler/table-webview/lib/table-webview"; import * as vscode from "vscode"; -import { TableWebview } from '@kieler/table-webview/lib/table-webview'; -import { SendContextTableDataAction } from '../src-context-table/actions'; -import { ContextTableData } from '../src-context-table/utils'; +import { SendContextTableDataAction } from "../src-context-table/actions"; +import { ContextTableData } from "../src-context-table/utils"; export class ContextTablePanel extends TableWebview { + constructor(identifier: string, localResourceRoots: vscode.Uri[], scriptUri: vscode.Uri) { + super(identifier, localResourceRoots, scriptUri); + this.createWebviewPanel([]); + } - constructor(identifier: string, localResourceRoots: vscode.Uri[], scriptUri: vscode.Uri) { - super(identifier, localResourceRoots, scriptUri); - this.createWebviewPanel([]); - } - - setData(data: ContextTableData): void { - this.sendToWebview({ action: SendContextTableDataAction.create(data) }); - } - -} \ No newline at end of file + setData(data: ContextTableData): void { + this.sendToWebview({ action: SendContextTableDataAction.create(data) }); + } +} diff --git a/extension/src/extension.ts b/extension/src/extension.ts index 64237d7f..c4de20ce 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -24,8 +24,10 @@ import { Messenger } from 'vscode-messenger'; import { command } from './constants'; import { StpaLspVscodeExtension } from './language-extension'; import { createQuickPickForWorkspaceOptions } from './utils'; +import { createSTPAResultMarkdownFile } from './report/md-export'; import { LTLFormula } from './sbm/utils'; import { createSBMs } from './sbm/sbm-generation'; +import { StpaResult } from './report/utils'; let languageClient: LanguageClient; @@ -139,6 +141,14 @@ function registerSTPACommands(manager: StpaLspVscodeExtension, context: vscode.E }) ); + // command for creating a pdf + context.subscriptions.push( + vscode.commands.registerCommand(options.extensionPrefix + '.md.creation', async (uri: vscode.Uri) => { + const data: StpaResult = await languageClient.sendRequest('result/getData', uri.toString()); + await createSTPAResultMarkdownFile(data, manager); + }) + ); + context.subscriptions.push( vscode.commands.registerCommand(options.extensionPrefix + '.SBM.generation', async (uri: vscode.Uri) => { await manager.lsReady; diff --git a/extension/src/language-extension.ts b/extension/src/language-extension.ts index 43e01a20..530d412f 100644 --- a/extension/src/language-extension.ts +++ b/extension/src/language-extension.ts @@ -15,17 +15,18 @@ * SPDX-License-Identifier: EPL-2.0 */ -import { SelectAction } from 'sprotty-protocol'; -import { createFileUri } from 'sprotty-vscode'; -import { SprottyDiagramIdentifier } from 'sprotty-vscode-protocol'; -import { LspWebviewEndpoint, LspWebviewPanelManager, LspWebviewPanelManagerOptions } from 'sprotty-vscode/lib/lsp'; -import * as vscode from 'vscode'; -import { ContextTablePanel } from './context-table-panel'; -import { StpaFormattingEditProvider } from './stpa-formatter'; -import { StpaLspWebview } from './wview'; +import { ActionMessage, JsonMap, SelectAction } from "sprotty-protocol"; +import { createFileUri } from "sprotty-vscode"; +import { SprottyDiagramIdentifier } from "sprotty-vscode-protocol"; +import { LspWebviewEndpoint, LspWebviewPanelManager, LspWebviewPanelManagerOptions } from "sprotty-vscode/lib/lsp"; +import * as vscode from "vscode"; +import { GenerateSVGsAction } from "./actions"; +import { ContextTablePanel } from "./context-table-panel"; +import { StpaFormattingEditProvider } from "./stpa-formatter"; +import { applyTextEdits, collectOptions, createFile } from "./utils"; +import { StpaLspWebview } from "./wview"; export class StpaLspVscodeExtension extends LspWebviewPanelManager { - protected extensionPrefix: string; public contextTable: ContextTablePanel; @@ -33,7 +34,7 @@ export class StpaLspVscodeExtension extends LspWebviewPanelManager { protected lastSelectedUCA: string[]; protected resolveLSReady: () => void; - readonly lsReady = new Promise(resolve => this.resolveLSReady = resolve); + readonly lsReady = new Promise((resolve) => (this.resolveLSReady = resolve)); /** needed for undo/redo actions when ID enforcement is active*/ ignoreNextTextChange: boolean = false; @@ -44,37 +45,58 @@ export class StpaLspVscodeExtension extends LspWebviewPanelManager { // user changed configuration settings vscode.workspace.onDidChangeConfiguration(() => { // sends configuration of stpa to the language server - options.languageClient.sendNotification('configuration', this.collectOptions(vscode.workspace.getConfiguration('pasta'))); + this.languageClient.sendNotification( + "configuration", + collectOptions(vscode.workspace.getConfiguration("pasta")) + ); }); // add auto formatting provider - const sel: vscode.DocumentSelector = { scheme: 'file', language: 'stpa' }; + const sel: vscode.DocumentSelector = { scheme: "file", language: "stpa" }; vscode.languages.registerDocumentFormattingEditProvider(sel, new StpaFormattingEditProvider()); // handling of notifications regarding the context table - options.languageClient.onNotification('contextTable/data', data => this.contextTable.setData(data)); - options.languageClient.onNotification('editor/highlight', (msg: { startLine: number, startChar: number, endLine: number, endChar: number; uri: string; }) => { - // highlight and reveal the given range in the editor - const editor = vscode.window.visibleTextEditors.find(visibleEditor => visibleEditor.document.uri.toString() === msg.uri); - if (editor) { - const startPosition = new vscode.Position(msg.startLine, msg.startChar); - const endPosition = new vscode.Position(msg.endLine, msg.endChar); - editor.selection = new vscode.Selection(startPosition, endPosition); - editor.revealRange(editor.selection, vscode.TextEditorRevealType.InCenter); + options.languageClient.onNotification("contextTable/data", (data) => this.contextTable.setData(data)); + options.languageClient.onNotification( + "editor/highlight", + (msg: { startLine: number; startChar: number; endLine: number; endChar: number; uri: string }) => { + // highlight and reveal the given range in the editor + const editor = vscode.window.visibleTextEditors.find( + (visibleEditor) => visibleEditor.document.uri.toString() === msg.uri + ); + if (editor) { + const startPosition = new vscode.Position(msg.startLine, msg.startChar); + const endPosition = new vscode.Position(msg.endLine, msg.endChar); + editor.selection = new vscode.Selection(startPosition, endPosition); + editor.revealRange(editor.selection, vscode.TextEditorRevealType.InCenter); + } } - }); + ); // textdocument has changed - vscode.workspace.onDidChangeTextDocument(changeEvent => { this.handleTextChangeEvent(changeEvent); }); + vscode.workspace.onDidChangeTextDocument((changeEvent) => { + this.handleTextChangeEvent(changeEvent); + }); // language client sent workspace edits - options.languageClient.onNotification('editor/workspaceedit', ({ edits, uri }) => this.applyTextEdits(edits, uri)); + options.languageClient.onNotification("editor/workspaceedit", ({ edits, uri }) => applyTextEdits(edits, uri)); // laguage server is ready options.languageClient.onNotification("ready", () => { this.resolveLSReady(); // open diagram - vscode.commands.executeCommand(this.extensionPrefix + '.diagram.open', vscode.window.activeTextEditor?.document.uri); + vscode.commands.executeCommand( + this.extensionPrefix + ".diagram.open", + vscode.window.activeTextEditor?.document.uri + ); // sends configuration of stpa to the language server - options.languageClient.sendNotification('configuration', this.collectOptions(vscode.workspace.getConfiguration('pasta'))); + options.languageClient.sendNotification( + "configuration", + collectOptions(vscode.workspace.getConfiguration("pasta")) + ); + }); + + // server sent svg that should be saved + this.languageClient.onNotification("svg", ({ uri, svg }) => { + createFile(uri, svg); }); } @@ -91,60 +113,21 @@ export class StpaLspVscodeExtension extends LspWebviewPanelManager { // send the changes to the language server const changes = changeEvent.contentChanges; const uri = changeEvent.document.uri.toString(); - this.languageClient.sendNotification('editor/textChange', { changes: changes, uri: uri }); + this.languageClient.sendNotification("editor/textChange", { changes: changes, uri: uri }); } - /** - * Applies text edits to the document. - * @param edits The edits to apply. - * @param uri The uri of the document that should be edited. - */ - protected async applyTextEdits(edits: vscode.TextEdit[], uri: string): Promise { - // create a workspace edit - const workSpaceEdit = new vscode.WorkspaceEdit(); - workSpaceEdit.set(vscode.Uri.parse(uri), edits); - // Apply the edit. Report possible failures. - const edited = await vscode.workspace.applyEdit(workSpaceEdit); - if (!edited) { - console.error("Workspace edit could not be applied!"); - return; - } - } - - /** - * Collects the STPA options of the configuration settings and returns them as a list of their ids and values. - * @param configuration The workspace configuration options. - * @returns A list of the workspace options, whereby a option is represented with an id and its value. - */ - protected collectOptions(configuration: vscode.WorkspaceConfiguration): { id: string, value: any; }[] { - const values: { id: string, value: any; }[] = []; - values.push({ id: "checkResponsibilitiesForConstraints", value: configuration.get("checkResponsibilitiesForConstraints") }); - values.push({ id: "checkConstraintsForUCAs", value: configuration.get("checkConstraintsForUCAs") }); - values.push({ id: "checkScenariosForUCAs", value: configuration.get("checkScenariosForUCAs") }); - values.push({ id: "checkSafetyRequirementsForUCAs", value: configuration.get("checkSafetyRequirementsForUCAs") }); - return values; - } - - // protected getDiagramType(uri: vscode.Uri): string | undefined { - // if (commandArgs.length === 0 - // || commandArgs[0] instanceof vscode.Uri && commandArgs[0].path.endsWith('.stpa')) { - // return 'stpa-diagram'; - // } - // return undefined; - // } - createContextTable(context: vscode.ExtensionContext): void { const extensionPath = this.options.extensionUri.fsPath; const tablePanel = new ContextTablePanel( - 'Context-Table', - [createFileUri(extensionPath, 'pack')], - createFileUri(extensionPath, 'pack', 'context-table-panel.js') + "Context-Table", + [createFileUri(extensionPath, "pack")], + createFileUri(extensionPath, "pack", "context-table-panel.js") ); this.contextTable = tablePanel; // adds listener for mouse click on a cell context.subscriptions.push( - this.contextTable.cellClicked((cell: { rowId: string; columnId: string, text?: string; } | undefined) => { + this.contextTable.cellClicked((cell: { rowId: string; columnId: string; text?: string } | undefined) => { if (cell?.text === "No") { // delete selection in the diagram this.endpoints[0].sendAction(SelectAction.create({ deselectedElementsIDs: this.lastSelectedUCA })); @@ -153,9 +136,11 @@ export class StpaLspVscodeExtension extends LspWebviewPanelManager { const texts = cell.text.split(","); // language server must determine the range of the selected uca in the editor in order to highlight it // when there are multiple UCAs in the cell only the first one is highlighted in the editor - this.languageClient.sendNotification('contextTable/selected', texts[0]); + this.languageClient.sendNotification("contextTable/selected", texts[0]); // highlight corresponding node in the diagram and maybe deselect the last selected one - this.endpoints[0].sendAction(SelectAction.create({ selectedElementsIDs: texts, deselectedElementsIDs: this.lastSelectedUCA })); + this.endpoints[0].sendAction( + SelectAction.create({ selectedElementsIDs: texts, deselectedElementsIDs: this.lastSelectedUCA }) + ); this.lastSelectedUCA = texts; } }) @@ -170,8 +155,41 @@ export class StpaLspVscodeExtension extends LspWebviewPanelManager { webviewContainer, messenger: this.messenger, messageParticipant: participant, - identifier + identifier, }); } + /** + * Triggers the creation of SVGs for the current model. + * @param uri The folder uri where to save the SVGs. + * @returns the widths of the resulting SVGs with the SVG name as the key. + */ + async createSVGDiagrams(uri: string): Promise> { + if (this.endpoints.length !== 0) { + const activeWebview = this.endpoints[0]; + if (activeWebview?.diagramIdentifier) { + // create GenerateSVGsAction + const mes: ActionMessage = { + clientId: activeWebview.diagramIdentifier.clientId, + action: { + kind: GenerateSVGsAction.KIND, + options: { + diagramType: activeWebview.diagramIdentifier.diagramType, + needsClientLayout: true, + needsServerLayout: true, + sourceUri: activeWebview.diagramIdentifier.uri, + } as JsonMap, + uri: uri, + } as GenerateSVGsAction, + }; + // send request + const diagramSize: Record = await this.languageClient.sendRequest( + "result/createDiagrams", + mes + ); + return diagramSize; + } + } + return {}; + } } diff --git a/extension/src/report/md-export.ts b/extension/src/report/md-export.ts new file mode 100644 index 00000000..90fa1473 --- /dev/null +++ b/extension/src/report/md-export.ts @@ -0,0 +1,432 @@ +/* + * KIELER - Kiel Integrated Environment for Layout Eclipse RichClient + * + * http://rtsys.informatik.uni-kiel.de/kieler + * + * Copyright 2023 by + * + Kiel University + * + Department of Computer Science + * + Real-Time and Embedded Systems Group + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import * as dayjs from "dayjs"; +import * as vscode from "vscode"; +import { StpaLspVscodeExtension } from "../language-extension"; +import { UCA_TYPE, createFile } from "../utils"; +import { + COMPLETE_GRAPH_PATH, + CONTROLLER_CONSTRAINT_PATH, + CONTROL_STRUCTURE_PATH, + FILTERED_CONTROLLER_CONSTRAINT_PATH, + FILTERED_SCENARIO_PATH, + FILTERED_UCA_PATH, + HAZARD_PATH, + Headers, + RESPONSIBILITY_PATH, + SAFETY_REQUIREMENT_PATH, + SCENARIO_PATH, + SCENARIO_WITH_HAZARDS_PATH, + SIZE_MULTIPLIER, + SVG_PATH, + SYSTEM_CONSTRAINT_PATH, + StpaComponent, + StpaResult, + UCA_PATH, +} from "./utils"; + +/** + * Creates a markdown file for the given {@code data}. + * @param data The STPA result for which the markdown file should be created. + * @param extension The PASTA extension. + * @returns + */ +export async function createSTPAResultMarkdownFile(data: StpaResult, extension: StpaLspVscodeExtension): Promise { + // Ask the user where to save the markdown file + const currentFolder = vscode.workspace.workspaceFolders + ? vscode.workspace.workspaceFolders[0].uri.fsPath + : undefined; + const uri = await vscode.window.showSaveDialog({ + filters: { Markdown: ["md"] }, + // TODO: not possible with current vscode version + // title: 'Save Markdown to...', + defaultUri: currentFolder ? vscode.Uri.file(`${currentFolder}/report.md`) : undefined, + }); + if (uri === undefined) { + // The user did not pick any file to save to. + return; + } + + // create svg diagrams and save their width + const diagramSizes: Record = await extension.createSVGDiagrams( + uri.path.substring(0, uri.path.lastIndexOf("/")) + SVG_PATH + ); + + // create the markdown text + const markdown = createSTPAResultMarkdownText(data, diagramSizes); + // create the file + createFile(uri.path, markdown); +} + +/** + * Creates the markdown text for the given {@code data}. + * @param data The STPA result for which the text should be created. + * @param diagramSizes The widths of the diagrams that will be embedded. + * @returns markdown text for the given {@code data} + */ +function createSTPAResultMarkdownText(data: StpaResult, diagramSizes: Record): string { + // TODO: consider context table + let markdown = `# STPA Report for ${data.title}\n\n`; + // table of contents + markdown += createTOC(); + // losses + markdown += stpaAspectToMarkdown(Headers.Loss, data.losses) + "\n"; + // hazards + markdown += stpaAspectToMarkdown(Headers.Hazard, data.hazards, HAZARD_PATH, diagramSizes); + // system-level constraints + markdown += stpaAspectToMarkdown( + Headers.SystemLevelConstraint, + data.systemLevelConstraints, + SYSTEM_CONSTRAINT_PATH, + diagramSizes + ); + // control structure + markdown += addControlStructure(diagramSizes); + // responsibilities + markdown += recordToMarkdown(data.responsibilities, Headers.Responsibility); + if (Object.keys(data.responsibilities).length > 0) { + markdown += `\n\n
\n\n`; + } + // UCAs + markdown += ucasToMarkdown(data.ucas, diagramSizes); + // controller constraints + markdown += constraintsToMarkdown(Headers.ControllerConstraint, data.controllerConstraints, diagramSizes); + // loss scenarios + markdown += scenariosToMarkdown(data.ucaScenarios, data.scenarios, diagramSizes); + // safety requirements + markdown += stpaAspectToMarkdown(Headers.SafetyRequirement, data.safetyConstraints, SAFETY_REQUIREMENT_PATH, diagramSizes); + // summarized safety constraints + markdown += addSummary(data, diagramSizes); + // copyright + markdown += addCopyRight(); + return markdown; +} + +/** + * Translates an STPA aspect to markdown text. + * @param aspect The header for the aspect to translate. + * @param components The components of the aspect to translate. + * @param svgName The name of the diagram that should be embedded. + * @param diagramSizes The widths of the diagrams that should be embedded. + * @returns the markdown text for the given aspect and its components. + */ +function stpaAspectToMarkdown( + aspect: string, + components: StpaComponent[], + svgName?: string, + diagramSizes?: Record +): string { + let markdown = `## ${aspect}\n\n`; + if (components.length === 0) { + // extra text if no components are defined. + markdown += `No ${aspect} defined.\n`; + } else { + // translate each component + for (const component of components) { + markdown += stpaComponentToMarkdown(component) + ` \n`; + if (component.subComponents) { + // translate the subcomponents of hazards/system-level constraints + markdown += subComponentsToMarkdown(component.subComponents, "      "); + } + } + // add the diagram if one is given + if (svgName && diagramSizes) { + markdown += `\n\n\n
\n\n`; + } + } + return markdown; +} + +/** + * Translates subcomponents (of Hayards/system-level constraints) to markdown. + * @param components The components to translate. + * @param tabs The current indentation. + * @returns the markdown text for the given {@code components}. + */ +function subComponentsToMarkdown(components: StpaComponent[], tabs: string): string { + let markdown = ""; + for (const component of components) { + // translate component + markdown += `${tabs} **${component.id}**: ${component.description} \n`; + if (component.subComponents) { + // translate further subcomponents + markdown += subComponentsToMarkdown(component.subComponents, tabs + "      "); + } + } + return markdown; +} + +/** + * Translates a single UCA to markdown. + * @param component The UCA to translate. + * @returns the markdown text for the given UCA. + */ +function ucaComponentToMarkdown(component: StpaComponent): string { + let markdown = `${component.id}: ${component.description}`; + if (component.references !== undefined && component.references !== "") { + markdown += ` [${component.references}]`; + } + return markdown; +} + +/** + * Translates a single STPA component to markdown. + * @param component The component to translate. + * @returns the markdown text for the given {@code component}. + */ +function stpaComponentToMarkdown(component: StpaComponent): string { + // Translation form: "**ID**: description [Refs]" + let markdown = `**${component.id}**: ${component.description}`; + // not all components have references + if (component.references !== undefined && component.references !== "") { + markdown += ` [${component.references}]`; + } + return markdown; +} + +/** + * Translates a record (responsibilities/loss scenarios) to markdown. + * @param aspect The header of the aspect to translate. + * @param data The data to translate. + * @returns the markdown text for the given {@code data}. + */ +function recordToMarkdown(data: Record, aspect?: string): string { + let markdown = ``; + if (aspect) { + markdown += `## ${aspect}\n\n`; + if (Object.keys(data).length === 0) { + // extra text if no component is defined + markdown += `No ${aspect} defined.\n`; + return markdown; + } + } + for (const reference in data) { + // the components are grouped by their keys + markdown += `_${reference}_ \n`; + // translate the components + for (const component of data[reference]) { + markdown += stpaComponentToMarkdown(component); + markdown += ` \n`; + } + markdown += `\n`; + } + + return markdown; +} + +/** + * Translates loss scenarios to markdown. + * @param ucaScenarios The scenarios with reference to an UCA. + * @param scenarios The scenarios without a reference to an UCA. + * @param diagramSizes The widths of the diagrams to include. + * @returns the markdown text for the given scenarios. + */ +function scenariosToMarkdown( + ucaScenarios: Record>, + scenarios: StpaComponent[], + diagramSizes: Record +): string { + // translate the uca scenarios + let markdown = `## ${Headers.LossScenario}\n\n`; + markdown += `### Scenarios with associated UCA\n\n`; + for (const key of Object.keys(ucaScenarios)) { + markdown += `#### _${key}_\n\n`; + markdown += recordToMarkdown(ucaScenarios[key]) + "\n"; + // add the filtered diagram for the control action + const path = FILTERED_SCENARIO_PATH(key); + markdown += `\n\n
\n\n`; + } + + // translate the other scenarios + if (scenarios.length !== 0) { + markdown += `### Scenarios without associated UCA\n\n`; + markdown += scenarios.map((scenario) => stpaComponentToMarkdown(scenario)).join(" \n") + `\n\n`; + markdown += `\n\n`; + } + + // add the diagram for all scenarios + markdown += `### All Scenarios\n\n`; + markdown += `\n\n
\n\n`; + return markdown; +} + +/** + * Translates the UCAs to a markdown table. + * @param actionUcas The UCAs to translate. + * @param diagramSizes The widths of the diagrams to include. + * @returns the markdown table for the UCAs. + */ +function ucasToMarkdown( + actionUcas: Record>, + diagramSizes: Record +): string { + let markdown = `## ${Headers.UCA}\n\n`; + for (const actionUCA of Object.keys(actionUcas)) { + // for each control action a table is generated + markdown += `### _${actionUCA}_\n\n`; + // header of the table containing the UCA types + markdown += `\n\n\n\n\n\n\n`; + markdown += "\n\n\n\n\n
not providedprovidedtoo late or too earlyapplied too long or stopped too soon
\n"; + // add not provided UCAs + markdown += actionUcas[actionUCA][UCA_TYPE.NOT_PROVIDED] + ?.map((uca) => ucaComponentToMarkdown(uca)) + .join("

"); + markdown += "
\n"; + // add provided UCAs + markdown += actionUcas[actionUCA][UCA_TYPE.PROVIDED] + ?.map((uca) => ucaComponentToMarkdown(uca)) + .join("

"); + markdown += "
\n"; + // add wrong timing UCAs + markdown += actionUcas[actionUCA][UCA_TYPE.WRONG_TIME] + ?.map((uca) => ucaComponentToMarkdown(uca)) + .join("

"); + markdown += "
\n"; + // add continous UCAs + markdown += actionUcas[actionUCA][UCA_TYPE.CONTINUOUS] + ?.map((uca) => ucaComponentToMarkdown(uca)) + .join("

"); + markdown += "
\n\n
\n\n"; + // add the filtered diagram for the control action + const path = FILTERED_UCA_PATH(actionUCA); + markdown += `\n\n
\n\n`; + } + // add a diagram for all UCAs + markdown += `### _All UCAs_\n\n`; + markdown += `\n\n
\n\n`; + return markdown; +} + +function constraintsToMarkdown( + header: string, + constraints: Record, + diagramSizes: Record +): string { + let markdown = `## ${header}\n\n`; + if (Object.keys(constraints).length === 0) { + // extra text if no component is defined + markdown += `No constraints defined.\n`; + } + for (const key of Object.keys(constraints)) { + markdown += `### _${key}_\n\n`; + markdown += constraints[key].map((constraint) => stpaComponentToMarkdown(constraint)).join(" \n") + "\n"; + const path = FILTERED_CONTROLLER_CONSTRAINT_PATH(key); + markdown += `\n\n\n`; + } + // add a diagram for all constraints + markdown += `### _All Controller Constraints_\n\n`; + markdown += `\n\n
\n\n`; + return markdown; +} + +/** + * Adds a summary of the defined constraints. + * @param data The STPA result data. + * @param diagramSizes The widths of the diagrams to include. + * @returns the markdown text for the summary of the defined constraints. + */ +function addSummary(data: StpaResult, diagramSizes: Record): string { + let markdown = `## ${Headers.Summary}\n\n`; + // add system-level constraints + for (const component of data.systemLevelConstraints) { + markdown += stpaComponentToMarkdown(component); + markdown += ` \n`; + } + // add controller constraints + for (const key of Object.keys(data.controllerConstraints)) { + for (const component of data.controllerConstraints[key]) { + markdown += stpaComponentToMarkdown(component); + markdown += ` \n`; + } + } + // add safety constraints + for (const component of data.safetyConstraints) { + markdown += stpaComponentToMarkdown(component); + markdown += ` \n`; + } + // add a diagram of the whole diagram + markdown += `\n\n\n\n`; + return markdown; +} + +/** + * Creates the markdown text to include the control structure diagram. + * @param diagramSizes The witdhs of the diagrams to include. + * @returns the markdown text to include the control structure diagram. + */ +function addControlStructure(diagramSizes: Record): string { + let markdown = `## ${Headers.ControlStructure}\n\n`; + markdown += `\n\n
\n\n`; + return markdown; +} + +/** + * Creates a copyright for the markdown file. + * @returns the copyright for the markdown file. + */ +function addCopyRight(): string { + const markdown = + "

\n\nSTPA Report generated by PASTA, " + + dayjs().format("YYYY-MM-DD HH:mm:ss") + + " (https://github.com/kieler/stpa)"; + return markdown; +} + +/** + * Creates a table of contents for the markdown file. + * @returns markdown text for the table of contents. + */ +function createTOC(): string { + //TODO: use regex for the whitespace + let markdown = "## Table of Contents\n\n"; + markdown += `1. [${Headers.Loss}](#${Headers.Loss.toLowerCase()})\n`; + markdown += `2. [${Headers.Hazard}](#${Headers.Hazard.toLowerCase()})\n`; + markdown += `3. [${Headers.SystemLevelConstraint}](#${Headers.SystemLevelConstraint.toLowerCase().replace( + " ", + "-" + )})\n`; + markdown += `4. [${Headers.ControlStructure}](#${Headers.ControlStructure.toLowerCase().replace(" ", "-")})\n`; + markdown += `5. [${Headers.Responsibility}](#${Headers.Responsibility.toLowerCase()})\n`; + markdown += `6. [${Headers.UCA}](#${Headers.UCA.toLowerCase()})\n`; + markdown += `7. [${Headers.ControllerConstraint}](#${Headers.ControllerConstraint.toLowerCase().replace( + " ", + "-" + )})\n`; + markdown += `8. [${Headers.LossScenario}](#${Headers.LossScenario.toLowerCase().replace(" ", "-")})\n`; + markdown += `9. [${Headers.SafetyRequirement}](#${Headers.SafetyRequirement.toLowerCase().replace(" ", "-")})\n`; + markdown += `10. [${Headers.ControllerConstraint}](#${Headers.ControllerConstraint.toLowerCase().replace( + " ", + "-" + )})\n`; + markdown += `11. [${Headers.Summary}](#${Headers.Summary.toLowerCase().replace(" ", "-").replace(" ", "-")})\n`; + return markdown; +} diff --git a/extension/src/report/utils.ts b/extension/src/report/utils.ts new file mode 100644 index 00000000..0747c6a5 --- /dev/null +++ b/extension/src/report/utils.ts @@ -0,0 +1,81 @@ +/* + * KIELER - Kiel Integrated Environment for Layout Eclipse RichClient + * + * http://rtsys.informatik.uni-kiel.de/kieler + * + * Copyright 2023 by + * + Kiel University + * + Department of Computer Science + * + Real-Time and Embedded Systems Group + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + */ + +/** + * The Headers for the STPA result file. + */ +export class Headers { + static Loss = "Losses"; + static Hazard = "Hazards"; + static SystemLevelConstraint = "System-level Constraints"; + static Responsibility = "Responsibilities"; + static ControlStructure = "Control Structure"; + static UCA = "UCAs"; + static ControllerConstraint = "Controller Constraints"; + static LossScenario = "Loss Scenarios"; + static SafetyRequirement = "Safety Requirements"; + static Summary = "Summarized Safety Constraints"; +} + +/* the paths for the several diagrams of the STPA aspects */ +export const SVG_PATH = "/images"; +export const CONTROL_STRUCTURE_PATH = "/control-structure.svg"; +export const HAZARD_PATH = "/hazard.svg"; +export const SYSTEM_CONSTRAINT_PATH = "/system-constraint.svg"; +export const RESPONSIBILITY_PATH = "/responsibility.svg"; +export const SAFETY_REQUIREMENT_PATH = "/safety-requirement.svg"; +export const COMPLETE_GRAPH_PATH = "/complete-graph.svg"; +export const FILTERED_UCA_PATH = (controlAction: string): string => { + return "/ucas/" + controlAction.replace(".", "-").replace(" ", "-") + ".svg"; +}; +export const UCA_PATH = FILTERED_UCA_PATH("all-UCAs"); +export const FILTERED_CONTROLLER_CONSTRAINT_PATH = (controlAction: string): string => { + return "/controller-constraints/" + controlAction.replace(".", "-").replace(" ", "-") + ".svg"; +}; +export const CONTROLLER_CONSTRAINT_PATH = FILTERED_CONTROLLER_CONSTRAINT_PATH("all-UCAs"); +export const FILTERED_SCENARIO_PATH = (controlAction: string): string => { + return "/scenarios/" + controlAction.replace(".", "-").replace(" ", "-") + ".svg"; +}; +export const SCENARIO_PATH = FILTERED_SCENARIO_PATH("all-UCAs"); +export const SCENARIO_WITH_HAZARDS_PATH = FILTERED_SCENARIO_PATH("no-UCAs"); + +/* size multiplier for the diagrams */ +export const SIZE_MULTIPLIER = 0.85; + +export class StpaResult { + title: string; + losses: StpaComponent[] = []; + hazards: StpaComponent[] = []; + systemLevelConstraints: StpaComponent[] = []; + // sorted by system components + responsibilities: Record = {}; + // sorted first by control action, then by uca type + ucas: Record> = {}; + // sorted by control action + controllerConstraints: Record = {}; + // sorted by control action and by ucas + ucaScenarios: Record> = {}; + scenarios: StpaComponent[] = []; + safetyConstraints: StpaComponent[] = []; +} + +export class StpaComponent { + id: string; + description: string; + references?: string; + subComponents?: StpaComponent[]; +} \ No newline at end of file diff --git a/extension/src/sbm/sbm-generation.ts b/extension/src/sbm/sbm-generation.ts index 1cf4b08a..74ae5e15 100644 --- a/extension/src/sbm/sbm-generation.ts +++ b/extension/src/sbm/sbm-generation.ts @@ -16,9 +16,9 @@ */ import * as vscode from "vscode"; -import { EMPTY_STATE_NAME, Enum, LTLFormula, State, Transition, UCA_TYPE, Variable } from "./utils"; -import { createSCChartText } from "./scchart-creation"; import { createFile } from "../utils"; +import { createSCChartText } from "./scchart-creation"; +import { EMPTY_STATE_NAME, Enum, LTLFormula, State, Transition, UCA_TYPE, Variable } from "./utils"; /** * Creates a safe behavioral model for each controller. diff --git a/extension/src/stpa-formatter.ts b/extension/src/stpa-formatter.ts index 4784ed27..c50a3255 100644 --- a/extension/src/stpa-formatter.ts +++ b/extension/src/stpa-formatter.ts @@ -15,13 +15,25 @@ * SPDX-License-Identifier: EPL-2.0 */ -import { CancellationToken, DocumentFormattingEditProvider, FormattingOptions, Position, ProviderResult, Range, TextDocument, TextEdit } from "vscode"; +import { + CancellationToken, + DocumentFormattingEditProvider, + FormattingOptions, + Position, + ProviderResult, + Range, + TextDocument, + TextEdit, +} from "vscode"; export class StpaFormattingEditProvider implements DocumentFormattingEditProvider { - protected tabSize: number; - provideDocumentFormattingEdits(document: TextDocument, options: FormattingOptions, token: CancellationToken): ProviderResult { + provideDocumentFormattingEdits( + document: TextDocument, + options: FormattingOptions, + token: CancellationToken + ): ProviderResult { this.tabSize = options.tabSize; const edits: TextEdit[] = []; const text = document.getText(); @@ -46,17 +58,17 @@ export class StpaFormattingEditProvider implements DocumentFormattingEditProvide for (let i = 1; i < splits.length; i++) { offset += splits[i - 1].length; switch (splits[i - 1][splits[i - 1].length - 1]) { - case '\n': + case "\n": this.formatIndentation(offset, document, openParens, edits, splits[i]); break; - case ']': + case "]": this.formatClosedBracket(offset, document, openParens, edits, splits[i]); break; - case '{': + case "{": openParens++; this.formatNewLineAfter(offset, document, openParens, edits, splits[i]); break; - case '}': + case "}": this.formatNewLineBefore(offset, document, openParens, edits, splits[i - 1]); openParens--; this.formatNewLineAfter(offset, document, openParens, edits, splits[i]); @@ -86,12 +98,18 @@ export class StpaFormattingEditProvider implements DocumentFormattingEditProvide * @param edits Array to push the created edits to. * @param line The text to indent. */ - protected formatIndentation(offset: number, document: TextDocument, openParens: number, edits: TextEdit[], line: string): void { + protected formatIndentation( + offset: number, + document: TextDocument, + openParens: number, + edits: TextEdit[], + line: string + ): void { let whiteSpaces = 0; - while (line[whiteSpaces] === ' ') { + while (line[whiteSpaces] === " ") { whiteSpaces++; } - if (line[whiteSpaces] === '}') { + if (line[whiteSpaces] === "}") { openParens--; } // adjust whitespaces @@ -106,14 +124,20 @@ export class StpaFormattingEditProvider implements DocumentFormattingEditProvide * @param edits Array to push the created edits to. * @param line The text at which end a newline should be inserted. */ - protected formatNewLineBefore(offset: number, document: TextDocument, openParens: number, edits: TextEdit[], line: string): void { + protected formatNewLineBefore( + offset: number, + document: TextDocument, + openParens: number, + edits: TextEdit[], + line: string + ): void { const trimmed = line.trim(); - if (trimmed !== '}') { + if (trimmed !== "}") { const newOffset = offset - 1; const pos: Position = document.positionAt(newOffset); // IMPORTANT: "\r\n" is specific to windows, linux just uses "\n" - edits.push(TextEdit.insert(pos, '\r\n')); - this.formatIndentation(newOffset + 1, document, openParens, edits, line.substring(line.indexOf('}'))); + edits.push(TextEdit.insert(pos, "\r\n")); + this.formatIndentation(newOffset + 1, document, openParens, edits, line.substring(line.indexOf("}"))); } } @@ -125,19 +149,25 @@ export class StpaFormattingEditProvider implements DocumentFormattingEditProvide * @param edits Array to push the created edits to. * @param line The text before which a newline should be inserted. */ - protected formatNewLineAfter(offset: number, document: TextDocument, openParens: number, edits: TextEdit[], line: string): void { + protected formatNewLineAfter( + offset: number, + document: TextDocument, + openParens: number, + edits: TextEdit[], + line: string + ): void { let nextChar = 0; - while (line[nextChar] === ' ') { + while (line[nextChar] === " ") { nextChar++; } const startPos = document.positionAt(offset); const endPos = document.positionAt(offset + nextChar); const delRange = new Range(startPos, endPos); edits.push(TextEdit.delete(delRange)); - if (line[nextChar] !== '\r') { + if (line[nextChar] !== "\r") { const pos: Position = document.positionAt(offset); // IMPORTANT: "\r\n" is specific to windows, linux just uses "\n" - edits.push(TextEdit.insert(pos, '\r\n')); + edits.push(TextEdit.insert(pos, "\r\n")); this.formatIndentation(offset + nextChar, document, openParens, edits, line.substring(nextChar)); } } @@ -150,13 +180,19 @@ export class StpaFormattingEditProvider implements DocumentFormattingEditProvide * @param edits Array to push the created edits to. * @param line The text before which a newline should be inserted. */ - protected formatClosedBracket(offset: number, document: TextDocument, openParens: number, edits: TextEdit[], line: string): void { + protected formatClosedBracket( + offset: number, + document: TextDocument, + openParens: number, + edits: TextEdit[], + line: string + ): void { const trimmed = line.trim(); - if (trimmed[0] === '-') { + if (trimmed[0] === "-") { // bracket belongs to a control action or feedback in the control structure // format before arrow let beforeWhitespaces = 0; - while (line[beforeWhitespaces] === ' ') { + while (line[beforeWhitespaces] === " ") { beforeWhitespaces++; } this.adjustWhitespaces(beforeWhitespaces, document, offset, 1, edits); @@ -164,11 +200,11 @@ export class StpaFormattingEditProvider implements DocumentFormattingEditProvide // format after arrow let afterWhitespaces = 0; - while (line[afterWhitespaces + beforeWhitespaces + 2] === ' ') { + while (line[afterWhitespaces + beforeWhitespaces + 2] === " ") { afterWhitespaces++; } this.adjustWhitespaces(afterWhitespaces, document, offset, 1, edits); - } else if (trimmed[0] !== '{') { + } else if (trimmed[0] !== "{") { this.formatNewLineAfter(offset, document, openParens, edits, line); } } @@ -181,19 +217,25 @@ export class StpaFormattingEditProvider implements DocumentFormattingEditProvide * @param edits Array to push the created edits to. * @param line The text before which a newline should be inserted. */ - protected formatQuotes(offset: number, document: TextDocument, openParens: number, edits: TextEdit[], line: string): void { + protected formatQuotes( + offset: number, + document: TextDocument, + openParens: number, + edits: TextEdit[], + line: string + ): void { let nextChar = 0; - while (line[nextChar] === ' ') { + while (line[nextChar] === " ") { nextChar++; } - if (line[nextChar] !== '[' && line[nextChar] !== '\r' && line[nextChar] !== ']' && line[nextChar] !== ',') { + if (line[nextChar] !== "[" && line[nextChar] !== "\r" && line[nextChar] !== "]" && line[nextChar] !== ",") { const startPos = document.positionAt(offset); const endPos = document.positionAt(offset + nextChar); const delRange = new Range(startPos, endPos); edits.push(TextEdit.delete(delRange)); const pos: Position = document.positionAt(offset); // IMPORTANT: "\r\n" is specific to windows, linux just uses "\n" - edits.push(TextEdit.insert(pos, '\r\n')); + edits.push(TextEdit.insert(pos, "\r\n")); this.formatIndentation(offset + nextChar, document, openParens, edits, line.substring(nextChar)); } } @@ -206,11 +248,17 @@ export class StpaFormattingEditProvider implements DocumentFormattingEditProvide * @param desired The desired number of whitespaces. * @param edits Array to push the created edits to. */ - protected adjustWhitespaces(whiteSpaces: number, document: TextDocument, offset: number, desired: number, edits: TextEdit[]): void { + protected adjustWhitespaces( + whiteSpaces: number, + document: TextDocument, + offset: number, + desired: number, + edits: TextEdit[] + ): void { if (whiteSpaces < desired) { - let insertWhiteSpaces = ''; + let insertWhiteSpaces = ""; while (whiteSpaces < desired) { - insertWhiteSpaces += ' '; + insertWhiteSpaces += " "; whiteSpaces++; } const pos: Position = document.positionAt(offset); @@ -227,5 +275,4 @@ export class StpaFormattingEditProvider implements DocumentFormattingEditProvide edits.push(TextEdit.delete(delRange)); } } - -} \ No newline at end of file +} diff --git a/extension/src/utils.ts b/extension/src/utils.ts index bd8855ba..55603626 100644 --- a/extension/src/utils.ts +++ b/extension/src/utils.ts @@ -15,10 +15,10 @@ * SPDX-License-Identifier: EPL-2.0 */ -import * as vscode from 'vscode'; +import * as vscode from "vscode"; /** - * Creates a quickpick containing the values "true" and "false". The selected value is set for the + * Creates a quickpick containing the values "true" and "false". The selected value is set for the * configuration option determined by {@code id}. * @param id The id of the configuration option that should be set. */ @@ -27,28 +27,59 @@ export function createQuickPickForWorkspaceOptions(id: string): void { quickPick.items = [{ label: "true" }, { label: "false" }]; quickPick.onDidChangeSelection((selection) => { if (selection[0]?.label === "true") { - vscode.workspace.getConfiguration('pasta').update(id, true); + vscode.workspace.getConfiguration("pasta").update(id, true); } else { - vscode.workspace.getConfiguration('pasta').update(id, false); + vscode.workspace.getConfiguration("pasta").update(id, false); } quickPick.hide(); }); quickPick.onDidHide(() => quickPick.dispose()); quickPick.show(); +} +/** + * Applies text edits to the document. + * @param edits The edits to apply. + * @param uri The uri of the document that should be edited. + */ +export async function applyTextEdits(edits: vscode.TextEdit[], uri: string): Promise { + // create a workspace edit + const workSpaceEdit = new vscode.WorkspaceEdit(); + workSpaceEdit.set(vscode.Uri.parse(uri), edits); + // Apply the edit. Report possible failures. + const edited = await vscode.workspace.applyEdit(workSpaceEdit); + if (!edited) { + console.error("Workspace edit could not be applied!"); + return; + } +} + +/** + * Collects the STPA options of the configuration settings and returns them as a list of their ids and values. + * @param configuration The workspace configuration options. + * @returns A list of the workspace options, whereby a option is represented with an id and its value. + */ +export function collectOptions(configuration: vscode.WorkspaceConfiguration): { id: string; value: any }[] { + const values: { id: string; value: any }[] = []; + values.push({ + id: "checkResponsibilitiesForConstraints", + value: configuration.get("checkResponsibilitiesForConstraints"), + }); + values.push({ id: "checkConstraintsForUCAs", value: configuration.get("checkConstraintsForUCAs") }); + values.push({ id: "checkScenariosForUCAs", value: configuration.get("checkScenariosForUCAs") }); + values.push({ id: "checkSafetyRequirementsForUCAs", value: configuration.get("checkSafetyRequirementsForUCAs") }); + return values; } /** * Creates a file with the given {@code uri} containing the {@code text}. * @param uri The uri of the file to create. * @param text The content of the file. - * @returns */ export async function createFile(uri: string, text: string): Promise { const edit = new vscode.WorkspaceEdit(); - // create the file - edit.createFile(vscode.Uri.parse(uri), {overwrite: true}); + edit.createFile(vscode.Uri.parse(uri), { overwrite: true }); // insert the content const pos = new vscode.Position(0, 0); edit.insert(vscode.Uri.parse(uri), pos, text); @@ -60,7 +91,7 @@ export async function createFile(uri: string, text: string): Promise { return; } // save the edit - const doc = vscode.workspace.textDocuments.find(doc => doc.uri.toString() === vscode.Uri.parse(uri).toString()); + const doc = vscode.workspace.textDocuments.find((doc) => doc.uri.toString() === vscode.Uri.parse(uri).toString()); const saved = await doc?.save(); if (!saved) { console.error(`TextDocument ${doc?.uri} could not be saved!`); @@ -68,3 +99,17 @@ export async function createFile(uri: string, text: string): Promise { } } +/** + * Provides the different UCA types. + */ +export class UCA_TYPE { + static NOT_PROVIDED = "not-provided"; + static PROVIDED = "provided"; + static TOO_EARLY = "too-early"; + static TOO_LATE = "too-late"; + static APPLIED_TOO_LONG = "applied-too-long"; + static STOPPED_TOO_SOON = "stopped-too-soon"; + static WRONG_TIME = "wrong-time"; + static CONTINUOUS = "continuous-problem"; + static UNDEFINED = "undefined"; +} diff --git a/extension/src/wview.ts b/extension/src/wview.ts index 14ee3e38..117eb755 100644 --- a/extension/src/wview.ts +++ b/extension/src/wview.ts @@ -15,13 +15,12 @@ * SPDX-License-Identifier: EPL-2.0 */ -import { isActionMessage, SelectAction } from 'sprotty-protocol'; +import { isActionMessage, SelectAction } from "sprotty-protocol"; import { LspWebviewEndpoint } from "sprotty-vscode/lib/lsp"; -import * as vscode from 'vscode'; -import { SendConfigAction } from './actions'; +import * as vscode from "vscode"; +import { SendConfigAction } from "./actions"; export class StpaLspWebview extends LspWebviewEndpoint { - receiveAction(message: any): Promise { // TODO: for multiple language support here the current language muste be determined if (isActionMessage(message)) { @@ -49,10 +48,13 @@ export class StpaLspWebview extends LspWebviewEndpoint { let uriString = this.diagramIdentifier.uri.toString(); const match = uriString.match(/file:\/\/\/([a-z]):/i); if (match) { - uriString = 'file:///' + match[1] + '%3A' + uriString.substring(match[0].length); + uriString = "file:///" + match[1] + "%3A" + uriString.substring(match[0].length); } // send ID of the first selected element to the language server to highlight the textual definition in the editor - this.languageClient.sendNotification('diagram/selected', { label: action.selectedElementsIDs[0], uri: uriString }); + this.languageClient.sendNotification("diagram/selected", { + label: action.selectedElementsIDs[0], + uri: uriString, + }); } } @@ -60,8 +62,8 @@ export class StpaLspWebview extends LspWebviewEndpoint { * Sends the config option values to the webview */ protected sendConfigValues(): void { - const renderOptions: { id: string, value: any; }[] = []; - const configOptions = vscode.workspace.getConfiguration('pasta'); + const renderOptions: { id: string; value: any }[] = []; + const configOptions = vscode.workspace.getConfiguration("pasta"); renderOptions.push({ id: "colorStyle", value: configOptions.get("colorStyle") }); renderOptions.push({ id: "differentForms", value: configOptions.get("differentForms") }); @@ -69,8 +71,7 @@ export class StpaLspWebview extends LspWebviewEndpoint { } protected updateConfigValues(action: SendConfigAction): void { - const configOptions = vscode.workspace.getConfiguration('pasta'); - action.options.forEach(element => configOptions.update(element.id, element.value)); + const configOptions = vscode.workspace.getConfiguration("pasta"); + action.options.forEach((element) => configOptions.update(element.id, element.value)); } } - diff --git a/extension/webpack.config.js b/extension/webpack.config.js index 2fd99ebd..652892b9 100644 --- a/extension/webpack.config.js +++ b/extension/webpack.config.js @@ -27,7 +27,8 @@ const commonConfig = { use: ['source-map-loader'], } ] - } + }, + ignoreWarnings: [/Failed to parse source map/] } /**@type {import('webpack').Configuration}*/ @@ -88,7 +89,8 @@ const commonWebConfig = { } }, ] - } + }, + ignoreWarnings: [/Failed to parse source map/] }; /**@type {import('webpack').Configuration}*/ diff --git a/yarn.lock b/yarn.lock index ad1bcbda..5f0fa116 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2555,6 +2555,11 @@ dateformat@^3.0.0: resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== +dayjs@^1.11.8: + version "1.11.8" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.8.tgz#4282f139c8c19dd6d0c7bd571e30c2d0ba7698ea" + integrity sha512-LcgxzFoWMEPO7ggRv1Y2N31hUf2R0Vj7fuy/m+Bg1K8rr+KAs1AEy4y9jd5DXe8pbHgX+srkHNS7TH6Q6ZhYeQ== + debug@3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"