From 63588f28673b47403f53eae2cf1a8596c601b306 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Thu, 22 Aug 2024 16:36:00 +0200 Subject: [PATCH 1/9] added a completion item to generate UCA texts for loss scenarios --- .../stpa/scenario-generator.ts | 128 ++++++++++++++++++ .../src-language-server/stpa/stpa-module.ts | 8 ++ .../src-language-server/stpa/stpa.langium | 4 +- 3 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 extension/src-language-server/stpa/scenario-generator.ts diff --git a/extension/src-language-server/stpa/scenario-generator.ts b/extension/src-language-server/stpa/scenario-generator.ts new file mode 100644 index 0000000..87f3404 --- /dev/null +++ b/extension/src-language-server/stpa/scenario-generator.ts @@ -0,0 +1,128 @@ +/* + * KIELER - Kiel Integrated Environment for Layout Eclipse RichClient + * + * http://rtsys.informatik.uni-kiel.de/kieler + * + * Copyright 2024 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 { CompletionAcceptor, CompletionContext, DefaultCompletionProvider, MaybePromise, NextFeature } from "langium"; +import { CompletionItemKind } from "vscode-languageserver"; +import { Context, LossScenario, UCA } from "../generated/ast"; + +/** + * Generates UCA text for loss scenarios by providing an additional completion item. + */ +export class ScenarioCompletionProvider extends DefaultCompletionProvider { + protected enabled: boolean = true; + + /** + * Overrides the default completionFor method to provide an additional completion item for generating UCA text in loss scenarios. + * @param context The completion context. + * @param next The next feature of the current rule to be called. + * @param acceptor The completion acceptor to add the completion items. + */ + protected completionFor( + context: CompletionContext, + next: NextFeature, + acceptor: CompletionAcceptor + ): MaybePromise { + super.completionFor(context, next, acceptor); + if (this.enabled) { + if (context.node?.$type === LossScenario && next.property === "description") { + const generatedText = this.generateScenarioForUCA(context.node as LossScenario); + if (generatedText !== "") { + acceptor({ + label: "Generate UCA Text", + kind: CompletionItemKind.Text, + insertText: this.generateScenarioForUCA(context.node as LossScenario), + detail: "Inserts the UCA text for this scenario.", + sortText: "0", + }); + } + } + } + } + + /** + * Generates the UCA text for the given scenario {@code scenario}. + * If no UCA is reference an empty string is returned. + * @param scenario The scenario node for which the UCA text should be generated. + */ + protected generateScenarioForUCA(scenario: LossScenario): string { + const uca = scenario.uca?.ref; + if (uca) { + if (uca.$type === Context) { + return this.generateScenarioForUCAWithContextTable(uca); + } else { + return this.generateScenarioForUCAWithPlainText(uca); + } + } + return ""; + } + + /** + * Generates a scenario text for a UCA defined with a context table. + * @param uca The UCA for which the scenario should be generated. + * @returns the generated scenario text. + */ + protected generateScenarioForUCAWithContextTable(uca: Context): string { + const rule = uca.$container; + let text = `${rule.system.$refText}`; + switch (rule.type) { + case "not-provided": + text += ` did not provide the control action ${rule.action.ref?.label}`; + break; + case "provided": + text += ` provided the control action ${rule.action.ref?.label}`; + break; + case "stopped-too-soon": + text += ` stopped the control action ${rule.action.ref?.label} too soon`; + break; + case "applied-too-long": + text += ` applied the control action ${rule.action.ref?.label} too long`; + break; + case "too-early": + text += ` provided the control action ${rule.action.ref?.label} too early`; + break; + case "too-late": + text += ` provided the control action ${rule.action.ref?.label} too late`; + break; + case "wrong-time": + text += ` provided the control action ${rule.action.ref?.label} at the wrong time`; + break; + } + + text += `, while`; + uca.assignedValues.forEach((assignedValue, index) => { + if (index > 0) { + text += ", "; + } + if ((index += uca.assignedValues.length - 1)) { + text += ", and"; + } + text += ` ${assignedValue.variable.$refText} was ${assignedValue.value.$refText}`; + }); + text += "."; + + return text; + } + + /** + * Generates a scenario text for a UCA defined with plain text. + * @param uca The UCA for which the scenario should be generated. + * @returns the generated scenario text. + */ + protected generateScenarioForUCAWithPlainText(uca: UCA): string { + return uca.description; + } +} diff --git a/extension/src-language-server/stpa/stpa-module.ts b/extension/src-language-server/stpa/stpa-module.ts index fe524fe..fd87221 100644 --- a/extension/src-language-server/stpa/stpa-module.ts +++ b/extension/src-language-server/stpa/stpa-module.ts @@ -26,6 +26,7 @@ import { ContextTableProvider } from "./contextTable/context-dataProvider"; import { StpaDiagramGenerator } from "./diagram/diagram-generator"; import { StpaLayoutConfigurator } from "./diagram/layout-config"; import { StpaSynthesisOptions } from "./diagram/stpa-synthesis-options"; +import { ScenarioCompletionProvider } from "./scenario-generator"; import { StpaScopeProvider } from "./stpa-scopeProvider"; import { StpaValidationRegistry, StpaValidator } from "./stpa-validator"; @@ -33,6 +34,9 @@ import { StpaValidationRegistry, StpaValidator } from "./stpa-validator"; * Declaration of custom services - add your own service classes here. */ export type StpaAddedServices = { + lsp: { + ScenarioCompletionProvider: ScenarioCompletionProvider; + }; references: { StpaScopeProvider: StpaScopeProvider; }; @@ -79,6 +83,10 @@ export const STPAModule: Module new ScenarioCompletionProvider(services), + ScenarioCompletionProvider: services => new ScenarioCompletionProvider(services), + }, references: { ScopeProvider: services => new StpaScopeProvider(services), StpaScopeProvider: services => new StpaScopeProvider(services), diff --git a/extension/src-language-server/stpa/stpa.langium b/extension/src-language-server/stpa/stpa.langium index 52015d1..abbab18 100644 --- a/extension/src-language-server/stpa/stpa.langium +++ b/extension/src-language-server/stpa/stpa.langium @@ -128,11 +128,11 @@ AssignedValue: variable=[Variable] '=' value=[VariableValue:QualifiedName]; ControllerConstraint: - name=ID description=STRING '['refs+=[UCA] (',' refs+=[UCA])*']'; + name=ID description=STRING '['(refs+=[UCA] | refs+=[Context]) (',' (refs+=[UCA] | refs+=[Context]))*']'; LossScenario: (name=ID ('<' factor=CausalFactor '>')? description=STRING list=HazardList) | - (name=ID ('<' factor=CausalFactor '>')? 'for' uca=[UCA] description=STRING (list=HazardList)?); + (name=ID ('<' factor=CausalFactor '>')? 'for' (uca=[UCA] | uca=[Context]) description=STRING (list=HazardList)?); CausalFactor returns string: 'controlAction' | 'inadequateOperation' | 'delayedOperation' | 'componentFailure' | 'changesOverTime' | From 1cea08a315bfcc98243576924a893f9286ae9f83 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Fri, 23 Aug 2024 10:50:31 +0200 Subject: [PATCH 2/9] ID enforcement: betetr determination of modified aspect --- .../src-language-server/stpa/ID-enforcer.ts | 195 ++++++++++-------- ...tor.ts => scenario-completion-provider.ts} | 0 .../src-language-server/stpa/stpa-module.ts | 2 +- 3 files changed, 106 insertions(+), 91 deletions(-) rename extension/src-language-server/stpa/{scenario-generator.ts => scenario-completion-provider.ts} (100%) diff --git a/extension/src-language-server/stpa/ID-enforcer.ts b/extension/src-language-server/stpa/ID-enforcer.ts index a9766c7..6ddd03f 100644 --- a/extension/src-language-server/stpa/ID-enforcer.ts +++ b/extension/src-language-server/stpa/ID-enforcer.ts @@ -15,10 +15,26 @@ * SPDX-License-Identifier: EPL-2.0 */ -import { isCompositeCstNode, LangiumDocument } from "langium"; +import { CstNode, isCompositeCstNode, LangiumDocument } from "langium"; import { TextDocumentContentChangeEvent } from "vscode"; import { Range, RenameParams, TextEdit } from "vscode-languageserver"; -import { Hazard, isHazard, isRule, isSystemConstraint, LossScenario, Model, SystemConstraint } from "../generated/ast"; +import { + ActionUCAs, + ControllerConstraint, + DCARule, + Hazard, + isHazard, + isModel, + isRule, + isSystemConstraint, + Loss, + LossScenario, + Model, + Rule, + SafetyConstraint, + SystemConstraint, + SystemResponsibilities +} from "../generated/ast"; import { StpaServices } from "./stpa-module"; import { collectElementsWithSubComps, elementWithName, elementWithRefs } from "./utils"; @@ -335,103 +351,102 @@ export class IDEnforcer { let prefix = ""; let ruleElements: elementWithName[] = []; - // offsets of the different aspects to determine the aspect for the given offset - const subtractOffset = 5; - const dcaOffset = - model.allDCAs.length !== 0 && model.allDCAs[0].$cstNode?.offset - ? model.allDCAs[0].$cstNode.offset - subtractOffset - : Number.MAX_VALUE; - const safetyConsOffset = - model.safetyCons.length !== 0 && model.safetyCons[0].$cstNode?.offset - ? model.safetyCons[0].$cstNode.offset - subtractOffset - : dcaOffset; - const scenarioOffset = - model.scenarios.length !== 0 && model.scenarios[0].$cstNode?.offset - ? model.scenarios[0].$cstNode.offset - subtractOffset - : safetyConsOffset; - const ucaConstraintOffset = - model.controllerConstraints.length !== 0 && model.controllerConstraints[0].$cstNode?.offset - ? model.controllerConstraints[0].$cstNode.offset - subtractOffset - : scenarioOffset; - const ucaOffset = - model.rules.length !== 0 && model.rules[0].$cstNode?.offset - ? model.rules[0].$cstNode.offset - subtractOffset - : model.allUCAs.length !== 0 && model.allUCAs[0].$cstNode?.offset - ? model.allUCAs[0].$cstNode.offset - subtractOffset - : ucaConstraintOffset; - const responsibilitiesOffset = - model.responsibilities.length !== 0 && model.responsibilities[0].$cstNode?.offset - ? model.responsibilities[0].$cstNode.offset - subtractOffset - : ucaOffset; - const constraintOffset = - model.systemLevelConstraints.length !== 0 && model.systemLevelConstraints[0].$cstNode?.offset - ? model.systemLevelConstraints[0].$cstNode.offset - subtractOffset - : responsibilitiesOffset; - const hazardOffset = - model.hazards.length !== 0 && model.hazards[0].$cstNode?.offset - ? model.hazards[0].$cstNode.offset - subtractOffset - : constraintOffset; + let node = this.findLeafNodeAtOffset(this.currentDocument.parseResult.value.$cstNode!, offset); + console.log(node); + while (node && !isModel(node?.element) && !isModel(node?.element.$container)) { + node = node?.parent; + } // determine the aspect for the given offset - if ( - !hazardOffset || - !constraintOffset || - !responsibilitiesOffset || - !ucaOffset || - !ucaConstraintOffset || - !scenarioOffset || - !safetyConsOffset || - !dcaOffset - ) { - console.log("Offset could not be determined for all aspects."); + if (!node) { return undefined; - } else if (offset < hazardOffset) { - elements = model.losses; - prefix = IDPrefix.Loss; - } else if (offset < constraintOffset && offset > hazardOffset) { - // sub-components must be considered when determining the affected elements - const modified = this.findAffectedSubComponents(model.hazards, IDPrefix.Hazard, offset); - elements = modified.elements; - prefix = modified.prefix; - } else if (offset < responsibilitiesOffset && offset > constraintOffset) { - // sub-components must be considered when determining the affected elements - const modified = this.findAffectedSubComponents( - model.systemLevelConstraints, - IDPrefix.SystemConstraint, - offset - ); - elements = modified.elements; - prefix = modified.prefix; - } else if (offset < ucaOffset && offset > responsibilitiesOffset) { - elements = model.responsibilities.flatMap(resp => resp.responsiblitiesForOneSystem); - prefix = IDPrefix.Responsibility; - } else if (offset < ucaConstraintOffset && offset > ucaOffset) { - elements = model.allUCAs.flatMap(sysUCA => - sysUCA.notProvidingUcas.concat(sysUCA.providingUcas, sysUCA.wrongTimingUcas, sysUCA.continousUcas) - ); - elements = elements.concat(model.rules.flatMap(rule => rule.contexts)); - prefix = IDPrefix.UCA; - // rules must be handled separately since they are mixed with the UCAs - ruleElements = model.rules; - } else if (offset < scenarioOffset && offset > ucaConstraintOffset) { - elements = model.controllerConstraints; - prefix = IDPrefix.ControllerConstraint; - } else if (offset < safetyConsOffset && offset > scenarioOffset) { - elements = model.scenarios; - prefix = IDPrefix.LossScenario; - } else if (offset < dcaOffset && offset > safetyConsOffset) { - elements = model.safetyCons; - prefix = IDPrefix.SafetyRequirement; } else { - elements = model.allDCAs.flatMap(dca => dca.contexts); - prefix = IDPrefix.DCA; - // rules must be handled separately since they are mixed with the DCAs - ruleElements = model.allDCAs; + switch (node.element.$type) { + case Loss: + elements = model.losses; + prefix = IDPrefix.Loss; + break; + case Hazard: + // sub-components must be considered when determining the affected elements + const modifiedHazard = this.findAffectedSubComponents(model.hazards, IDPrefix.Hazard, offset); + elements = modifiedHazard.elements; + prefix = modifiedHazard.prefix; + break; + case SystemConstraint: + // sub-components must be considered when determining the affected elements + const modifiedSystemConstraint = this.findAffectedSubComponents( + model.systemLevelConstraints, + IDPrefix.SystemConstraint, + offset + ); + elements = modifiedSystemConstraint.elements; + prefix = modifiedSystemConstraint.prefix; + break; + case SystemResponsibilities: + elements = model.responsibilities.flatMap(resp => resp.responsiblitiesForOneSystem); + prefix = IDPrefix.Responsibility; + break; + case ActionUCAs: + case Rule: + elements = model.allUCAs.flatMap(sysUCA => + sysUCA.notProvidingUcas.concat( + sysUCA.providingUcas, + sysUCA.wrongTimingUcas, + sysUCA.continousUcas + ) + ); + elements = elements.concat(model.rules.flatMap(rule => rule.contexts)); + prefix = IDPrefix.UCA; + // rules must be handled separately since they are mixed with the UCAs + ruleElements = model.rules; + break; + case ControllerConstraint: + elements = model.controllerConstraints; + prefix = IDPrefix.ControllerConstraint; + break; + case LossScenario: + elements = model.scenarios; + prefix = IDPrefix.LossScenario; + break; + case SafetyConstraint: + elements = model.safetyCons; + prefix = IDPrefix.SafetyRequirement; + break; + case DCARule: + elements = model.allDCAs.flatMap(dca => dca.contexts); + prefix = IDPrefix.DCA; + // rules must be handled separately since they are mixed with the DCAs + ruleElements = model.allDCAs; + break; + } } return { elements, prefix, ruleElements }; } + // TODO: modify this method to find also a node when the offset is not on a leaf node!! + protected findLeafNodeAtOffset(node: CstNode, offset: number): CstNode | undefined { + if (isCompositeCstNode(node)) { + let firstChild = 0; + let lastChild = node.children.length - 1; + while (firstChild < lastChild) { + const middleChild = Math.floor((firstChild + lastChild) / 2); + const n = node.children[middleChild]; + if (n.offset > offset) { + lastChild = middleChild - 1; + } else if (n.end <= offset) { + firstChild = middleChild + 1; + } else { + return this.findLeafNodeAtOffset(n, offset); + } + } + if (firstChild === lastChild) { + return this.findLeafNodeAtOffset(node.children[firstChild], offset); + } + } + return node; + } + /** * Determines the elements affected by the given {@code offfset}. * @param originalElements The top-level elements which may be affected by the offset. diff --git a/extension/src-language-server/stpa/scenario-generator.ts b/extension/src-language-server/stpa/scenario-completion-provider.ts similarity index 100% rename from extension/src-language-server/stpa/scenario-generator.ts rename to extension/src-language-server/stpa/scenario-completion-provider.ts diff --git a/extension/src-language-server/stpa/stpa-module.ts b/extension/src-language-server/stpa/stpa-module.ts index fd87221..5bab6e1 100644 --- a/extension/src-language-server/stpa/stpa-module.ts +++ b/extension/src-language-server/stpa/stpa-module.ts @@ -26,7 +26,7 @@ import { ContextTableProvider } from "./contextTable/context-dataProvider"; import { StpaDiagramGenerator } from "./diagram/diagram-generator"; import { StpaLayoutConfigurator } from "./diagram/layout-config"; import { StpaSynthesisOptions } from "./diagram/stpa-synthesis-options"; -import { ScenarioCompletionProvider } from "./scenario-generator"; +import { ScenarioCompletionProvider } from "./scenario-completion-provider"; import { StpaScopeProvider } from "./stpa-scopeProvider"; import { StpaValidationRegistry, StpaValidator } from "./stpa-validator"; From 719b4c2008ff470b34949dc09e9c05b81a58b76f Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Fri, 23 Aug 2024 11:16:07 +0200 Subject: [PATCH 3/9] ID enforcement: fixed bug (index off by one) --- extension/src-language-server/stpa/ID-enforcer.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/extension/src-language-server/stpa/ID-enforcer.ts b/extension/src-language-server/stpa/ID-enforcer.ts index 6ddd03f..1324c74 100644 --- a/extension/src-language-server/stpa/ID-enforcer.ts +++ b/extension/src-language-server/stpa/ID-enforcer.ts @@ -194,7 +194,7 @@ export class IDEnforcer { let edits: TextEdit[] = []; // renaming is only needed, when elements not have the correct ID yet if (elements[elements.length - 1].name !== prefix + elements.length) { - const modifiedElement = elements[index - 1]; + const modifiedElement = elements[index]; if (decrease) { // IDs of the elements are decreased so we must start with the lowest ID @@ -424,7 +424,12 @@ export class IDEnforcer { return { elements, prefix, ruleElements }; } - // TODO: modify this method to find also a node when the offset is not on a leaf node!! + /** + * Changes the method from sprotty to return the closest CstNode to the given offset. + * @param node The node to start the search from. + * @param offset The offset for which the closest node should be determined. + * @returns the closest node to the given offset. + */ protected findLeafNodeAtOffset(node: CstNode, offset: number): CstNode | undefined { if (isCompositeCstNode(node)) { let firstChild = 0; From 20ad989239b68f3dc060607c5d6abd9930967d51 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Mon, 26 Aug 2024 15:42:39 +0200 Subject: [PATCH 4/9] automatic text generation for plain text UCAs --- .../src-language-server/stpa/ID-enforcer.ts | 1 - .../stpa/scenario-completion-provider.ts | 115 +++++++++++++++--- .../src-language-server/stpa/stpa-module.ts | 8 +- 3 files changed, 105 insertions(+), 19 deletions(-) diff --git a/extension/src-language-server/stpa/ID-enforcer.ts b/extension/src-language-server/stpa/ID-enforcer.ts index a4313f9..ca2ee70 100644 --- a/extension/src-language-server/stpa/ID-enforcer.ts +++ b/extension/src-language-server/stpa/ID-enforcer.ts @@ -372,7 +372,6 @@ export class IDEnforcer { let ruleElements: elementWithName[] = []; let node = this.findLeafNodeAtOffset(this.currentDocument.parseResult.value.$cstNode!, offset); - console.log(node); while (node && !isModel(node?.element) && !isModel(node?.element.$container)) { node = node?.parent; } diff --git a/extension/src-language-server/stpa/scenario-completion-provider.ts b/extension/src-language-server/stpa/scenario-completion-provider.ts index 87f3404..fd5cbc2 100644 --- a/extension/src-language-server/stpa/scenario-completion-provider.ts +++ b/extension/src-language-server/stpa/scenario-completion-provider.ts @@ -15,14 +15,21 @@ * SPDX-License-Identifier: EPL-2.0 */ -import { CompletionAcceptor, CompletionContext, DefaultCompletionProvider, MaybePromise, NextFeature } from "langium"; +import { + CompletionAcceptor, + CompletionContext, + CompletionValueItem, + DefaultCompletionProvider, + MaybePromise, + NextFeature, +} from "langium"; import { CompletionItemKind } from "vscode-languageserver"; -import { Context, LossScenario, UCA } from "../generated/ast"; +import { Context, isVerticalEdge, LossScenario, UCA } from "../generated/ast"; /** * Generates UCA text for loss scenarios by providing an additional completion item. */ -export class ScenarioCompletionProvider extends DefaultCompletionProvider { +export class STPACompletionProvider extends DefaultCompletionProvider { protected enabled: boolean = true; /** @@ -38,17 +45,97 @@ export class ScenarioCompletionProvider extends DefaultCompletionProvider { ): MaybePromise { super.completionFor(context, next, acceptor); if (this.enabled) { - if (context.node?.$type === LossScenario && next.property === "description") { - const generatedText = this.generateScenarioForUCA(context.node as LossScenario); - if (generatedText !== "") { - acceptor({ - label: "Generate UCA Text", - kind: CompletionItemKind.Text, - insertText: this.generateScenarioForUCA(context.node as LossScenario), - detail: "Inserts the UCA text for this scenario.", - sortText: "0", - }); - } + this.completionForScenario(context, next, acceptor); + this.completionForUCA(context, next, acceptor); + } + } + + protected completionForUCA(context: CompletionContext, next: NextFeature, acceptor: CompletionAcceptor): void { + if (context.node?.$type === UCA && next.property === "description") { + const generatedItems = this.generateTextForUCAWithPlainText( + context.node as UCA, + context.node.$containerProperty + ); + + if (generatedItems.length > 0) { + generatedItems.forEach(item => acceptor(item)); + } + } + } + + protected generateTextForUCAWithPlainText(uca: UCA, property?: string): CompletionValueItem[] { + const actionUca = uca.$container; + let controlAction = `the control action '${actionUca.action.ref?.label}'`; + const parent = actionUca.action.ref?.$container; + if (isVerticalEdge(parent)) { + controlAction += ` to ${parent.target.$refText}`; + } + switch (property) { + case "notProvidingUcas": + const notProvidedItem = { + label: "Generate not provided UCA Text", + kind: CompletionItemKind.Text, + insertText: `${actionUca.system.$refText} did not provide ${controlAction}, `, + detail: "Inserts the starting text for this UCA.", + sortText: "0", + }; + return [notProvidedItem]; + case "providingUcas": + const providedItem = { + label: "Generate provided UCA Text", + kind: CompletionItemKind.Text, + insertText: `${actionUca.system.$refText} provided ${controlAction}, `, + detail: "Inserts the starting text for this UCA.", + sortText: "0", + }; + return [providedItem]; + case "wrongTimingUcas": + const tooEarlyItem = { + label: "Generate too-early UCA Text", + kind: CompletionItemKind.Text, + insertText: `${actionUca.system.$refText} provided ${controlAction} before`, + detail: "Inserts the starting text for this UCA.", + sortText: "0", + }; + const tooLateItem = { + label: "Generate too-late UCA Text", + kind: CompletionItemKind.Text, + insertText: `${actionUca.system.$refText} provided ${controlAction} after`, + detail: "Inserts the starting text for this UCA.", + sortText: "1", + }; + return [tooEarlyItem, tooLateItem]; + case "continousUcas": + const stoppedTooSoonItem = { + label: "Generate stopped-too-soon UCA Text", + kind: CompletionItemKind.Text, + insertText: `${actionUca.system.$refText} stopped ${controlAction} before`, + detail: "Inserts the starting text for this UCA.", + sortText: "0", + }; + const appliedTooLongItem = { + label: "Generate applied-too-long UCA Text", + kind: CompletionItemKind.Text, + insertText: `${actionUca.system.$refText} still applied ${controlAction} after`, + detail: "Inserts the starting text for this UCA.", + sortText: "1", + }; + return [stoppedTooSoonItem, appliedTooLongItem]; + } + return []; + } + + protected completionForScenario(context: CompletionContext, next: NextFeature, acceptor: CompletionAcceptor): void { + if (context.node?.$type === LossScenario && next.property === "description") { + const generatedText = this.generateScenarioForUCA(context.node as LossScenario); + if (generatedText !== "") { + acceptor({ + label: "Generate UCA Text", + kind: CompletionItemKind.Text, + insertText: generatedText, + detail: "Inserts the UCA text for this scenario.", + sortText: "0", + }); } } } diff --git a/extension/src-language-server/stpa/stpa-module.ts b/extension/src-language-server/stpa/stpa-module.ts index 5bab6e1..52e5b56 100644 --- a/extension/src-language-server/stpa/stpa-module.ts +++ b/extension/src-language-server/stpa/stpa-module.ts @@ -26,7 +26,7 @@ import { ContextTableProvider } from "./contextTable/context-dataProvider"; import { StpaDiagramGenerator } from "./diagram/diagram-generator"; import { StpaLayoutConfigurator } from "./diagram/layout-config"; import { StpaSynthesisOptions } from "./diagram/stpa-synthesis-options"; -import { ScenarioCompletionProvider } from "./scenario-completion-provider"; +import { STPACompletionProvider } from "./scenario-completion-provider"; import { StpaScopeProvider } from "./stpa-scopeProvider"; import { StpaValidationRegistry, StpaValidator } from "./stpa-validator"; @@ -35,7 +35,7 @@ import { StpaValidationRegistry, StpaValidator } from "./stpa-validator"; */ export type StpaAddedServices = { lsp: { - ScenarioCompletionProvider: ScenarioCompletionProvider; + ScenarioCompletionProvider: STPACompletionProvider; }; references: { StpaScopeProvider: StpaScopeProvider; @@ -84,8 +84,8 @@ export const STPAModule: Module new ScenarioCompletionProvider(services), - ScenarioCompletionProvider: services => new ScenarioCompletionProvider(services), + CompletionProvider: services => new STPACompletionProvider(services), + ScenarioCompletionProvider: services => new STPACompletionProvider(services), }, references: { ScopeProvider: services => new StpaScopeProvider(services), From a392d6032f8642edcb927dc314472d4de00a43e3 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Tue, 27 Aug 2024 13:12:54 +0200 Subject: [PATCH 5/9] added completion items for Rule definiton --- .../stpa/scenario-completion-provider.ts | 155 +++++++++++++++++- 1 file changed, 154 insertions(+), 1 deletion(-) diff --git a/extension/src-language-server/stpa/scenario-completion-provider.ts b/extension/src-language-server/stpa/scenario-completion-provider.ts index fd5cbc2..f77deec 100644 --- a/extension/src-language-server/stpa/scenario-completion-provider.ts +++ b/extension/src-language-server/stpa/scenario-completion-provider.ts @@ -24,7 +24,7 @@ import { NextFeature, } from "langium"; import { CompletionItemKind } from "vscode-languageserver"; -import { Context, isVerticalEdge, LossScenario, UCA } from "../generated/ast"; +import { Context, isModel, isVerticalEdge, LossScenario, Model, Node, Rule, UCA, VerticalEdge } from "../generated/ast"; /** * Generates UCA text for loss scenarios by providing an additional completion item. @@ -47,9 +47,150 @@ export class STPACompletionProvider extends DefaultCompletionProvider { if (this.enabled) { this.completionForScenario(context, next, acceptor); this.completionForUCA(context, next, acceptor); + this.completionForUCARule(context, next, acceptor); } } + /** + * Adds completion items for generating rules for UCAs if the current context is a rule. + * @param context The completion context. + * @param next The next feature of the current rule to be called. + * @param acceptor The completion acceptor to add the completion items. + */ + protected completionForUCARule(context: CompletionContext, next: NextFeature, acceptor: CompletionAcceptor): void { + if ((context.node?.$type === Rule || next.type === Rule) && next.property === "name") { + const templateRuleItem = this.generateTemplateRuleItem(); + acceptor(templateRuleItem); + const model = context.node?.$type === Model ? context.node : context.node?.$container; + if (isModel(model)) { + const controlActions = this.collectControlActions(model); + const rulesForEverythingItem = this.generateRulesForEverythingItem(controlActions); + acceptor(rulesForEverythingItem); + const ruleForSpecificControlActionItems = + this.generateRuleForSpecificControlActionItems(controlActions); + ruleForSpecificControlActionItems.forEach(item => acceptor(item)); + } + } + } + + /** + * Determines all control actions in the given model. + * @param model The model for which the control actions should be collected. + */ + protected collectControlActions(model: Model): VerticalEdge[] { + const actions: VerticalEdge[] = []; + model.controlStructure?.nodes.forEach(node => { + actions.push(...this.getControlActions(node)); + }); + return actions; + } + + /** + * Gets all control actions for the given node and its children. + * @param node The node for which the control actions should be collected. + * @returns the control actions for the given node and its children. + */ + protected getControlActions(node: Node): VerticalEdge[] { + const actions = node.actions; + node.children.forEach(child => { + actions.push(...this.getControlActions(child)); + }); + return actions; + } + + /** + * Creates for each control action a completion item for generating a rule for this control action. + * @param controlActions The control actions for which the rules should be generated. + */ + protected generateRuleForSpecificControlActionItems(controlActions: VerticalEdge[]): CompletionValueItem[] { + const items: CompletionValueItem[] = []; + let counter = 3; + for (const controlAction of controlActions) { + for (const action of controlAction.comms) { + const item: CompletionValueItem = { + label: `Generate a rule for ${controlAction.$container.name}.${action.name}`, + kind: CompletionItemKind.Snippet, + insertText: `RL { + controlAction: ${controlAction.$container.name}.${action.name} + type: + contexts: { + } +}`, + detail: `Inserts a rule for ${controlAction.$container.name}.${action.name} with missing content.`, + sortText: `${counter}`, + }; + items.push(item); + counter++; + } + } + return items; + } + + /** + * Creates a completion item for generating a rule for every possible control action and type combination. + * @param controlActions The control actions for which the rules should be generated. + * @returns a completion item for generating a rule for every possible control action and type combination. + */ + protected generateRulesForEverythingItem(controlActions: VerticalEdge[]): CompletionValueItem { + let insertText = ``; + let counter = 0; + for (const controlAction of controlActions) { + for (const action of controlAction.comms) { + for (const type of [ + "not-provided", + "provided", + "too-early", + "too-late", + "wrong-time", + "applied-too-long", + "stopped-too-soon", + ]) { + insertText += `RL${counter} { + controlAction: ${controlAction.$container.name}.${action.name} + type: ${type} + contexts: { + } +} +`; + counter++; + } + } + } + const item: CompletionValueItem = { + label: `Generate rules for every control action and type combination`, + kind: CompletionItemKind.Snippet, + insertText: insertText, + detail: "Inserts for every control action rules for every UCA type.", + sortText: "1", + }; + return item; + } + + /** + * Creates a completion item for generating a template rule. + * @returns a completion item for generating a template rule. + */ + protected generateTemplateRuleItem(): CompletionValueItem { + return { + label: "Generate template Rule", + kind: CompletionItemKind.Snippet, + insertText: `RL { + controlAction: + type: + contexts: { + } +}`, + detail: "Inserts a rule with missing content.", + sortText: "0", + }; + } + + /** + * Adds completion items for generating UCA text for a UCA if the current context is a UCA. + * @param context The completion context. + * @param next The next feature of the current rule to be called. + * @param acceptor The completion acceptor to add the completion items. + */ protected completionForUCA(context: CompletionContext, next: NextFeature, acceptor: CompletionAcceptor): void { if (context.node?.$type === UCA && next.property === "description") { const generatedItems = this.generateTextForUCAWithPlainText( @@ -63,6 +204,12 @@ export class STPACompletionProvider extends DefaultCompletionProvider { } } + /** + * Generates completion items for the given UCA {@code uca}. + * @param uca The UCA for which the completion items should be generated. + * @param property The property in which the UCA is contained. Should be one of "notProvidingUcas", "providingUcas", "wrongTimingUcas", or "continousUcas". + * @returns completion items for the given UCA. + */ protected generateTextForUCAWithPlainText(uca: UCA, property?: string): CompletionValueItem[] { const actionUca = uca.$container; let controlAction = `the control action '${actionUca.action.ref?.label}'`; @@ -125,6 +272,12 @@ export class STPACompletionProvider extends DefaultCompletionProvider { return []; } + /** + * Adds a completion item for generating UCA text for a scenario if the current context is a loss scenario. + * @param context The completion context. + * @param next The next feature of the current rule to be called. + * @param acceptor The completion acceptor to add the completion items. + */ protected completionForScenario(context: CompletionContext, next: NextFeature, acceptor: CompletionAcceptor): void { if (context.node?.$type === LossScenario && next.property === "description") { const generatedText = this.generateScenarioForUCA(context.node as LossScenario); From 4360a62cf33f5de2f63fe0f5a3f096babc591061 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Tue, 27 Aug 2024 14:06:25 +0200 Subject: [PATCH 6/9] added completion item for system component definition --- .../stpa/scenario-completion-provider.ts | 46 ++++++++++++++++--- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/extension/src-language-server/stpa/scenario-completion-provider.ts b/extension/src-language-server/stpa/scenario-completion-provider.ts index f77deec..b582dc4 100644 --- a/extension/src-language-server/stpa/scenario-completion-provider.ts +++ b/extension/src-language-server/stpa/scenario-completion-provider.ts @@ -45,12 +45,43 @@ export class STPACompletionProvider extends DefaultCompletionProvider { ): MaybePromise { super.completionFor(context, next, acceptor); if (this.enabled) { + this.completionForSystemComponent(next, acceptor); this.completionForScenario(context, next, acceptor); this.completionForUCA(context, next, acceptor); this.completionForUCARule(context, next, acceptor); } } + /** + * Adds a completion item for generating a system component if the current context is a system component. + * @param next The next feature of the current rule to be called. + * @param acceptor The completion acceptor to add the completion items. + */ + protected completionForSystemComponent( + next: NextFeature, + acceptor: CompletionAcceptor + ): void { + if (next.type === Node && next.property === "name") { + const generatedText = `Comp { + hierarchyLevel 0 + label "Component" + processModel { + } + controlActions { + } + feedback { + } +}`; + acceptor({ + label: "Generate System Component", + kind: CompletionItemKind.Text, + insertText: generatedText, + detail: "Inserts a system component.", + sortText: "0", + }); + } + } + /** * Adds completion items for generating rules for UCAs if the current context is a rule. * @param context The completion context. @@ -212,17 +243,18 @@ export class STPACompletionProvider extends DefaultCompletionProvider { */ protected generateTextForUCAWithPlainText(uca: UCA, property?: string): CompletionValueItem[] { const actionUca = uca.$container; + const system = actionUca.system.ref?.label ?? actionUca.system.$refText; let controlAction = `the control action '${actionUca.action.ref?.label}'`; const parent = actionUca.action.ref?.$container; if (isVerticalEdge(parent)) { - controlAction += ` to ${parent.target.$refText}`; + controlAction += ` to ${parent.target.ref?.label ?? parent.target.$refText}`; } switch (property) { case "notProvidingUcas": const notProvidedItem = { label: "Generate not provided UCA Text", kind: CompletionItemKind.Text, - insertText: `${actionUca.system.$refText} did not provide ${controlAction}, `, + insertText: `${system} did not provide ${controlAction}, `, detail: "Inserts the starting text for this UCA.", sortText: "0", }; @@ -231,7 +263,7 @@ export class STPACompletionProvider extends DefaultCompletionProvider { const providedItem = { label: "Generate provided UCA Text", kind: CompletionItemKind.Text, - insertText: `${actionUca.system.$refText} provided ${controlAction}, `, + insertText: `${system} provided ${controlAction}, `, detail: "Inserts the starting text for this UCA.", sortText: "0", }; @@ -240,14 +272,14 @@ export class STPACompletionProvider extends DefaultCompletionProvider { const tooEarlyItem = { label: "Generate too-early UCA Text", kind: CompletionItemKind.Text, - insertText: `${actionUca.system.$refText} provided ${controlAction} before`, + insertText: `${system} provided ${controlAction} before`, detail: "Inserts the starting text for this UCA.", sortText: "0", }; const tooLateItem = { label: "Generate too-late UCA Text", kind: CompletionItemKind.Text, - insertText: `${actionUca.system.$refText} provided ${controlAction} after`, + insertText: `${system} provided ${controlAction} after`, detail: "Inserts the starting text for this UCA.", sortText: "1", }; @@ -256,14 +288,14 @@ export class STPACompletionProvider extends DefaultCompletionProvider { const stoppedTooSoonItem = { label: "Generate stopped-too-soon UCA Text", kind: CompletionItemKind.Text, - insertText: `${actionUca.system.$refText} stopped ${controlAction} before`, + insertText: `${system} stopped ${controlAction} before`, detail: "Inserts the starting text for this UCA.", sortText: "0", }; const appliedTooLongItem = { label: "Generate applied-too-long UCA Text", kind: CompletionItemKind.Text, - insertText: `${actionUca.system.$refText} still applied ${controlAction} after`, + insertText: `${system} still applied ${controlAction} after`, detail: "Inserts the starting text for this UCA.", sortText: "1", }; From 8eda46dc12a6b9af88d996cb0c0ec49b15de132f Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Mon, 2 Sep 2024 14:48:47 +0200 Subject: [PATCH 7/9] completion item to create basic scenarios --- ...rovider.ts => stpa-completion-provider.ts} | 146 ++++++++++++++---- .../src-language-server/stpa/stpa-module.ts | 2 +- extension/src/language-extension.ts | 5 +- 3 files changed, 121 insertions(+), 32 deletions(-) rename extension/src-language-server/stpa/{scenario-completion-provider.ts => stpa-completion-provider.ts} (69%) diff --git a/extension/src-language-server/stpa/scenario-completion-provider.ts b/extension/src-language-server/stpa/stpa-completion-provider.ts similarity index 69% rename from extension/src-language-server/stpa/scenario-completion-provider.ts rename to extension/src-language-server/stpa/stpa-completion-provider.ts index b582dc4..ffca94a 100644 --- a/extension/src-language-server/stpa/scenario-completion-provider.ts +++ b/extension/src-language-server/stpa/stpa-completion-provider.ts @@ -57,10 +57,7 @@ export class STPACompletionProvider extends DefaultCompletionProvider { * @param next The next feature of the current rule to be called. * @param acceptor The completion acceptor to add the completion items. */ - protected completionForSystemComponent( - next: NextFeature, - acceptor: CompletionAcceptor - ): void { + protected completionForSystemComponent(next: NextFeature, acceptor: CompletionAcceptor): void { if (next.type === Node && next.property === "name") { const generatedText = `Comp { hierarchyLevel 0 @@ -305,26 +302,102 @@ export class STPACompletionProvider extends DefaultCompletionProvider { } /** - * Adds a completion item for generating UCA text for a scenario if the current context is a loss scenario. + * Adds a completion item for generating UCA text for a scenario and completion items to generate basic scenarios if the current context is a loss scenario. * @param context The completion context. * @param next The next feature of the current rule to be called. * @param acceptor The completion acceptor to add the completion items. */ protected completionForScenario(context: CompletionContext, next: NextFeature, acceptor: CompletionAcceptor): void { - if (context.node?.$type === LossScenario && next.property === "description") { - const generatedText = this.generateScenarioForUCA(context.node as LossScenario); - if (generatedText !== "") { - acceptor({ - label: "Generate UCA Text", - kind: CompletionItemKind.Text, - insertText: generatedText, - detail: "Inserts the UCA text for this scenario.", - sortText: "0", - }); + if (context.node?.$type === LossScenario) { + if (next.property === "description") { + const generatedText = this.generateScenarioForUCA(context.node as LossScenario); + if (generatedText !== "") { + acceptor({ + label: "Generate UCA Text", + kind: CompletionItemKind.Text, + insertText: generatedText, + detail: "Inserts the UCA text for this scenario.", + sortText: "0", + }); + } + } + if (next.type === LossScenario && next.property === "name") { + const generatedBasicScenariosText = this.generateBasicScenarios(context.node.$container as Model); + if (generatedBasicScenariosText !== "") { + acceptor({ + label: "Generate Basic Scenarios", + kind: CompletionItemKind.Snippet, + insertText: generatedBasicScenariosText, + detail: "Creates basic scenarios for all UCAs.", + sortText: "0", + }); + } } } } + /** + * Creates text for basic scenarios for all UCAs in the given {@code model}. + * @param model The model for which the basic scenarios should be generated. + * @returns the generated basic scenarios as text. + */ + protected generateBasicScenarios(model: Model): string { + let text = ``; + model.rules.forEach(rule => { + const system = rule.system.ref?.label ?? rule.system.$refText; + const controlAction = `the control action '${rule.action.ref?.label}'`; + rule.contexts.forEach(context => { + // add scenario for actuator/controlled process failure + let scenario = `${system}`; + const contextText = this.createContextText(context); + switch (rule.type) { + case "not-provided": + scenario += ` provided ${controlAction}, while ${contextText}, but it is not executed.`; + break; + case "provided": + scenario += ` not provided ${controlAction}, while ${contextText}, but it is executed.`; + break; + case "too-late": + scenario += ` provided ${controlAction} in time, while ${contextText}, but it is executed too late.`; + break; + case "stopped-too-soon": + scenario += ` applied ${controlAction} long enough, while ${contextText}, but execution is stopped too soon.`; + break; + case "applied-too-long": + scenario += ` stopped ${controlAction} in time, while ${contextText}, but it is executed too long.`; + break; + } + text += `S for ${context.name} "${scenario}"\n`; + // add scenarios for incorrect process model values + const scenarioStart = this.generateScenarioForUCAWithContextTable(context); + switch (rule.type) { + case "not-provided": + case "provided": + context.assignedValues.forEach(assignedValue => { + text += `S for ${context.name} "${scenarioStart} Because ${system} incorrectly believes that ${assignedValue.variable.$refText} is not ${assignedValue.value.$refText}."\n`; + }); + break; + case "too-late": + context.assignedValues.forEach(assignedValue => { + text += `S for ${context.name} "${scenarioStart} Because ${system} realized too late that ${assignedValue.variable.$refText} is ${assignedValue.value.$refText}."\n`; + }); + break; + case "stopped-too-soon": + context.assignedValues.forEach(assignedValue => { + text += `S for ${context.name} "${scenarioStart} Because ${system} incorrectly believes that ${assignedValue.variable.$refText} is not ${assignedValue.value.$refText} anymore."\n`; + }); + break; + case "applied-too-long": + context.assignedValues.forEach(assignedValue => { + text += `S for ${context.name} "${scenarioStart} Because ${system} realized too late that ${assignedValue.variable.$refText} is not ${assignedValue.value.$refText} anymore."\n`; + }); + break; + } + }); + }); + return text; + } + /** * Generates the UCA text for the given scenario {@code scenario}. * If no UCA is reference an empty string is returned. @@ -344,48 +417,61 @@ export class STPACompletionProvider extends DefaultCompletionProvider { /** * Generates a scenario text for a UCA defined with a context table. - * @param uca The UCA for which the scenario should be generated. + * @param context The UCA context for which the scenario should be generated. * @returns the generated scenario text. */ - protected generateScenarioForUCAWithContextTable(uca: Context): string { - const rule = uca.$container; - let text = `${rule.system.$refText}`; + protected generateScenarioForUCAWithContextTable(context: Context): string { + const rule = context.$container; + const system = rule.system.ref?.label ?? rule.system.$refText; + let text = `${system}`; + const controlAction = `the control action '${rule.action.ref?.label}'`; switch (rule.type) { case "not-provided": - text += ` did not provide the control action ${rule.action.ref?.label}`; + text += ` did not provide ${controlAction}`; break; case "provided": - text += ` provided the control action ${rule.action.ref?.label}`; + text += ` provided ${controlAction}`; break; case "stopped-too-soon": - text += ` stopped the control action ${rule.action.ref?.label} too soon`; + text += ` stopped ${controlAction} too soon`; break; case "applied-too-long": - text += ` applied the control action ${rule.action.ref?.label} too long`; + text += ` applied ${controlAction} too long`; break; case "too-early": - text += ` provided the control action ${rule.action.ref?.label} too early`; + text += ` provided ${controlAction} too early`; break; case "too-late": - text += ` provided the control action ${rule.action.ref?.label} too late`; + text += ` provided ${controlAction} too late`; break; case "wrong-time": - text += ` provided the control action ${rule.action.ref?.label} at the wrong time`; + text += ` provided ${controlAction} at the wrong time`; break; } text += `, while`; - uca.assignedValues.forEach((assignedValue, index) => { + text += this.createContextText(context); + text += "."; + + return text; + } + + /** + * Creates a text for the given context {@code context}. + * @param context The context for which the text should be generated. + * @returns the generated text. + */ + protected createContextText(context: Context): string { + let text = ``; + context.assignedValues.forEach((assignedValue, index) => { if (index > 0) { text += ", "; } - if ((index += uca.assignedValues.length - 1)) { + if ((index += context.assignedValues.length - 1)) { text += ", and"; } text += ` ${assignedValue.variable.$refText} was ${assignedValue.value.$refText}`; }); - text += "."; - return text; } diff --git a/extension/src-language-server/stpa/stpa-module.ts b/extension/src-language-server/stpa/stpa-module.ts index 52e5b56..8898565 100644 --- a/extension/src-language-server/stpa/stpa-module.ts +++ b/extension/src-language-server/stpa/stpa-module.ts @@ -26,7 +26,7 @@ import { ContextTableProvider } from "./contextTable/context-dataProvider"; import { StpaDiagramGenerator } from "./diagram/diagram-generator"; import { StpaLayoutConfigurator } from "./diagram/layout-config"; import { StpaSynthesisOptions } from "./diagram/stpa-synthesis-options"; -import { STPACompletionProvider } from "./scenario-completion-provider"; +import { STPACompletionProvider } from "./stpa-completion-provider"; import { StpaScopeProvider } from "./stpa-scopeProvider"; import { StpaValidationRegistry, StpaValidator } from "./stpa-validator"; diff --git a/extension/src/language-extension.ts b/extension/src/language-extension.ts index 89b81cd..c3a3f8f 100644 --- a/extension/src/language-extension.ts +++ b/extension/src/language-extension.ts @@ -78,7 +78,10 @@ export class StpaLspVscodeExtension extends LspWebviewPanelManager { this.handleTextChangeEvent(changeEvent); }); // language client sent workspace edits - options.languageClient.onNotification("editor/workspaceedit", ({ edits, uri }) => applyTextEdits(edits, uri)); + options.languageClient.onNotification("editor/workspaceedit", ({ edits, uri }) => { + this.ignoreNextTextChange = true; + applyTextEdits(edits, uri); + }); // laguage server is ready options.languageClient.onNotification("ready", () => { this.resolveLSReady(); From 07adc13909316c747a846ff707bf1b5e51158694 Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Mon, 2 Sep 2024 15:08:28 +0200 Subject: [PATCH 8/9] added "TODO" to completion item texts --- .../stpa/stpa-completion-provider.ts | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/extension/src-language-server/stpa/stpa-completion-provider.ts b/extension/src-language-server/stpa/stpa-completion-provider.ts index ffca94a..67d194f 100644 --- a/extension/src-language-server/stpa/stpa-completion-provider.ts +++ b/extension/src-language-server/stpa/stpa-completion-provider.ts @@ -251,7 +251,7 @@ export class STPACompletionProvider extends DefaultCompletionProvider { const notProvidedItem = { label: "Generate not provided UCA Text", kind: CompletionItemKind.Text, - insertText: `${system} did not provide ${controlAction}, `, + insertText: `${system} did not provide ${controlAction}, TODO`, detail: "Inserts the starting text for this UCA.", sortText: "0", }; @@ -260,7 +260,7 @@ export class STPACompletionProvider extends DefaultCompletionProvider { const providedItem = { label: "Generate provided UCA Text", kind: CompletionItemKind.Text, - insertText: `${system} provided ${controlAction}, `, + insertText: `${system} provided ${controlAction}, TODO`, detail: "Inserts the starting text for this UCA.", sortText: "0", }; @@ -269,14 +269,14 @@ export class STPACompletionProvider extends DefaultCompletionProvider { const tooEarlyItem = { label: "Generate too-early UCA Text", kind: CompletionItemKind.Text, - insertText: `${system} provided ${controlAction} before`, + insertText: `${system} provided ${controlAction} before TODO`, detail: "Inserts the starting text for this UCA.", sortText: "0", }; const tooLateItem = { label: "Generate too-late UCA Text", kind: CompletionItemKind.Text, - insertText: `${system} provided ${controlAction} after`, + insertText: `${system} provided ${controlAction} after TODO`, detail: "Inserts the starting text for this UCA.", sortText: "1", }; @@ -285,14 +285,14 @@ export class STPACompletionProvider extends DefaultCompletionProvider { const stoppedTooSoonItem = { label: "Generate stopped-too-soon UCA Text", kind: CompletionItemKind.Text, - insertText: `${system} stopped ${controlAction} before`, + insertText: `${system} stopped ${controlAction} before TODO`, detail: "Inserts the starting text for this UCA.", sortText: "0", }; const appliedTooLongItem = { label: "Generate applied-too-long UCA Text", kind: CompletionItemKind.Text, - insertText: `${system} still applied ${controlAction} after`, + insertText: `${system} still applied ${controlAction} after TODO`, detail: "Inserts the starting text for this UCA.", sortText: "1", }; @@ -367,29 +367,29 @@ export class STPACompletionProvider extends DefaultCompletionProvider { scenario += ` stopped ${controlAction} in time, while ${contextText}, but it is executed too long.`; break; } - text += `S for ${context.name} "${scenario}"\n`; + text += `S for ${context.name} "${scenario} TODO"\n`; // add scenarios for incorrect process model values const scenarioStart = this.generateScenarioForUCAWithContextTable(context); switch (rule.type) { case "not-provided": case "provided": context.assignedValues.forEach(assignedValue => { - text += `S for ${context.name} "${scenarioStart} Because ${system} incorrectly believes that ${assignedValue.variable.$refText} is not ${assignedValue.value.$refText}."\n`; + text += `S for ${context.name} "${scenarioStart} Because ${system} incorrectly believes that ${assignedValue.variable.$refText} is not ${assignedValue.value.$refText}. TODO"\n`; }); break; case "too-late": context.assignedValues.forEach(assignedValue => { - text += `S for ${context.name} "${scenarioStart} Because ${system} realized too late that ${assignedValue.variable.$refText} is ${assignedValue.value.$refText}."\n`; + text += `S for ${context.name} "${scenarioStart} Because ${system} realized too late that ${assignedValue.variable.$refText} is ${assignedValue.value.$refText}. TODO"\n`; }); break; case "stopped-too-soon": context.assignedValues.forEach(assignedValue => { - text += `S for ${context.name} "${scenarioStart} Because ${system} incorrectly believes that ${assignedValue.variable.$refText} is not ${assignedValue.value.$refText} anymore."\n`; + text += `S for ${context.name} "${scenarioStart} Because ${system} incorrectly believes that ${assignedValue.variable.$refText} is not ${assignedValue.value.$refText} anymore. TODO"\n`; }); break; case "applied-too-long": context.assignedValues.forEach(assignedValue => { - text += `S for ${context.name} "${scenarioStart} Because ${system} realized too late that ${assignedValue.variable.$refText} is not ${assignedValue.value.$refText} anymore."\n`; + text += `S for ${context.name} "${scenarioStart} Because ${system} realized too late that ${assignedValue.variable.$refText} is not ${assignedValue.value.$refText} anymore. TODO"\n`; }); break; } @@ -451,7 +451,7 @@ export class STPACompletionProvider extends DefaultCompletionProvider { text += `, while`; text += this.createContextText(context); - text += "."; + text += ". TODO"; return text; } @@ -481,6 +481,6 @@ export class STPACompletionProvider extends DefaultCompletionProvider { * @returns the generated scenario text. */ protected generateScenarioForUCAWithPlainText(uca: UCA): string { - return uca.description; + return `${uca.description} TODO`; } } From 415d9385d0494a02efb44f581dea4e9db2fcd1cf Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Tue, 3 Sep 2024 15:02:12 +0200 Subject: [PATCH 9/9] added info highlighting for TODOs in stpa file --- .../stpa/stpa-completion-provider.ts | 7 +- .../stpa/stpa-validator.ts | 88 +++++++++++++++++-- 2 files changed, 88 insertions(+), 7 deletions(-) diff --git a/extension/src-language-server/stpa/stpa-completion-provider.ts b/extension/src-language-server/stpa/stpa-completion-provider.ts index 67d194f..49ab8fc 100644 --- a/extension/src-language-server/stpa/stpa-completion-provider.ts +++ b/extension/src-language-server/stpa/stpa-completion-provider.ts @@ -360,6 +360,9 @@ export class STPACompletionProvider extends DefaultCompletionProvider { case "too-late": scenario += ` provided ${controlAction} in time, while ${contextText}, but it is executed too late.`; break; + case "too-early": + scenario += ` provided ${controlAction} in time, while ${contextText}, but it is already executed before.`; + break; case "stopped-too-soon": scenario += ` applied ${controlAction} long enough, while ${contextText}, but execution is stopped too soon.`; break; @@ -407,7 +410,7 @@ export class STPACompletionProvider extends DefaultCompletionProvider { const uca = scenario.uca?.ref; if (uca) { if (uca.$type === Context) { - return this.generateScenarioForUCAWithContextTable(uca); + return this.generateScenarioForUCAWithContextTable(uca) + " TODO"; } else { return this.generateScenarioForUCAWithPlainText(uca); } @@ -451,7 +454,7 @@ export class STPACompletionProvider extends DefaultCompletionProvider { text += `, while`; text += this.createContextText(context); - text += ". TODO"; + text += "."; return text; } diff --git a/extension/src-language-server/stpa/stpa-validator.ts b/extension/src-language-server/stpa/stpa-validator.ts index a42f683..721f04d 100644 --- a/extension/src-language-server/stpa/stpa-validator.ts +++ b/extension/src-language-server/stpa/stpa-validator.ts @@ -84,6 +84,7 @@ export class StpaValidator { */ checkModel(model: Model, accept: ValidationAcceptor): void { this.checkAllAspectsPresent(model, accept); + this.checkForTODOs(model, accept); const hazards = collectElementsWithSubComps(model.hazards) as Hazard[]; const sysCons = collectElementsWithSubComps(model.systemLevelConstraints) as SystemConstraint[]; @@ -179,7 +180,7 @@ export class StpaValidator { * Check whether the control actions of a node are referenced by at least one UCA. * @param nodes The nodes to check. * @param ucaActions The control actions that are referenced by a UCA. - * @param accept + * @param accept */ protected checkControlActionsReferencedByUCA( nodes: Node[], @@ -353,8 +354,13 @@ export class StpaValidator { let isSame = true; // check whether context1 is a subset of context2 for (let i = 0; i < context1.assignedValues.length; i++) { - const varIndex = context2.assignedValues.findIndex(v => v.variable.$refText === context1.assignedValues[i].variable.$refText); - if (varIndex === -1 || context2.assignedValues[varIndex].value.$refText !== context1.assignedValues[i].value.$refText) { + const varIndex = context2.assignedValues.findIndex( + v => v.variable.$refText === context1.assignedValues[i].variable.$refText + ); + if ( + varIndex === -1 || + context2.assignedValues[varIndex].value.$refText !== context1.assignedValues[i].value.$refText + ) { isSame = false; break; } @@ -363,8 +369,13 @@ export class StpaValidator { isSame = true; // check whether context2 is a subset of context1 for (let i = 0; i < context2.assignedValues.length; i++) { - const varIndex = context1.assignedValues.findIndex(v => v.variable.$refText === context2.assignedValues[i].variable.$refText); - if (varIndex === -1 || context1.assignedValues[varIndex].value.$refText !== context2.assignedValues[i].value.$refText) { + const varIndex = context1.assignedValues.findIndex( + v => v.variable.$refText === context2.assignedValues[i].variable.$refText + ); + if ( + varIndex === -1 || + context1.assignedValues[varIndex].value.$refText !== context2.assignedValues[i].value.$refText + ) { isSame = false; break; } @@ -545,6 +556,73 @@ export class StpaValidator { } } + /** + * Checks whether the model contains any TODOs. + * @param model The model to check. + * @param accept + */ + protected checkForTODOs(model: Model, accept: ValidationAcceptor): void { + model.losses.forEach(loss => { + if (loss.description && loss.description.includes("TODO")) { + accept("info", "This element contains a TODO", { node: loss, property: "description" }); + } + }); + model.hazards.forEach(hazard => { + if (hazard.description && hazard.description.includes("TODO")) { + accept("info", "This element contains a TODO", { node: hazard, property: "description" }); + } + }); + model.systemLevelConstraints.forEach(sysCon => { + if (sysCon.description && sysCon.description.includes("TODO")) { + accept("info", "This element contains a TODO", { node: sysCon, property: "description" }); + } + }); + model.responsibilities.forEach(resp => { + resp.responsiblitiesForOneSystem.forEach(responsibility => { + if (responsibility.description && responsibility.description.includes("TODO")) { + accept("info", "This element contains a TODO", { node: responsibility, property: "description" }); + } + }); + }); + model.allUCAs.forEach(uca => { + uca.providingUcas.forEach(providingUca => { + if (providingUca.description && providingUca.description.includes("TODO")) { + accept("info", "This element contains a TODO", { node: providingUca, property: "description" }); + } + }); + uca.notProvidingUcas.forEach(notProvidingUca => { + if (notProvidingUca.description && notProvidingUca.description.includes("TODO")) { + accept("info", "This element contains a TODO", { node: notProvidingUca, property: "description" }); + } + }); + uca.wrongTimingUcas.forEach(wrongTimingUca => { + if (wrongTimingUca.description && wrongTimingUca.description.includes("TODO")) { + accept("info", "This element contains a TODO", { node: wrongTimingUca, property: "description" }); + } + }); + uca.continousUcas.forEach(continousUca => { + if (continousUca.description && continousUca.description.includes("TODO")) { + accept("info", "This element contains a TODO", { node: continousUca, property: "description" }); + } + }); + }); + model.controllerConstraints.forEach(constraint => { + if (constraint.description && constraint.description.includes("TODO")) { + accept("info", "This element contains a TODO", { node: constraint, property: "description" }); + } + }); + model.scenarios.forEach(scenario => { + if (scenario.description && scenario.description.includes("TODO")) { + accept("info", "This element contains a TODO", { node: scenario, property: "description" }); + } + }); + model.safetyCons.forEach(safetyReq => { + if (safetyReq.description && safetyReq.description.includes("TODO")) { + accept("info", "This element contains a TODO", { node: safetyReq, property: "description" }); + } + }); + } + /** * Checks whether IDs are mentioned more than once in a reference list. * @param main The AstNode containing the {@code list}.