// This program has been developed by students from the bachelor Computer Science at Utrecht // University within the Software Project course. // © Copyright Utrecht University (Department of Information and Computing Sciences) /* contains all logic for the VisProgEditor warning system * * Missing but desirable features: * - Warning filtering: * - if there is no completely connected chain of startNode-[PhaseNodes]-EndNode * then hide any startNode, phaseNode, or endNode specific warnings */ import useFlowStore from "../VisProgStores.tsx"; import type {FlowState} from "../VisProgTypes.tsx"; // --| Type definitions |-- export type WarningId = NodeId | "GLOBAL_WARNINGS"; export type NodeId = string; export type WarningType = | 'MISSING_INPUT' | 'MISSING_OUTPUT' | 'PLAN_IS_UNDEFINED' | 'INCOMPLETE_PROGRAM' | 'NOT_CONNECTED_TO_PROGRAM' | string export type WarningSeverity = | 'INFO' // Acceptable, but important to be aware of | 'WARNING' // Acceptable, but probably undesirable behavior | 'ERROR' // Prevents running program, should be fixed before running program is allowed /** * warning scope, include a handleId if the warning is handle specific */ export type WarningScope = { id: string; handleId?: string; } export type EditorWarning = { scope: WarningScope; type: WarningType; severity: WarningSeverity; description: string; }; /** * a scoped WarningKey, * the handleId scoping is only needed for handle specific errors * * "`WarningType`:`handleId`" */ export type WarningKey = string; // for warnings that can occur on a per-handle basis /** * a composite key used in the severityIndex * * "`WarningId`|`WarningKey`" */ export type CompositeWarningKey = string; export type WarningRegistry = Map>; export type SeverityIndex = Map>; type ZustandSet = (partial: Partial | ((state: FlowState) => Partial)) => void; type ZustandGet = () => FlowState; export type EditorWarningRegistry = { /** * stores all editor warnings */ editorWarningRegistry: WarningRegistry; /** * index of warnings by severity */ severityIndex: SeverityIndex; /** * gets all warnings and returns them as a list of warnings * @returns {EditorWarning[]} */ getWarnings: () => EditorWarning[]; /** * gets all warnings with the current severity * @param {WarningSeverity} warningSeverity * @returns {EditorWarning[]} */ getWarningsBySeverity: (warningSeverity: WarningSeverity) => EditorWarning[]; /** * checks if there are no warnings of breaking severity * @returns {boolean} */ isProgramValid: () => boolean; /** * registers a warning to the warningRegistry and the SeverityIndex * @param {EditorWarning} warning */ registerWarning: (warning: EditorWarning) => void; /** * unregisters a warning from the warningRegistry and the SeverityIndex * @param {EditorWarning} warning */ unregisterWarning: (id: WarningId, warningKey: WarningKey) => void /** * unregisters warnings from the warningRegistry and the SeverityIndex * @param {WarningId} warning */ unregisterWarningsForId: (id: WarningId) => void; } // --| implemented logic |-- /** * the id to use for global editor warnings * @type {string} */ export const globalWarning = "GLOBAL_WARNINGS"; export function editorWarningRegistry(get: ZustandGet, set: ZustandSet) : EditorWarningRegistry { return { editorWarningRegistry: new Map>(), severityIndex: new Map([ ['INFO', new Set()], ['WARNING', new Set()], ['ERROR', new Set()], ]), getWarningsBySeverity: (warningSeverity) => { const wRegistry = new Map([...get().editorWarningRegistry].map(([k, v]) => [k, new Map(v)])); const sIndex = new Map(get().severityIndex); const warningKeys = sIndex.get(warningSeverity); const warnings: EditorWarning[] = []; warningKeys?.forEach( (compositeKey) => { const [id, warningKey] = compositeKey.split('|'); const warning = wRegistry.get(id)?.get(warningKey); if (warning) { warnings.push(warning); } } ) return warnings; }, isProgramValid: () => { const sIndex = get().severityIndex; return (sIndex.get("ERROR")!.size === 0); }, getWarnings: () => Array.from(get().editorWarningRegistry.values()) .flatMap(innerMap => Array.from(innerMap.values())), registerWarning: (warning) => { const { scope: {id, handleId}, type, severity } = warning; const warningKey = handleId ? `${type}:${handleId}` : type; const compositeKey = `${id}|${warningKey}`; const wRegistry = new Map([...get().editorWarningRegistry].map(([k, v]) => [k, new Map(v)])); const sIndex = new Map(get().severityIndex); // add to warning registry if (!wRegistry.has(id)) { wRegistry.set(id, new Map()); } wRegistry.get(id)!.set(warningKey, warning); // add to severityIndex if (!sIndex.get(severity)!.has(compositeKey)) { sIndex.get(severity)!.add(compositeKey); } set({ editorWarningRegistry: wRegistry, severityIndex: sIndex }) }, unregisterWarning: (id, warningKey) => { const wRegistry = new Map([...get().editorWarningRegistry].map(([k, v]) => [k, new Map(v)])); const sIndex = new Map(get().severityIndex); // verify if the warning was created already const warning = wRegistry.get(id)?.get(warningKey); if (!warning) return; // remove from warning registry wRegistry.get(id)!.delete(warningKey); // remove from severityIndex sIndex.get(warning.severity)!.delete(`${id}|${warningKey}`); set({ editorWarningRegistry: wRegistry, severityIndex: sIndex }) }, unregisterWarningsForId: (id) => { const wRegistry = new Map([...get().editorWarningRegistry].map(([k, v]) => [k, new Map(v)])); const sIndex = new Map(get().severityIndex); const nodeWarnings = wRegistry.get(id); // remove from severity index if (nodeWarnings) { nodeWarnings.forEach((warning) => { const warningKey = warning.scope.handleId ? `${warning.type}:${warning.scope.handleId}` : warning.type; sIndex.get(warning.severity)?.delete(`${id}|${warningKey}`); }); } // remove from warning registry wRegistry.delete(id); set({ editorWarningRegistry: wRegistry, severityIndex: sIndex }) }, }} /** * returns a summary of the warningRegistry * @returns {{info: number, warning: number, error: number, isValid: boolean}} */ export function warningSummary() { const {severityIndex, isProgramValid} = useFlowStore.getState(); return { info: severityIndex.get('INFO')!.size, warning: severityIndex.get('WARNING')!.size, error: severityIndex.get('ERROR')!.size, isValid: isProgramValid(), }; }