diff --git a/extension/src-language-server/stpa/ID-enforcer.ts b/extension/src-language-server/stpa/ID-enforcer.ts index 5229de7..ca2ee70 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"; @@ -198,7 +214,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 @@ -355,103 +371,106 @@ 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); + 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 }; } + /** + * 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; + 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/stpa-completion-provider.ts b/extension/src-language-server/stpa/stpa-completion-provider.ts new file mode 100644 index 0000000..49ab8fc --- /dev/null +++ b/extension/src-language-server/stpa/stpa-completion-provider.ts @@ -0,0 +1,489 @@ +/* + * 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, + CompletionValueItem, + DefaultCompletionProvider, + MaybePromise, + NextFeature, +} from "langium"; +import { CompletionItemKind } from "vscode-languageserver"; +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. + */ +export class STPACompletionProvider 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) { + 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. + * @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( + context.node as UCA, + context.node.$containerProperty + ); + + if (generatedItems.length > 0) { + generatedItems.forEach(item => acceptor(item)); + } + } + } + + /** + * 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; + 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.ref?.label ?? parent.target.$refText}`; + } + switch (property) { + case "notProvidingUcas": + const notProvidedItem = { + label: "Generate not provided UCA Text", + kind: CompletionItemKind.Text, + insertText: `${system} did not provide ${controlAction}, TODO`, + 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: `${system} provided ${controlAction}, TODO`, + 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: `${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 TODO`, + 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: `${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 TODO`, + detail: "Inserts the starting text for this UCA.", + sortText: "1", + }; + return [stoppedTooSoonItem, appliedTooLongItem]; + } + return []; + } + + /** + * 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) { + 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 "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; + case "applied-too-long": + scenario += ` stopped ${controlAction} in time, while ${contextText}, but it is executed too long.`; + break; + } + 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}. 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}. 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. 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. TODO"\n`; + }); + break; + } + }); + }); + return text; + } + + /** + * 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) + " TODO"; + } else { + return this.generateScenarioForUCAWithPlainText(uca); + } + } + return ""; + } + + /** + * Generates a scenario text for a UCA defined with a context table. + * @param context The UCA context for which the scenario should be generated. + * @returns the generated scenario text. + */ + 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 ${controlAction}`; + break; + case "provided": + text += ` provided ${controlAction}`; + break; + case "stopped-too-soon": + text += ` stopped ${controlAction} too soon`; + break; + case "applied-too-long": + text += ` applied ${controlAction} too long`; + break; + case "too-early": + text += ` provided ${controlAction} too early`; + break; + case "too-late": + text += ` provided ${controlAction} too late`; + break; + case "wrong-time": + text += ` provided ${controlAction} at the wrong time`; + break; + } + + text += `, while`; + 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 += context.assignedValues.length - 1)) { + text += ", and"; + } + text += ` ${assignedValue.variable.$refText} was ${assignedValue.value.$refText}`; + }); + 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} TODO`; + } +} diff --git a/extension/src-language-server/stpa/stpa-module.ts b/extension/src-language-server/stpa/stpa-module.ts index fe524fe..8898565 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 { STPACompletionProvider } from "./stpa-completion-provider"; 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: STPACompletionProvider; + }; references: { StpaScopeProvider: StpaScopeProvider; }; @@ -79,6 +83,10 @@ export const STPAModule: Module new STPACompletionProvider(services), + ScenarioCompletionProvider: services => new STPACompletionProvider(services), + }, references: { ScopeProvider: services => new StpaScopeProvider(services), StpaScopeProvider: services => new StpaScopeProvider(services), 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}. 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' | 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();