From e97b772c5b6484522d1f2ad71abee9b5d3585f9a Mon Sep 17 00:00:00 2001 From: Cody Hansen Date: Mon, 30 Sep 2024 09:04:51 -1000 Subject: [PATCH 1/5] Revered linter back to the original state --- .../sequence-editor/sequence-linter.ts | 2473 ++++++++--------- 1 file changed, 1222 insertions(+), 1251 deletions(-) diff --git a/src/utilities/sequence-editor/sequence-linter.ts b/src/utilities/sequence-editor/sequence-linter.ts index cb4580e34f..2d3ff89741 100644 --- a/src/utilities/sequence-editor/sequence-linter.ts +++ b/src/utilities/sequence-editor/sequence-linter.ts @@ -1,6 +1,6 @@ import { syntaxTree } from '@codemirror/language'; -import { type Diagnostic } from '@codemirror/lint'; -import { EditorState } from '@codemirror/state'; +import { linter, type Diagnostic } from '@codemirror/lint'; +import type { Extension } from '@codemirror/state'; import type { SyntaxNode, Tree } from '@lezer/common'; import type { ChannelDictionary, @@ -20,8 +20,7 @@ import { TOKEN_COMMAND, TOKEN_ERROR, TOKEN_REPEAT_ARG, TOKEN_REQUEST } from '../ import { TimeTypes } from '../../enums/time'; import { getGlobals, sequenceAdaptation } from '../../stores/sequence-adaptation'; import { CustomErrorCodes } from '../../workers/customCodes'; -import { addDefaultArgs, quoteEscape } from '../codemirror/codemirror-utils'; -import { closeSuggestion, computeBlocks, openSuggestion } from '../codemirror/custom-folder'; +import { addDefaultArgs, isHexValue, parseNumericArg, quoteEscape } from '../codemirror/codemirror-utils'; import { getBalancedDuration, getDoyTime, @@ -57,6 +56,18 @@ function closestStrings(value: string, potentialMatches: string[], n: number) { return distances.slice(0, n).map(pair => pair.s); } +type WhileOpener = { + command: SyntaxNode; + from: number; + stemToClose: string; + to: number; + word: string; +}; + +type IfOpener = WhileOpener & { + hasElse: boolean; +}; + type VariableMap = { [name: string]: VariableDeclaration; }; @@ -66,1022 +77,993 @@ type VariableMap = { * Can be optionally called with a command dictionary so it's available during linting. */ export function sequenceLinter( - view: EditorView, channelDictionary: ChannelDictionary | null = null, commandDictionary: CommandDictionary | null = null, parameterDictionaries: ParameterDictionary[] = [], -): Diagnostic[] { - const tree = syntaxTree(view.state); - const treeNode = tree.topNode; - const docText = view.state.doc.toString(); - let diagnostics: Diagnostic[] = []; +): Extension { + return linter(view => { + const tree = syntaxTree(view.state); + const treeNode = tree.topNode; + const docText = view.state.doc.toString(); + let diagnostics: Diagnostic[] = []; - diagnostics.push(...validateParserErrors(tree)); + diagnostics.push(...validateParserErrors(tree)); - // TODO: Get identify type mapping to use - const variables: VariableDeclaration[] = [ - ...(getGlobals().map(g => ({ name: g.name, type: 'STRING' }) as const) ?? []), - ]; + // TODO: Get identify type mapping to use + const variables: VariableDeclaration[] = [ + ...(getGlobals().map(g => ({ name: g.name, type: 'STRING' }) as const) ?? []), + ]; - // Validate top level metadata - diagnostics.push(...validateMetadata(treeNode)); + // Validate top level metadata + diagnostics.push(...validateMetadata(treeNode)); - diagnostics.push(...validateId(treeNode, docText)); + diagnostics.push(...validateId(treeNode, docText)); - const localsValidation = validateLocals(treeNode.getChildren('LocalDeclaration'), docText); - variables.push(...localsValidation.variables); - diagnostics.push(...localsValidation.diagnostics); + const localsValidation = validateLocals(treeNode.getChildren('LocalDeclaration'), docText); + variables.push(...localsValidation.variables); + diagnostics.push(...localsValidation.diagnostics); - const parameterValidation = validateParameters(treeNode.getChildren('ParameterDeclaration'), docText); - variables.push(...parameterValidation.variables); - diagnostics.push(...parameterValidation.diagnostics); + const parameterValidation = validateParameters(treeNode.getChildren('ParameterDeclaration'), docText); + variables.push(...parameterValidation.variables); + diagnostics.push(...parameterValidation.diagnostics); - const variableMap: VariableMap = {}; - for (const variable of variables) { - variableMap[variable.name] = variable; - } + const variableMap: VariableMap = {}; + for (const variable of variables) { + variableMap[variable.name] = variable; + } - // Validate command type mixing - diagnostics.push(...validateCommandTypeMixing(treeNode)); + // Validate command type mixing + diagnostics.push(...validateCommandTypeMixing(treeNode)); - diagnostics.push(...validateCustomDirectives(treeNode, docText)); + diagnostics.push(...validateCustomDirectives(treeNode, docText)); + + const commandsNode = treeNode.getChild('Commands'); + if (commandsNode) { + diagnostics.push(...commandLinter(commandsNode.getChildren(TOKEN_COMMAND), docText, variableMap)); + diagnostics.push(...validateRequests(commandsNode.getChildren(TOKEN_REQUEST), docText, variableMap)); + } - const commandsNode = treeNode.getChild('Commands'); - if (commandsNode) { diagnostics.push( - ...commandLinter( - commandsNode.getChildren(TOKEN_COMMAND), + ...immediateCommandLinter( + treeNode.getChild('ImmediateCommands')?.getChildren(TOKEN_COMMAND) || [], docText, variableMap, - commandDictionary, - channelDictionary, - parameterDictionaries, ), ); + diagnostics.push( - ...validateRequests( - commandsNode.getChildren(TOKEN_REQUEST), - docText, - variableMap, - commandDictionary, - channelDictionary, - parameterDictionaries, - ), + ...hardwareCommandLinter(treeNode.getChild('HardwareCommands')?.getChildren(TOKEN_COMMAND) || [], docText), ); - } - diagnostics.push( - ...immediateCommandLinter( - treeNode.getChild('ImmediateCommands')?.getChildren(TOKEN_COMMAND) || [], - docText, - variableMap, - commandDictionary, - channelDictionary, - parameterDictionaries, - ), - ); - - diagnostics.push( - ...hardwareCommandLinter( - treeNode.getChild('HardwareCommands')?.getChildren(TOKEN_COMMAND) || [], - docText, - commandDictionary, - channelDictionary, - parameterDictionaries, - ), - ); - - diagnostics.push( - ...conditionalAndLoopKeywordsLinter(treeNode.getChild('Commands')?.getChildren(TOKEN_COMMAND) || [], view.state), - ); - - const inputLinter = get(sequenceAdaptation)?.inputFormat.linter; - - if (inputLinter !== undefined && commandDictionary !== null) { - diagnostics = inputLinter(diagnostics, commandDictionary, view, treeNode); - } + diagnostics.push( + ...conditionalAndLoopKeywordsLinter(treeNode.getChild('Commands')?.getChildren(TOKEN_COMMAND) || [], docText), + ); - return diagnostics; -} + const inputLinter = get(sequenceAdaptation)?.inputFormat.linter; -/** - * Checks for unexpected tokens. - * - * @param tree - * @returns - */ -function validateParserErrors(tree: Tree) { - const diagnostics: Diagnostic[] = []; - const MAX_PARSER_ERRORS = 100; - tree.iterate({ - enter: node => { - if (node.name === TOKEN_ERROR && diagnostics.length < MAX_PARSER_ERRORS) { - const { from, to } = node; - diagnostics.push({ - from, - message: `Unexpected token`, - severity: 'error', - to, - }); - } - }, + if (inputLinter !== undefined && commandDictionary !== null) { + diagnostics = inputLinter(diagnostics, commandDictionary, view, treeNode); + } + + return diagnostics; }); - return diagnostics; -} -function conditionalAndLoopKeywordsLinter(commandNodes: SyntaxNode[], state: EditorState): Diagnostic[] { - const diagnostics: Diagnostic[] = []; + /** + * Checks for unexpected tokens. + * + * @param tree + * @returns + */ + function validateParserErrors(tree: Tree) { + const diagnostics: Diagnostic[] = []; + const MAX_PARSER_ERRORS = 100; + tree.iterate({ + enter: node => { + if (node.name === TOKEN_ERROR && diagnostics.length < MAX_PARSER_ERRORS) { + const { from, to } = node; + diagnostics.push({ + from, + message: `Unexpected token`, + severity: 'error', + to, + }); + } + }, + }); + return diagnostics; + } + + function conditionalAndLoopKeywordsLinter(commandNodes: SyntaxNode[], text: string): Diagnostic[] { + const diagnostics: Diagnostic[] = []; + const conditionalStack: IfOpener[] = []; + const loopStack: WhileOpener[] = []; + const conditionalKeywords = []; + const loopKeywords = []; + const sequenceAdaptationConditionalKeywords = get(sequenceAdaptation)?.conditionalKeywords; + const sequenceAdaptationLoopKeywords = get(sequenceAdaptation)?.loopKeywords; + + conditionalKeywords.push( + sequenceAdaptationConditionalKeywords?.else, + ...(sequenceAdaptationConditionalKeywords?.elseIf ?? []), + sequenceAdaptationConditionalKeywords?.endIf, + ); + loopKeywords.push( + sequenceAdaptationLoopKeywords?.break, + sequenceAdaptationLoopKeywords?.continue, + sequenceAdaptationLoopKeywords?.endWhileLoop, + ); - const blocks = computeBlocks(state); + for (const command of commandNodes) { + const stem = command.getChild('Stem'); + if (stem) { + const word = text.slice(stem.from, stem.to); + + if (sequenceAdaptationConditionalKeywords?.if.includes(word)) { + conditionalStack.push({ + command, + from: stem.from, + hasElse: false, + stemToClose: sequenceAdaptationConditionalKeywords.endIf, + to: stem.to, + word, + }); + } - if (blocks) { - const pairs = Object.values(blocks); - pairs.forEach(pair => { - if (!pair.start && pair.end) { - const stem = state.sliceDoc(pair.end.from, pair.end.to); - diagnostics.push({ - from: pair.end.from, - message: `${stem} must match a preceding ${openSuggestion(stem)}`, - severity: 'error', - to: pair.end.to, - }); - } else if (pair.start && !pair.end) { - const stem = state.sliceDoc(pair.start.from, pair.start.to); - const suggestion = closeSuggestion(stem); - diagnostics.push({ + if (conditionalKeywords.includes(word)) { + if (conditionalStack.length === 0) { + diagnostics.push({ + from: stem.from, + message: `${word} doesn't match a preceding ${sequenceAdaptationConditionalKeywords?.if.join(', ')}.`, + severity: 'error', + to: stem.to, + }); + } else if (word === sequenceAdaptationConditionalKeywords?.else) { + if (!conditionalStack[conditionalStack.length - 1].hasElse) { + conditionalStack[conditionalStack.length - 1].hasElse = true; + } else { + diagnostics.push({ + from: stem.from, + message: `${word} doesn't match a preceding ${sequenceAdaptationConditionalKeywords?.if.join(', ')}.`, + severity: 'error', + to: stem.to, + }); + } + } else if (word === sequenceAdaptationConditionalKeywords?.endIf) { + conditionalStack.pop(); + } + } + + if (sequenceAdaptationLoopKeywords?.whileLoop.includes(word)) { + loopStack.push({ + command, + from: stem.from, + stemToClose: sequenceAdaptationLoopKeywords.endWhileLoop, + to: stem.to, + word, + }); + } + + if (loopKeywords.includes(word)) { + if (loopStack.length === 0) { + diagnostics.push({ + from: stem.from, + message: `${word} doesn't match a preceding ${sequenceAdaptationLoopKeywords?.whileLoop.join(', ')}.`, + severity: 'error', + to: stem.to, + }); + } + + if (word === sequenceAdaptationLoopKeywords?.endWhileLoop) { + loopStack.pop(); + } + } + } + } + + // Anything left on the stack is unclosed + diagnostics.push( + ...[...loopStack, ...conditionalStack].map(block => { + return { actions: [ { - apply(view: EditorView) { - if (pair.start?.parent) { - view.dispatch({ - changes: { - from: pair.start?.parent.to, - insert: `\nC ${suggestion}\n`, - }, - }); - } + apply(view: EditorView, _from: number, _to: number) { + view.dispatch({ + changes: { + from: block.command.to, + insert: `\nC ${block.stemToClose}\n`, + }, + }); }, - name: `Insert ${suggestion}`, + name: `Insert ${block.stemToClose}`, }, ], - from: pair.start.from, - message: `Block opened by ${stem} is not closed`, + from: block.from, + message: `Unclosed ${block.word}`, severity: 'error', - to: pair.start.to, - }); - } - }); + to: block.to, + } as const; + }), + ); + + return diagnostics; } - return diagnostics; -} + function validateRequests(requestNodes: SyntaxNode[], text: string, variables: VariableMap): Diagnostic[] { + const diagnostics: Diagnostic[] = []; -function validateRequests( - requestNodes: SyntaxNode[], - text: string, - variables: VariableMap, - commandDictionary: CommandDictionary | null, - channelDictionary: ChannelDictionary | null, - parameterDictionaries: ParameterDictionary[], -): Diagnostic[] { - const diagnostics: Diagnostic[] = []; - - for (const request of requestNodes) { - // Get the TimeTag node for the current command - diagnostics.push(...validateTimeTags(request, text)); - } + for (const request of requestNodes) { + // Get the TimeTag node for the current command + diagnostics.push(...validateTimeTags(request, text)); + } - diagnostics.push( - ...requestNodes.flatMap(request => - commandLinter( - request.getChild('Steps')?.getChildren(TOKEN_COMMAND) ?? [], - text, - variables, - commandDictionary, - channelDictionary, - parameterDictionaries, + diagnostics.push( + ...requestNodes.flatMap(request => + commandLinter(request.getChild('Steps')?.getChildren(TOKEN_COMMAND) ?? [], text, variables), ), - ), - ); + ); - return diagnostics; -} + return diagnostics; + } -/** - * Validates that a syntax node does not mix different command types. - * - * @param {SyntaxNode} node - The syntax node to validate. - * @return {Diagnostic[]} An array of diagnostics. - */ -function validateCommandTypeMixing(node: SyntaxNode): Diagnostic[] { - // Get the child nodes for Commands, ImmediateCommands, and HardwareCommands. - const commands = node.getChild('Commands'); - const immediateCommands = node.getChild('ImmediateCommands'); - const hardwareCommands = node.getChild('HardwareCommands'); - const lgo = commands?.getChild('LoadAndGoDirective') ?? null; - - // Check if each command type exists and has at least one child node. - const hasCommands = commands !== null && (commands?.getChildren(TOKEN_COMMAND).length > 0 || lgo !== null); - const hasImmediateCommands = immediateCommands !== null; - const hasHardwareCommands = hardwareCommands !== null; - - const diagnostics: Diagnostic[] = []; - - // Get the start. - const { from, to } = getFromAndTo([commands, immediateCommands, hardwareCommands]); - - // If there is a mix of command types, push a diagnostic. - if ((hasCommands && (hasImmediateCommands || hasHardwareCommands)) || (hasImmediateCommands && hasHardwareCommands)) { - if (lgo) { + /** + * Validates that a syntax node does not mix different command types. + * + * @param {SyntaxNode} node - The syntax node to validate. + * @return {Diagnostic[]} An array of diagnostics. + */ + function validateCommandTypeMixing(node: SyntaxNode): Diagnostic[] { + // Get the child nodes for Commands, ImmediateCommands, and HardwareCommands. + const commands = node.getChild('Commands'); + const immediateCommands = node.getChild('ImmediateCommands'); + const hardwareCommands = node.getChild('HardwareCommands'); + const lgo = commands?.getChild('LoadAndGoDirective') ?? null; + + // Check if each command type exists and has at least one child node. + const hasCommands = commands !== null && (commands?.getChildren(TOKEN_COMMAND).length > 0 || lgo !== null); + const hasImmediateCommands = immediateCommands !== null; + const hasHardwareCommands = hardwareCommands !== null; + + const diagnostics: Diagnostic[] = []; + + // Get the start. + const { from, to } = getFromAndTo([commands, immediateCommands, hardwareCommands]); + + // If there is a mix of command types, push a diagnostic. + if ( + (hasCommands && (hasImmediateCommands || hasHardwareCommands)) || + (hasImmediateCommands && hasHardwareCommands) + ) { + if (lgo) { + diagnostics.push({ + from, + message: `Directive 'LOAD_AND_GO' cannot be used with 'Immediate Commands' or 'Hardware Commands'.`, + severity: 'error', + to, + }); + } diagnostics.push({ from, - message: `Directive 'LOAD_AND_GO' cannot be used with 'Immediate Commands' or 'Hardware Commands'.`, + message: 'Cannot mix different command types in one Sequence.', severity: 'error', to, }); } - diagnostics.push({ - from, - message: 'Cannot mix different command types in one Sequence.', - severity: 'error', - to, - }); + return diagnostics; } - return diagnostics; -} -function validateLocals(locals: SyntaxNode[], text: string) { - const variables: VariableDeclaration[] = []; - const diagnostics: Diagnostic[] = []; - diagnostics.push( - ...locals.slice(1).map( - local => - ({ - ...getFromAndTo([local]), - message: 'There is a maximum of one @LOCALS directive per sequence', - severity: 'error', - }) as const, - ), - ); - locals.forEach(local => { - let child = local.firstChild; - while (child) { - if (child.name !== 'Enum') { - diagnostics.push({ - from: child.from, - message: `@LOCALS values are required to be Enums`, - severity: 'error', - to: child.to, - }); - } else { - variables.push({ - name: text.slice(child.from, child.to), - // TODO - hook to check mission specific nomenclature - type: 'STRING', - }); + function validateLocals(locals: SyntaxNode[], text: string) { + const variables: VariableDeclaration[] = []; + const diagnostics: Diagnostic[] = []; + diagnostics.push( + ...locals.slice(1).map( + local => + ({ + ...getFromAndTo([local]), + message: 'There is a maximum of one @LOCALS directive per sequence', + severity: 'error', + }) as const, + ), + ); + locals.forEach(local => { + let child = local.firstChild; + while (child) { + if (child.name !== 'Enum') { + diagnostics.push({ + from: child.from, + message: `@LOCALS values are required to be Enums`, + severity: 'error', + to: child.to, + }); + } else { + variables.push({ + name: text.slice(child.from, child.to), + // TODO - hook to check mission specific nomenclature + type: 'STRING', + }); + } + child = child.nextSibling; } - child = child.nextSibling; - } - }); - return { - diagnostics, - variables, - }; -} - -function validateParameters(inputParams: SyntaxNode[], text: string) { - const variables: VariableDeclaration[] = []; - const diagnostics: Diagnostic[] = []; - diagnostics.push( - ...inputParams.slice(1).map( - inputParam => - ({ - ...getFromAndTo([inputParam]), - message: 'There is a maximum of @INPUT_PARAMS directive per sequence', - severity: 'error', - }) as const, - ), - ); - - inputParams.forEach(inputParam => { - let child = inputParam.firstChild; - - while (child) { - if (child.name !== 'Enum' && child.name !== 'Object') { - diagnostics.push({ - from: child.from, - message: `@INPUT_PARAMS values are required to be Enums`, - severity: 'error', - to: child.to, - }); - } else { - const variable = { - name: text.slice(child.from, child.to), - type: 'STRING', - } as VariableDeclaration; - - variables.push(variable); - - const metadata: SyntaxNode | null = child?.nextSibling; - - if (metadata !== null) { - const properties = metadata.getChildren('Property'); - let allowableRanges: string | undefined = undefined; - let isEnum = false; - let isString = false; - let enumName: string | undefined = undefined; + }); + return { + diagnostics, + variables, + }; + } - properties.forEach(property => { - const propertyNameNode = property.getChild('PropertyName'); - const propertyValueNode = propertyNameNode?.nextSibling; + function validateParameters(inputParams: SyntaxNode[], text: string) { + const variables: VariableDeclaration[] = []; + const diagnostics: Diagnostic[] = []; + diagnostics.push( + ...inputParams.slice(1).map( + inputParam => + ({ + ...getFromAndTo([inputParam]), + message: 'There is a maximum of @INPUT_PARAMS directive per sequence', + severity: 'error', + }) as const, + ), + ); - if (propertyNameNode !== null && propertyValueNode !== null && propertyValueNode !== undefined) { - const propertyName = text.slice(propertyNameNode.from, propertyNameNode.to); - const propertyValue = text.slice(propertyValueNode.from, propertyValueNode.to); + inputParams.forEach(inputParam => { + let child = inputParam.firstChild; - switch (propertyName.toLowerCase()) { - case '"allowable_ranges"': - allowableRanges = propertyValue; - break; - case '"enum_name"': - enumName = propertyValue; - break; - case '"type"': - isEnum = propertyValue === '"ENUM"'; - isString = propertyValue === '"STRING"'; - break; - } - } + while (child) { + if (child.name !== 'Enum' && child.name !== 'Object') { + diagnostics.push({ + from: child.from, + message: `@INPUT_PARAMS values are required to be Enums`, + severity: 'error', + to: child.to, }); - - if (isEnum && enumName === undefined) { - diagnostics.push({ - from: child.from, - message: '"enum_name" is required for ENUM type.', - severity: 'error', - to: child.to, - }); - } else if (!isEnum && enumName !== undefined) { - diagnostics.push({ - from: child.from, - message: `"enum_name": ${enumName} is not required for non-ENUM type.`, - severity: 'error', - to: child.to, - }); - } else if (isString && allowableRanges !== undefined) { - diagnostics.push({ - from: child.from, - message: `'allowable_ranges' is not required for STRING type.`, - severity: 'error', - to: child.to, + } else { + const variable = { + name: text.slice(child.from, child.to), + type: 'STRING', + } as VariableDeclaration; + + variables.push(variable); + + const metadata: SyntaxNode | null = child?.nextSibling; + + if (metadata !== null) { + const properties = metadata.getChildren('Property'); + let allowableRanges: string | undefined = undefined; + let isEnum = false; + let isString = false; + let enumName: string | undefined = undefined; + + properties.forEach(property => { + const propertyNameNode = property.getChild('PropertyName'); + const propertyValueNode = propertyNameNode?.nextSibling; + + if (propertyNameNode !== null && propertyValueNode !== null && propertyValueNode !== undefined) { + const propertyName = text.slice(propertyNameNode.from, propertyNameNode.to); + const propertyValue = text.slice(propertyValueNode.from, propertyValueNode.to); + + switch (propertyName.toLowerCase()) { + case '"allowable_ranges"': + allowableRanges = propertyValue; + break; + case '"enum_name"': + enumName = propertyValue; + break; + case '"type"': + isEnum = propertyValue === '"ENUM"'; + isString = propertyValue === '"STRING"'; + break; + } + } }); + + if (isEnum && enumName === undefined) { + diagnostics.push({ + from: child.from, + message: '"enum_name" is required for ENUM type.', + severity: 'error', + to: child.to, + }); + } else if (!isEnum && enumName !== undefined) { + diagnostics.push({ + from: child.from, + message: `"enum_name": ${enumName} is not required for non-ENUM type.`, + severity: 'error', + to: child.to, + }); + } else if (isString && allowableRanges !== undefined) { + diagnostics.push({ + from: child.from, + message: `'allowable_ranges' is not required for STRING type.`, + severity: 'error', + to: child.to, + }); + } } } + child = child.nextSibling; } - child = child.nextSibling; - } - }); + }); - return { - diagnostics, - variables, - }; -} + return { + diagnostics, + variables, + }; + } -function validateCustomDirectives(node: SyntaxNode, text: string): Diagnostic[] { - const diagnostics: Diagnostic[] = []; - node.getChildren('GenericDirective').forEach(directiveNode => { - const child = directiveNode.firstChild; - // use first token as directive, preserve remainder of line - const { from, to } = { ...getFromAndTo([directiveNode]), ...(child ? { to: child.from } : {}) }; - const custom = text.slice(from, to).trim(); - const guess = closest(custom, KNOWN_DIRECTIVES); - const insert = guess + (child ? ' ' : '\n'); - diagnostics.push({ - actions: [ - { - apply(view, from, to) { - view.dispatch({ changes: { from, insert, to } }); + function validateCustomDirectives(node: SyntaxNode, text: string): Diagnostic[] { + const diagnostics: Diagnostic[] = []; + node.getChildren('GenericDirective').forEach(directiveNode => { + const child = directiveNode.firstChild; + // use first token as directive, preserve remainder of line + const { from, to } = { ...getFromAndTo([directiveNode]), ...(child ? { to: child.from } : {}) }; + const custom = text.slice(from, to).trim(); + const guess = closest(custom, KNOWN_DIRECTIVES); + const insert = guess + (child ? ' ' : '\n'); + diagnostics.push({ + actions: [ + { + apply(view, from, to) { + view.dispatch({ changes: { from, insert, to } }); + }, + name: `Change to ${guess}`, }, - name: `Change to ${guess}`, - }, - ], - from, - message: `Unknown Directive ${custom}, did you mean ${guess}`, - severity: 'error', - to, + ], + from, + message: `Unknown Directive ${custom}, did you mean ${guess}`, + severity: 'error', + to, + }); }); - }); - return diagnostics; -} - -function insertAction(name: string, insert: string) { - return { - apply(view: EditorView, from: number) { - view.dispatch({ changes: { from, insert } }); - }, - name, - }; -} + return diagnostics; + } -/** - * Function to generate diagnostics based on Commands section in the parse tree. - * - * @param {SyntaxNode[] | undefined} commandNodes - nodes representing commands - * @param {string} text - the text to validate against - * @return {Diagnostic[]} an array of diagnostics - */ -function commandLinter( - commandNodes: SyntaxNode[] | undefined, - text: string, - variables: VariableMap, - commandDictionary: CommandDictionary | null, - channelDictionary: ChannelDictionary | null, - parameterDictionaries: ParameterDictionary[], -): Diagnostic[] { - // If there are no command nodes, return an empty array of diagnostics - if (!commandNodes) { - return []; + function insertAction(name: string, insert: string) { + return { + apply(view: EditorView, from: number, _to: number) { + view.dispatch({ changes: { from, insert } }); + }, + name, + }; } - // Initialize an empty array to hold diagnostics - const diagnostics: Diagnostic[] = []; + /** + * Function to generate diagnostics based on Commands section in the parse tree. + * + * @param {SyntaxNode[] | undefined} commandNodes - nodes representing commands + * @param {string} text - the text to validate against + * @return {Diagnostic[]} an array of diagnostics + */ + function commandLinter(commandNodes: SyntaxNode[] | undefined, text: string, variables: VariableMap): Diagnostic[] { + // If there are no command nodes, return an empty array of diagnostics + if (!commandNodes) { + return []; + } - // Iterate over each command node - for (const command of commandNodes) { - // Get the TimeTag node for the current command - diagnostics.push(...validateTimeTags(command, text)); + // Initialize an empty array to hold diagnostics + const diagnostics: Diagnostic[] = []; - // Validate the command and push the generated diagnostics to the array - diagnostics.push( - ...validateCommand( - command, - text, - 'command', - variables, - commandDictionary, - channelDictionary, - parameterDictionaries, - ), - ); + // Iterate over each command node + for (const command of commandNodes) { + // Get the TimeTag node for the current command + diagnostics.push(...validateTimeTags(command, text)); + + // Validate the command and push the generated diagnostics to the array + diagnostics.push(...validateCommand(command, text, 'command', variables)); + + // Lint the metadata and models + diagnostics.push(...validateMetadata(command)); + diagnostics.push(...validateModel(command)); + } - // Lint the metadata and models - diagnostics.push(...validateMetadata(command)); - diagnostics.push(...validateModel(command)); + // Return the array of diagnostics + return diagnostics; } - // Return the array of diagnostics - return diagnostics; -} + function validateTimeTags(command: SyntaxNode, text: string): Diagnostic[] { + const diagnostics: Diagnostic[] = []; + const timeTagNode = command.getChild('TimeTag'); -function validateTimeTags(command: SyntaxNode, text: string): Diagnostic[] { - const diagnostics: Diagnostic[] = []; - const timeTagNode = command.getChild('TimeTag'); - - // If the TimeTag node is missing, create a diagnostic - if (!timeTagNode) { - diagnostics.push({ - actions: [insertAction(`Insert 'C' (command complete)`, 'C '), insertAction(`Insert 'R1' (relative 1)`, 'R ')], - from: command.from, - message: "Missing 'Time Tag' for command", - severity: 'error', - to: command.to, - }); - } else { - // Commands can't have a ground epoch time tag - if (command.name === TOKEN_COMMAND && timeTagNode.getChild('TimeGroundEpoch')) { + // If the TimeTag node is missing, create a diagnostic + if (!timeTagNode) { diagnostics.push({ - actions: [], - from: timeTagNode.from, - message: 'Ground Epoch Time Tags are not allowed for commands', + actions: [insertAction(`Insert 'C' (command complete)`, 'C '), insertAction(`Insert 'R1' (relative 1)`, 'R ')], + from: command.from, + message: "Missing 'Time Tag' for command", severity: 'error', - to: timeTagNode.to, + to: command.to, }); - } - - const timeTagAbsoluteNode = timeTagNode?.getChild('TimeAbsolute'); - const timeTagEpochNode = timeTagNode?.getChild('TimeEpoch') ?? timeTagNode.getChild('TimeGroundEpoch'); - const timeTagRelativeNode = timeTagNode?.getChild('TimeRelative'); - - if (timeTagAbsoluteNode) { - const absoluteText = text.slice(timeTagAbsoluteNode.from + 1, timeTagAbsoluteNode.to).trim(); - - const isValid = validateTime(absoluteText, TimeTypes.ABSOLUTE); - if (!isValid) { + } else { + // Commands can't have a ground epoch time tag + if (command.name === TOKEN_COMMAND && timeTagNode.getChild('TimeGroundEpoch')) { diagnostics.push({ actions: [], - from: timeTagAbsoluteNode.from, - message: CustomErrorCodes.InvalidAbsoluteTime().message, + from: timeTagNode.from, + message: 'Ground Epoch Time Tags are not allowed for commands', severity: 'error', - to: timeTagAbsoluteNode.to, + to: timeTagNode.to, }); - } else { - if (isTimeMax(absoluteText, TimeTypes.ABSOLUTE)) { + } + + const timeTagAbsoluteNode = timeTagNode?.getChild('TimeAbsolute'); + const timeTagEpochNode = timeTagNode?.getChild('TimeEpoch') ?? timeTagNode.getChild('TimeGroundEpoch'); + const timeTagRelativeNode = timeTagNode?.getChild('TimeRelative'); + + if (timeTagAbsoluteNode) { + const absoluteText = text.slice(timeTagAbsoluteNode.from + 1, timeTagAbsoluteNode.to).trim(); + + const isValid = validateTime(absoluteText, TimeTypes.ABSOLUTE); + if (!isValid) { diagnostics.push({ actions: [], from: timeTagAbsoluteNode.from, - message: CustomErrorCodes.MaxAbsoluteTime().message, + message: CustomErrorCodes.InvalidAbsoluteTime().message, severity: 'error', to: timeTagAbsoluteNode.to, }); } else { - if (!isTimeBalanced(absoluteText, TimeTypes.ABSOLUTE)) { + if (isTimeMax(absoluteText, TimeTypes.ABSOLUTE)) { diagnostics.push({ actions: [], from: timeTagAbsoluteNode.from, - message: CustomErrorCodes.UnbalancedTime(getDoyTime(new Date(getUnixEpochTime(absoluteText)))).message, - severity: 'warning', - to: timeTagAbsoluteNode.to, - }); - } - } - } - } else if (timeTagEpochNode) { - const epochText = text.slice(timeTagEpochNode.from + 1, timeTagEpochNode.to).trim(); - const isValid = validateTime(epochText, TimeTypes.EPOCH) || validateTime(epochText, TimeTypes.EPOCH_SIMPLE); - if (!isValid) { - diagnostics.push({ - actions: [], - from: timeTagEpochNode.from, - message: CustomErrorCodes.InvalidEpochTime().message, - severity: 'error', - to: timeTagEpochNode.to, - }); - } else { - if (validateTime(epochText, TimeTypes.EPOCH)) { - if (isTimeMax(epochText, TimeTypes.EPOCH)) { - diagnostics.push({ - actions: [], - from: timeTagEpochNode.from, - message: CustomErrorCodes.MaxEpochTime(parseDurationString(epochText, 'seconds').isNegative).message, + message: CustomErrorCodes.MaxAbsoluteTime().message, severity: 'error', - to: timeTagEpochNode.to, + to: timeTagAbsoluteNode.to, }); } else { - if (!isTimeBalanced(epochText, TimeTypes.EPOCH)) { + if (!isTimeBalanced(absoluteText, TimeTypes.ABSOLUTE)) { diagnostics.push({ actions: [], - from: timeTagEpochNode.from, - message: CustomErrorCodes.UnbalancedTime(getBalancedDuration(epochText)).message, + from: timeTagAbsoluteNode.from, + message: CustomErrorCodes.UnbalancedTime(getDoyTime(new Date(getUnixEpochTime(absoluteText)))).message, severity: 'warning', - to: timeTagEpochNode.to, + to: timeTagAbsoluteNode.to, }); } } } - } - } else if (timeTagRelativeNode) { - const relativeText = text.slice(timeTagRelativeNode.from + 1, timeTagRelativeNode.to).trim(); - const isValid = - validateTime(relativeText, TimeTypes.RELATIVE) || validateTime(relativeText, TimeTypes.RELATIVE_SIMPLE); - if (!isValid) { - diagnostics.push({ - actions: [], - from: timeTagRelativeNode.from, - message: CustomErrorCodes.InvalidRelativeTime().message, - severity: 'error', - to: timeTagRelativeNode.to, - }); - } else { - if (validateTime(relativeText, TimeTypes.RELATIVE)) { - if (isTimeMax(relativeText, TimeTypes.RELATIVE)) { - diagnostics.push({ - actions: [], - from: timeTagRelativeNode.from, - message: CustomErrorCodes.MaxRelativeTime().message, - severity: 'error', - to: timeTagRelativeNode.to, - }); - } else { - if (!isTimeBalanced(relativeText, TimeTypes.EPOCH)) { - diagnostics.push({ - actions: [], + } else if (timeTagEpochNode) { + const epochText = text.slice(timeTagEpochNode.from + 1, timeTagEpochNode.to).trim(); + const isValid = validateTime(epochText, TimeTypes.EPOCH) || validateTime(epochText, TimeTypes.EPOCH_SIMPLE); + if (!isValid) { + diagnostics.push({ + actions: [], + from: timeTagEpochNode.from, + message: CustomErrorCodes.InvalidEpochTime().message, + severity: 'error', + to: timeTagEpochNode.to, + }); + } else { + if (validateTime(epochText, TimeTypes.EPOCH)) { + if (isTimeMax(epochText, TimeTypes.EPOCH)) { + diagnostics.push({ + actions: [], + from: timeTagEpochNode.from, + message: CustomErrorCodes.MaxEpochTime(parseDurationString(epochText, 'seconds').isNegative).message, + severity: 'error', + to: timeTagEpochNode.to, + }); + } else { + if (!isTimeBalanced(epochText, TimeTypes.EPOCH)) { + diagnostics.push({ + actions: [], + from: timeTagEpochNode.from, + message: CustomErrorCodes.UnbalancedTime(getBalancedDuration(epochText)).message, + severity: 'warning', + to: timeTagEpochNode.to, + }); + } + } + } + } + } else if (timeTagRelativeNode) { + const relativeText = text.slice(timeTagRelativeNode.from + 1, timeTagRelativeNode.to).trim(); + const isValid = + validateTime(relativeText, TimeTypes.RELATIVE) || validateTime(relativeText, TimeTypes.RELATIVE_SIMPLE); + if (!isValid) { + diagnostics.push({ + actions: [], + from: timeTagRelativeNode.from, + message: CustomErrorCodes.InvalidRelativeTime().message, + severity: 'error', + to: timeTagRelativeNode.to, + }); + } else { + if (validateTime(relativeText, TimeTypes.RELATIVE)) { + if (isTimeMax(relativeText, TimeTypes.RELATIVE)) { + diagnostics.push({ + actions: [], from: timeTagRelativeNode.from, - message: CustomErrorCodes.UnbalancedTime(getBalancedDuration(relativeText)).message, + message: CustomErrorCodes.MaxRelativeTime().message, severity: 'error', to: timeTagRelativeNode.to, }); + } else { + if (!isTimeBalanced(relativeText, TimeTypes.EPOCH)) { + diagnostics.push({ + actions: [], + from: timeTagRelativeNode.from, + message: CustomErrorCodes.UnbalancedTime(getBalancedDuration(relativeText)).message, + severity: 'error', + to: timeTagRelativeNode.to, + }); + } } } } } } + return diagnostics; } - return diagnostics; -} -/** - * Function to generate diagnostics for immediate commands in the parse tree. - * - * @param {SyntaxNode[] | undefined} commandNodes - Array of command nodes or undefined. - * @param {string} text - Text of the sequence. - * @return {Diagnostic[]} Array of diagnostics. - */ -function immediateCommandLinter( - commandNodes: SyntaxNode[] | undefined, - text: string, - variables: VariableMap, - commandDictionary: CommandDictionary | null, - channelDictionary: ChannelDictionary | null, - parameterDictionaries: ParameterDictionary[], -): Diagnostic[] { - // If there are no command nodes, return the empty array - if (!commandNodes) { - return []; - } - // Initialize an array to hold diagnostics + /** + * Function to generate diagnostics for immediate commands in the parse tree. + * + * @param {SyntaxNode[] | undefined} commandNodes - Array of command nodes or undefined. + * @param {string} text - Text of the sequence. + * @return {Diagnostic[]} Array of diagnostics. + */ + function immediateCommandLinter( + commandNodes: SyntaxNode[] | undefined, + text: string, + variables: VariableMap, + ): Diagnostic[] { + // If there are no command nodes, return the empty array + if (!commandNodes) { + return []; + } + // Initialize an array to hold diagnostics - const diagnostics: Diagnostic[] = []; + const diagnostics: Diagnostic[] = []; - // Iterate over each command node - for (const command of commandNodes) { - // Get the TimeTag node for the current command - const timeTagNode = command.getChild('TimeTag'); + // Iterate over each command node + for (const command of commandNodes) { + // Get the TimeTag node for the current command + const timeTagNode = command.getChild('TimeTag'); - // If the TimeTag node exists, create a diagnostic - if (timeTagNode) { - diagnostics.push({ - actions: [], - from: command.from, - message: "Immediate commands can't have a time tag", - severity: 'error', - to: command.to, - }); - } + // If the TimeTag node exists, create a diagnostic + if (timeTagNode) { + diagnostics.push({ + actions: [], + from: command.from, + message: "Immediate commands can't have a time tag", + severity: 'error', + to: command.to, + }); + } - // Validate the command and push the generated diagnostics to the array - diagnostics.push( - ...validateCommand( - command, - text, - 'immediate', - variables, - commandDictionary, - channelDictionary, - parameterDictionaries, - ), - ); + // Validate the command and push the generated diagnostics to the array + diagnostics.push(...validateCommand(command, text, 'immediate', variables)); - // Lint the metadata - diagnostics.push(...validateMetadata(command)); + // Lint the metadata + diagnostics.push(...validateMetadata(command)); - // immediate commands don't have models - const modelsNode = command.getChild('Models'); - if (modelsNode) { - diagnostics.push({ - from: modelsNode.from, - message: "Immediate commands can't have models", - severity: 'error', - to: modelsNode.to, - }); + // immediate commands don't have models + const modelsNode = command.getChild('Models'); + if (modelsNode) { + diagnostics.push({ + from: modelsNode.from, + message: "Immediate commands can't have models", + severity: 'error', + to: modelsNode.to, + }); + } } - } - - // Return the array of diagnostics - return diagnostics; -} -/** - * Function to generate diagnostics based on HardwareCommands section in the parse tree. - * - * @param {SyntaxNode[] | undefined} commands - nodes representing hardware commands - * @param {string} text - the text to validate against - * @return {Diagnostic[]} an array of diagnostics - */ -function hardwareCommandLinter( - commands: SyntaxNode[] | undefined, - text: string, - commandDictionary: CommandDictionary | null, - channelDictionary: ChannelDictionary | null, - parameterDictionaries: ParameterDictionary[], -): Diagnostic[] { - // Initialize an empty array to hold diagnostics - const diagnostics: Diagnostic[] = []; - - // If there are no command nodes, return an empty array of diagnostics - if (!commands) { + // Return the array of diagnostics return diagnostics; } - // Iterate over each command node - for (const command of commands) { - // Get the TimeTag node for the current command - const timeTag = command.getChild('TimeTag'); - - // If the TimeTag node exists, create a diagnostic - if (timeTag) { - // Push a diagnostic to the array indicating that time tags are not allowed for hardware commands - diagnostics.push({ - actions: [], - from: command.from, - message: 'Time tag is not allowed for hardware commands', - severity: 'error', - to: command.to, - }); + /** + * Function to generate diagnostics based on HardwareCommands section in the parse tree. + * + * @param {SyntaxNode[] | undefined} commands - nodes representing hardware commands + * @param {string} text - the text to validate against + * @return {Diagnostic[]} an array of diagnostics + */ + function hardwareCommandLinter(commands: SyntaxNode[] | undefined, text: string): Diagnostic[] { + // Initialize an empty array to hold diagnostics + const diagnostics: Diagnostic[] = []; + + // If there are no command nodes, return an empty array of diagnostics + if (!commands) { + return diagnostics; } - // Validate the command and push the generated diagnostics to the array - diagnostics.push( - ...validateCommand(command, text, 'hardware', {}, commandDictionary, channelDictionary, parameterDictionaries), - ); + // Iterate over each command node + for (const command of commands) { + // Get the TimeTag node for the current command + const timeTag = command.getChild('TimeTag'); + + // If the TimeTag node exists, create a diagnostic + if (timeTag) { + // Push a diagnostic to the array indicating that time tags are not allowed for hardware commands + diagnostics.push({ + actions: [], + from: command.from, + message: 'Time tag is not allowed for hardware commands', + severity: 'error', + to: command.to, + }); + } - // Lint the metadata - diagnostics.push(...validateMetadata(command)); + // Validate the command and push the generated diagnostics to the array + diagnostics.push(...validateCommand(command, text, 'hardware', {})); - // hardware commands don't have models - const modelsNode = command.getChild('Models'); - if (modelsNode) { - diagnostics.push({ - actions: [], - from: modelsNode.from, - message: "Immediate commands can't have models", - severity: 'error', - to: modelsNode.to, - }); + // Lint the metadata + diagnostics.push(...validateMetadata(command)); + + // hardware commands don't have models + const modelsNode = command.getChild('Models'); + if (modelsNode) { + diagnostics.push({ + actions: [], + from: modelsNode.from, + message: "Immediate commands can't have models", + severity: 'error', + to: modelsNode.to, + }); + } } + + // Return the array of diagnostics + return diagnostics; } - // Return the array of diagnostics - return diagnostics; -} + /** + * Validates a command by validating its stem and arguments. + * + * @param command - The SyntaxNode representing the command. + * @param text - The text of the whole command. + * @returns An array of Diagnostic objects representing the validation errors. + */ + function validateCommand( + command: SyntaxNode, + text: string, + type: 'command' | 'immediate' | 'hardware' = 'command', + variables: VariableMap, + ): Diagnostic[] { + // If the command dictionary is not initialized, return an empty array of diagnostics. + if (!commandDictionary) { + return []; + } -/** - * Validates a command by validating its stem and arguments. - * - * @param command - The SyntaxNode representing the command. - * @param text - The text of the whole command. - * @returns An array of Diagnostic objects representing the validation errors. - */ -function validateCommand( - command: SyntaxNode, - text: string, - type: 'command' | 'immediate' | 'hardware' = 'command', - variables: VariableMap, - commandDictionary: CommandDictionary | null, - channelDictionary: ChannelDictionary | null, - parameterDictionaries: ParameterDictionary[], -): Diagnostic[] { - // If the command dictionary is not initialized, return an empty array of diagnostics. - if (!commandDictionary) { - return []; - } + // Get the stem node of the command. + const stem = command.getChild('Stem'); + // If the stem node is null, return an empty array of diagnostics. + if (stem === null) { + return []; + } - // Get the stem node of the command. - const stem = command.getChild('Stem'); - // If the stem node is null, return an empty array of diagnostics. - if (stem === null) { - return []; - } + const stemText = text.slice(stem.from, stem.to); - const stemText = text.slice(stem.from, stem.to); + // Initialize an array to store the diagnostic errors. + const diagnostics: Diagnostic[] = []; - // Initialize an array to store the diagnostic errors. - const diagnostics: Diagnostic[] = []; + // Validate the stem of the command. + const result = validateStem(stem, stemText, type); + // No command dictionary return []. + if (result === null) { + return []; + } - // Validate the stem of the command. - const result = validateStem(stem, stemText, type, commandDictionary); - // No command dictionary return []. - if (result === null) { - return []; - } + // Stem was invalid. + else if (typeof result === 'object' && 'message' in result) { + diagnostics.push(result); + return diagnostics; + } - // Stem was invalid. - else if (typeof result === 'object' && 'message' in result) { - diagnostics.push(result); - return diagnostics; - } + const argNode = command.getChild('Args'); + const dictArgs = (result as FswCommand).arguments ?? []; - const argNode = command.getChild('Args'); - const dictArgs = (result as FswCommand).arguments ?? []; - - // Lint the arguments of the command. - diagnostics.push( - ...validateAndLintArguments( - dictArgs, - argNode ? getChildrenNode(argNode) : null, - command, - text, - stemText, - variables, - commandDictionary, - channelDictionary, - parameterDictionaries, - ), - ); - - // Return the array of diagnostics. - return diagnostics; -} + // Lint the arguments of the command. + diagnostics.push( + ...validateAndLintArguments( + dictArgs, + argNode ? getChildrenNode(argNode) : null, + command, + text, + stemText, + variables, + ), + ); -/** - * Validates the stem of a command. - * @param stem - The SyntaxNode representing the stem of the command. - * @param stemText - The command name - * @param type - The type of command (default: 'command'). - * @returns A Diagnostic if the stem is invalid, a FswCommand if the stem is valid, or null if the command dictionary is not initialized. - */ -function validateStem( - stem: SyntaxNode, - stemText: string, - type: 'command' | 'immediate' | 'hardware' = 'command', - commandDictionary: CommandDictionary | null, -): Diagnostic | FswCommand | HwCommand | null { - if (commandDictionary === null) { - return null; + // Return the array of diagnostics. + return diagnostics; } - const { fswCommandMap, fswCommands, hwCommandMap, hwCommands } = commandDictionary; - const dictionaryCommand: FswCommand | HwCommand | null = fswCommandMap[stemText] - ? fswCommandMap[stemText] - : hwCommandMap[stemText] - ? hwCommandMap[stemText] - : null; + /** + * Validates the stem of a command. + * @param stem - The SyntaxNode representing the stem of the command. + * @param stemText - The command name + * @param type - The type of command (default: 'command'). + * @returns A Diagnostic if the stem is invalid, a FswCommand if the stem is valid, or null if the command dictionary is not initialized. + */ + function validateStem( + stem: SyntaxNode, + stemText: string, + type: 'command' | 'immediate' | 'hardware' = 'command', + ): Diagnostic | FswCommand | HwCommand | null { + if (commandDictionary === null) { + return null; + } + const { fswCommandMap, fswCommands, hwCommandMap, hwCommands } = commandDictionary; + + const dictionaryCommand: FswCommand | HwCommand | null = fswCommandMap[stemText] + ? fswCommandMap[stemText] + : hwCommandMap[stemText] + ? hwCommandMap[stemText] + : null; + + if (!dictionaryCommand) { + const ALL_STEMS = [...fswCommands.map(cmd => cmd.stem), ...hwCommands.map(cmd => cmd.stem)]; + return { + actions: closestStrings(stemText.toUpperCase(), ALL_STEMS, 3).map(guess => ({ + apply(view, from, to) { + view.dispatch({ changes: { from, insert: guess, to } }); + }, + name: `Change to ${guess}`, + })), + from: stem.from, + message: `Command '${stemText}' not found`, + severity: 'error', + to: stem.to, + }; + } - if (!dictionaryCommand) { - const ALL_STEMS = [...fswCommands.map(cmd => cmd.stem), ...hwCommands.map(cmd => cmd.stem)]; - return { - actions: closestStrings(stemText.toUpperCase(), ALL_STEMS, 3).map(guess => ({ - apply(view, from, to) { - view.dispatch({ changes: { from, insert: guess, to } }); - }, - name: `Change to ${guess}`, - })), - from: stem.from, - message: `Command '${stemText}' not found`, - severity: 'error', - to: stem.to, - }; + switch (type) { + case 'command': + case 'immediate': + if (!fswCommandMap[stemText]) { + return { + from: stem.from, + message: 'Command must be a fsw command', + severity: 'error', + to: stem.to, + }; + } + break; + case 'hardware': + if (!hwCommandMap[stemText]) { + return { + from: stem.from, + message: 'Command must be a hardware command', + severity: 'error', + to: stem.to, + }; + } + break; + } + + return dictionaryCommand; } - switch (type) { - case 'command': - case 'immediate': - if (!fswCommandMap[stemText]) { - return { - from: stem.from, - message: 'Command must be a fsw command', + /** + * Validates and lints the command arguments based on the dictionary of command arguments. + * @param dictArgs - The dictionary of command arguments. + * @param argNode - The SyntaxNode representing the arguments of the command. + * @param command - The SyntaxNode representing the command. + * @param text - The text of the document. + * @returns An array of Diagnostic objects representing the validation errors. + */ + function validateAndLintArguments( + dictArgs: FswCommandArgument[], + argNode: SyntaxNode[] | null, + command: SyntaxNode, + text: string, + stem: string, + variables: VariableMap, + ): Diagnostic[] { + // Initialize an array to store the validation errors + let diagnostics: Diagnostic[] = []; + + // Validate argument presence based on dictionary definition + if (dictArgs.length > 0) { + if (!argNode || argNode.length === 0) { + diagnostics.push({ + actions: [], + from: command.from, + message: 'The command is missing arguments.', severity: 'error', - to: stem.to, - }; + to: command.to, + }); + return diagnostics; } - break; - case 'hardware': - if (!hwCommandMap[stemText]) { - return { - from: stem.from, - message: 'Command must be a hardware command', + + if (argNode.length > dictArgs.length) { + const extraArgs = argNode.slice(dictArgs.length); + const { from, to } = getFromAndTo(extraArgs); + diagnostics.push({ + actions: [ + { + apply(view, from, to) { + view.dispatch({ changes: { from, to } }); + }, + name: `Remove ${extraArgs.length} extra argument${extraArgs.length > 1 ? 's' : ''}`, + }, + ], + from, + message: `Extra arguments, definition has ${dictArgs.length}, but ${argNode.length} are present`, severity: 'error', - to: stem.to, - }; + to, + }); + return diagnostics; + } else if (argNode.length < dictArgs.length) { + const { from, to } = getFromAndTo(argNode); + const pluralS = dictArgs.length > argNode.length + 1 ? 's' : ''; + diagnostics.push({ + actions: [ + { + apply(view, _from, _to) { + if (commandDictionary) { + addDefaultArgs(commandDictionary, view, command, dictArgs.slice(argNode.length)); + } + }, + name: `Add default missing argument${pluralS}`, + }, + ], + from, + message: `Missing argument${pluralS}, definition has ${argNode.length}, but ${dictArgs.length} are present`, + severity: 'error', + to, + }); + return diagnostics; } - break; - } - - return dictionaryCommand; -} - -/** - * Validates and lints the command arguments based on the dictionary of command arguments. - * @param dictArgs - The dictionary of command arguments. - * @param argNode - The SyntaxNode representing the arguments of the command. - * @param command - The SyntaxNode representing the command. - * @param text - The text of the document. - * @returns An array of Diagnostic objects representing the validation errors. - */ -function validateAndLintArguments( - dictArgs: FswCommandArgument[], - argNode: SyntaxNode[] | null, - command: SyntaxNode, - text: string, - stem: string, - variables: VariableMap, - commandDictionary: CommandDictionary | null, - channelDictionary: ChannelDictionary | null, - parameterDictionaries: ParameterDictionary[], -): Diagnostic[] { - // Initialize an array to store the validation errors - let diagnostics: Diagnostic[] = []; - - // Validate argument presence based on dictionary definition - if (dictArgs.length > 0) { - if (!argNode || argNode.length === 0) { - diagnostics.push({ - actions: [], - from: command.from, - message: 'The command is missing arguments.', - severity: 'error', - to: command.to, - }); - return diagnostics; - } - - if (argNode.length > dictArgs.length) { - const extraArgs = argNode.slice(dictArgs.length); - const { from, to } = getFromAndTo(extraArgs); + } else if (argNode && argNode.length > 0) { + const { from, to } = getFromAndTo(argNode); diagnostics.push({ actions: [ { apply(view, from, to) { view.dispatch({ changes: { from, to } }); }, - name: `Remove ${extraArgs.length} extra argument${extraArgs.length > 1 ? 's' : ''}`, + name: `Remove argument${argNode.length > 1 ? 's' : ''}`, }, ], - from, - message: `Extra arguments, definition has ${dictArgs.length}, but ${argNode.length} are present`, + from: from, + message: 'The command should not have arguments', severity: 'error', - to, + to: to, }); return diagnostics; - } else if (argNode.length < dictArgs.length) { - const { from, to } = getFromAndTo(argNode); - const pluralS = dictArgs.length > argNode.length + 1 ? 's' : ''; - diagnostics.push({ - actions: [ - { - apply(view) { - if (commandDictionary) { - addDefaultArgs(commandDictionary, view, command, dictArgs.slice(argNode.length)); - } - }, - name: `Add default missing argument${pluralS}`, - }, - ], - from, - message: `Missing argument${pluralS}, definition has ${argNode.length}, but ${dictArgs.length} are present`, - severity: 'error', - to, - }); + } + + // don't check any further as there are no arguments in the command dictionary + if (dictArgs.length === 0) { return diagnostics; } - } else if (argNode && argNode.length > 0) { - const { from, to } = getFromAndTo(argNode); - diagnostics.push({ - actions: [ - { - apply(view, from, to) { - view.dispatch({ changes: { from, to } }); - }, - name: `Remove argument${argNode.length > 1 ? 's' : ''}`, - }, - ], - from: from, - message: 'The command should not have arguments', - severity: 'error', - to: to, - }); - return diagnostics; - } - // don't check any further as there are no arguments in the command dictionary - if (dictArgs.length === 0) { - return diagnostics; - } + const argValues = argNode?.map(arg => text.slice(arg.from, arg.to)) ?? []; - const argValues = argNode?.map(arg => text.slice(arg.from, arg.to)) ?? []; + // grab the first argument node + // let node = argNode?.firstChild ?? null; - // grab the first argument node - // let node = argNode?.firstChild ?? null; + // Iterate through the dictionary of command arguments + for (let i = 0; i < dictArgs.length; i++) { + const dictArg = dictArgs[i]; // Get the current dictionary argument + const arg = argNode?.[i]; // Get the current argument node + // Check if there are no more argument nodes + if (!arg) { + // Push a diagnostic error for missing argument + diagnostics.push({ + actions: [], + from: command.from, + message: `Missing argument #${i + 1}, '${dictArg.name}' of type '${dictArg.arg_type}'`, + severity: 'error', + to: command.to, + }); + break; + } - // Iterate through the dictionary of command arguments - for (let i = 0; i < dictArgs.length; i++) { - const dictArg = dictArgs[i]; // Get the current dictionary argument - const arg = argNode?.[i]; // Get the current argument node - // Check if there are no more argument nodes - if (!arg) { - // Push a diagnostic error for missing argument - diagnostics.push({ - actions: [], - from: command.from, - message: `Missing argument #${i + 1}, '${dictArg.name}' of type '${dictArg.arg_type}'`, - severity: 'error', - to: command.to, - }); - break; + // Validate and lint the current argument node + diagnostics = diagnostics.concat( + ...validateArgument(dictArg, arg, command, text, stem, argValues.slice(0, i), variables), + ); } - // Validate and lint the current argument node - diagnostics = diagnostics.concat( - ...validateArgument( - dictArg, - arg, - command, - text, - stem, - argValues.slice(0, i), - variables, - commandDictionary, - channelDictionary, - parameterDictionaries, - ), - ); + // Return the array of validation errors + return diagnostics; } - // Return the array of validation errors - return diagnostics; -} - -/** + /** + * Validates the given FSW command argument against the provided syntax node, + * and generates diagnostics if the validation fails. + * @@ -1091,476 +1073,465 @@ function validateAndLintArguments( + * @param text The full text of the document. + * @returns An array of diagnostics generated during the validation. + */ -function validateArgument( - dictArg: FswCommandArgument, - argNode: SyntaxNode, - command: SyntaxNode, - text: string, - stemText: string, - precedingArgValues: string[], - variables: VariableMap, - commandDictionary: CommandDictionary | null, - channelDictionary: ChannelDictionary | null, - parameterDictionaries: ParameterDictionary[], -): Diagnostic[] { - dictArg = getCustomArgDef(stemText, dictArg, precedingArgValues, parameterDictionaries, channelDictionary); - - const diagnostics: Diagnostic[] = []; - - const dictArgType = dictArg.arg_type; - const argType = argNode.name; - const argText = text.slice(argNode.from, argNode.to); - - switch (dictArgType) { - case 'enum': - if (argType === 'Enum') { - if (!variables[argText]) { - // TODO -- potentially check that variable types match usage - diagnostics.push({ - from: argNode.from, - message: `Unrecognized variable name ${argText}`, - severity: 'error', - to: argNode.to, - }); - } - } else if (argType !== 'String') { - diagnostics.push({ - actions: [], - from: argNode.from, - message: `Incorrect type - expected double quoted 'enum' but got ${argType}`, - severity: 'error', - to: argNode.to, - }); - } else { - if (commandDictionary) { - const symbols = getAllEnumSymbols(commandDictionary?.enumMap, dictArg.enum_name) ?? dictArg.range ?? []; - const unquotedArgText = argText.replace(/^"|"$/g, ''); - if (!symbols.includes(unquotedArgText)) { - const guess = closest(unquotedArgText.toUpperCase(), symbols); + function validateArgument( + dictArg: FswCommandArgument, + argNode: SyntaxNode, + command: SyntaxNode, + text: string, + stemText: string, + precedingArgValues: string[], + variables: VariableMap, + ): Diagnostic[] { + dictArg = getCustomArgDef(stemText, dictArg, precedingArgValues, parameterDictionaries, channelDictionary); + + const diagnostics: Diagnostic[] = []; + + const dictArgType = dictArg.arg_type; + const argType = argNode.name; + const argText = text.slice(argNode.from, argNode.to); + + switch (dictArgType) { + case 'enum': + if (argType === 'Enum') { + if (!variables[argText]) { + // TODO -- potentially check that variable types match usage diagnostics.push({ - actions: [ - { - apply(view, from, to) { - view.dispatch({ changes: { from, insert: `"${guess}"`, to } }); - }, - name: `Change to ${guess}`, - }, - ], from: argNode.from, - message: `Enum should be "${symbols.slice(0, MAX_ENUMS_TO_SHOW).join(' | ')}${symbols.length > MAX_ENUMS_TO_SHOW ? ' ...' : ''}"\n`, + message: `Unrecognized variable name ${argText}`, severity: 'error', to: argNode.to, }); - break; } - } - } - break; - case 'boolean': - if (argType !== 'Boolean') { - diagnostics.push({ - actions: [], - from: argNode.from, - message: `Incorrect type - expected 'Boolean' but got ${argType}`, - severity: 'error', - to: argNode.to, - }); - } - if (['true', 'false'].includes(argText) === false) { - diagnostics.push({ - actions: [], - from: argNode.from, - message: `Incorrect value - expected true or false but got ${argText}`, - severity: 'error', - to: argNode.to, - }); - } - break; - case 'float': - case 'integer': - case 'numeric': - case 'unsigned': - if (argType === 'Number') { - if (dictArg.range === null) { - break; - } - const { max, min } = dictArg.range; - const nodeTextAsNumber = parseFloat(argText); - - if (nodeTextAsNumber < min || nodeTextAsNumber > max) { - const message = - max !== min - ? `Number out of range. Range is between ${min} and ${max} inclusive.` - : `Number out of range. Range is ${min}.`; + } else if (argType !== 'String') { diagnostics.push({ - actions: - max === min - ? [ - { - apply(view, from, to) { - view.dispatch({ changes: { from, insert: `${min}`, to } }); - }, - name: `Change to ${min}`, - }, - ] - : [], + actions: [], from: argNode.from, - message, + message: `Incorrect type - expected double quoted 'enum' but got ${argType}`, severity: 'error', to: argNode.to, }); + } else { + if (commandDictionary) { + const symbols = getAllEnumSymbols(commandDictionary?.enumMap, dictArg.enum_name) ?? dictArg.range ?? []; + const unquotedArgText = argText.replace(/^"|"$/g, ''); + if (!symbols.includes(unquotedArgText)) { + const guess = closest(unquotedArgText.toUpperCase(), symbols); + diagnostics.push({ + actions: [ + { + apply(view, from, to) { + view.dispatch({ changes: { from, insert: `"${guess}"`, to } }); + }, + name: `Change to ${guess}`, + }, + ], + from: argNode.from, + message: `Enum should be "${symbols.slice(0, MAX_ENUMS_TO_SHOW).join(' | ')}${symbols.length > MAX_ENUMS_TO_SHOW ? ' ...' : ''}"\n`, + severity: 'error', + to: argNode.to, + }); + break; + } + } } - } else if (argType === 'Enum') { - if (!variables[argText]) { + break; + case 'boolean': + if (argType !== 'Boolean') { diagnostics.push({ + actions: [], from: argNode.from, - message: `Unrecognized variable name ${argText}`, + message: `Incorrect type - expected 'Boolean' but got ${argType}`, severity: 'error', to: argNode.to, }); } - } else { - diagnostics.push({ - from: argNode.from, - message: `Incorrect type - expected 'Number' but got ${argType}`, - severity: 'error', - to: argNode.to, - }); - } - break; - case 'fixed_string': - case 'var_string': - if (argType === 'Enum') { - if (!variables[argText]) { - const insert = closest(argText, Object.keys(variables)); + if (['true', 'false'].includes(argText) === false) { diagnostics.push({ - actions: [ - { - apply(view, from, to) { - view.dispatch({ changes: { from, insert, to } }); - }, - name: `Change to ${insert}`, - }, - ], + actions: [], from: argNode.from, - message: `Unrecognized variable name ${argText}`, + message: `Incorrect value - expected true or false but got ${argText}`, severity: 'error', to: argNode.to, }); } - } else if (argType !== 'String') { - diagnostics.push({ - from: argNode.from, - message: `Incorrect type - expected 'String' but got ${argType}`, - severity: 'error', - to: argNode.to, - }); - } - break; - case 'repeat': - if (argType !== TOKEN_REPEAT_ARG) { - diagnostics.push({ - from: argNode.from, - message: `Incorrect type - expected '${TOKEN_REPEAT_ARG}' but got ${argType}`, - severity: 'error', - to: argNode.to, - }); - } else { - const repeatNodes = argNode.getChildren('Arguments'); - const repeatDef = dictArg.repeat; - if (repeatDef) { - const repeatLength = repeatDef.arguments.length; - const minSets = repeatDef.min ?? 0; - const maxSets = repeatDef.max ?? Infinity; - const minCount = repeatLength * minSets; - const maxCount = repeatLength * maxSets; - if (minCount > repeatNodes.length) { + break; + case 'float': + case 'integer': + case 'numeric': + case 'unsigned': + if (argType === 'Number') { + if (dictArg.range === null) { + break; + } + const { max, min } = dictArg.range; + const nodeTextAsNumber = parseNumericArg(argText, dictArgType); + const isHex = isHexValue(argText); + if (nodeTextAsNumber < min || nodeTextAsNumber > max) { + const numFormat = (n: number) => (isHex ? `0x${n.toString(16).toUpperCase()}` : n); + const message = + max !== min + ? `Number out of range. Range is between ${numFormat(min)} and ${numFormat(max)} inclusive.` + : `Number out of range. Range is ${numFormat(min)}.`; diagnostics.push({ - actions: [], + actions: + max === min + ? [ + { + apply(view, from, to) { + view.dispatch({ changes: { from, insert: `${min}`, to } }); + }, + name: `Change to ${min}`, + }, + ] + : [], from: argNode.from, - message: `Repeat argument should have at least ${minCount} value${minCount !== 0 ? 's' : ''} but has ${ - repeatNodes.length - }`, + message, severity: 'error', to: argNode.to, }); - } else if (maxCount < repeatNodes.length) { + } + } else if (argType === 'Enum') { + if (!variables[argText]) { diagnostics.push({ - actions: [], from: argNode.from, - message: `Repeat argument should have at most ${maxCount} value${maxCount !== 0 ? 's' : ''} but has ${ - repeatNodes.length - }`, + message: `Unrecognized variable name ${argText}`, severity: 'error', to: argNode.to, }); - } else if (repeatNodes.length % repeatLength !== 0) { - const allowedValues: number[] = []; - for (let i = minSets; i <= Math.min(maxSets, minSets + 2); i++) { - allowedValues.push(i * repeatLength); - } - let showEllipses = false; - if (allowedValues.length) { - const lastVal = allowedValues[allowedValues.length - 1]; - if (maxCount > lastVal) { - if (maxCount > lastVal + repeatLength) { - showEllipses = true; - } - allowedValues.push(maxCount); - } - } - const valStrings = allowedValues.map(i => i.toString()); - if (showEllipses) { - valStrings.splice(allowedValues.length - 1, 0, '...'); - } - + } + } else { + diagnostics.push({ + from: argNode.from, + message: `Incorrect type - expected 'Number' but got ${argType}`, + severity: 'error', + to: argNode.to, + }); + } + break; + case 'fixed_string': + case 'var_string': + if (argType === 'Enum') { + if (!variables[argText]) { + const insert = closest(argText, Object.keys(variables)); diagnostics.push({ - actions: [], + actions: [ + { + apply(view, from, to) { + view.dispatch({ changes: { from, insert, to } }); + }, + name: `Change to ${insert}`, + }, + ], from: argNode.from, - message: `Repeat argument should have [${valStrings.join(', ')}] values`, + message: `Unrecognized variable name ${argText}`, severity: 'error', to: argNode.to, }); - } else { - repeatNodes - .reduce((acc, node, i) => { - const chunkIndex = Math.floor(i / repeatLength); - if (!acc[chunkIndex]) { - acc[chunkIndex] = []; - } - acc[chunkIndex].push(node); - return acc; - }, []) - .forEach((repeat: SyntaxNode[]) => { - // check individual args - diagnostics.push( - ...validateAndLintArguments( - repeatDef.arguments ?? [], - repeat, - command, - text, - stemText, - variables, - commandDictionary, - channelDictionary, - parameterDictionaries, - ), - ); - }); } - } - } - - break; - } - return diagnostics; -} - -function validateId(commandNode: SyntaxNode, text: string): Diagnostic[] { - const diagnostics: Diagnostic[] = []; - const idNodes = commandNode.getChildren('IdDeclaration'); - if (idNodes.length) { - const idNode = idNodes[0]; - const idValNode = idNode.firstChild; - if (idValNode?.name === 'Enum' || idValNode?.name === 'Number') { - const { from, to } = getFromAndTo([idValNode]); - const idVal = text.slice(from, to); - diagnostics.push({ - actions: idValNode - ? [ - { - apply(view, from, to) { - view.dispatch({ changes: { from, insert: quoteEscape(idVal), to } }); - }, - name: `Quote ${idVal}`, - }, - ] - : [], - from, - message: `@ID directives must include double quoted string e.g. '@ID "sequence.name"'`, - severity: 'error', - to, - }); - } else if (!idValNode) { - diagnostics.push({ - ...getFromAndTo([idNode]), - message: `@ID directives must include a double quoted string e.g. '@ID "sequence.name"`, - severity: 'error', - }); - } - } - diagnostics.push( - ...idNodes.slice(1).map( - idNode => - ({ - ...getFromAndTo([idNode]), - message: 'Only one @ID directive is allowed per sequence', - severity: 'error', - }) as const, - ), - ); - return diagnostics; -} - -/** - * Validates the metadata of a command node and returns an array of diagnostics. - * @param commandNode - The command node to validate. - * @returns An array of diagnostic objects. - */ -function validateMetadata(commandNode: SyntaxNode): Diagnostic[] { - // Get the metadata node of the command node - const metadataNode = commandNode.getChild('Metadata'); - // If there is no metadata node, return an empty array - if (!metadataNode) { - return []; - } - // Get the metadata entry nodes of the metadata node - const metadataEntry = metadataNode.getChildren('MetaEntry'); - // If there are no metadata entry nodes, return an empty array - if (!metadataEntry) { - return []; - } - - const diagnostics: Diagnostic[] = []; - - // Iterate over each metadata entry node - metadataEntry.forEach(entry => { - // Get the children nodes of the metadata entry node - const metadataNodeChildren = getChildrenNode(entry); - - if (metadataNodeChildren.length > 2) { - diagnostics.push({ - actions: [], - from: entry.from, - message: `Should only have a 'key' and a 'value'`, - severity: 'error', - to: entry.to, - }); - } else { - // Define the template for metadata nodes - const metadataTemplate = ['Key', 'Value']; - // Iterate over each template node - for (let i = 0; i < metadataTemplate.length; i++) { - // Get the name of the template node - const templateName = metadataTemplate[i]; - // Get the metadata node of the current template node - const metadataNode = metadataNodeChildren[i]; - - // If there is no metadata node, add a diagnostic - if (!metadataNode) { + } else if (argType !== 'String') { diagnostics.push({ - actions: [], - from: entry.from, - message: `Missing ${templateName}`, + from: argNode.from, + message: `Incorrect type - expected 'String' but got ${argType}`, severity: 'error', - to: entry.to, + to: argNode.to, }); - break; } - - // If the name of the metadata node is not the template node name - if (metadataNode.name !== templateName) { - // Get the name of the deepest node of the metadata node - const deepestNodeName = getDeepestNode(metadataNode).name; - // Add a diagnostic based on the name of the deepest node - switch (deepestNodeName) { - case 'String': - break; // do nothing as it is a string - case 'Number': - case 'Enum': - case 'Boolean': + break; + case 'repeat': + if (argType !== TOKEN_REPEAT_ARG) { + diagnostics.push({ + from: argNode.from, + message: `Incorrect type - expected '${TOKEN_REPEAT_ARG}' but got ${argType}`, + severity: 'error', + to: argNode.to, + }); + } else { + const repeatNodes = argNode.getChildren('Arguments'); + const repeatDef = dictArg.repeat; + if (repeatDef) { + const repeatLength = repeatDef.arguments.length; + const minSets = repeatDef.min ?? 0; + const maxSets = repeatDef.max ?? Infinity; + const minCount = repeatLength * minSets; + const maxCount = repeatLength * maxSets; + if (minCount > repeatNodes.length) { diagnostics.push({ - from: metadataNode.from, - message: `Incorrect type - expected 'String' but got ${deepestNodeName}`, + actions: [], + from: argNode.from, + message: `Repeat argument should have at least ${minCount} value${minCount !== 0 ? 's' : ''} but has ${ + repeatNodes.length + }`, severity: 'error', - to: metadataNode.to, + to: argNode.to, }); - break; - default: + } else if (maxCount < repeatNodes.length) { diagnostics.push({ - from: entry.from, - message: `Missing ${templateName}`, + actions: [], + from: argNode.from, + message: `Repeat argument should have at most ${maxCount} value${maxCount !== 0 ? 's' : ''} but has ${ + repeatNodes.length + }`, severity: 'error', - to: entry.to, + to: argNode.to, }); + } else if (repeatNodes.length % repeatLength !== 0) { + const allowedValues: number[] = []; + for (let i = minSets; i <= Math.min(maxSets, minSets + 2); i++) { + allowedValues.push(i * repeatLength); + } + let showEllipses = false; + if (allowedValues.length) { + const lastVal = allowedValues[allowedValues.length - 1]; + if (maxCount > lastVal) { + if (maxCount > lastVal + repeatLength) { + showEllipses = true; + } + allowedValues.push(maxCount); + } + } + const valStrings = allowedValues.map(i => i.toString()); + if (showEllipses) { + valStrings.splice(allowedValues.length - 1, 0, '...'); + } + + diagnostics.push({ + actions: [], + from: argNode.from, + message: `Repeat argument should have [${valStrings.join(', ')}] values`, + severity: 'error', + to: argNode.to, + }); + } else { + repeatNodes + .reduce((acc, node, i) => { + const chunkIndex = Math.floor(i / repeatLength); + if (!acc[chunkIndex]) { + acc[chunkIndex] = []; + } + acc[chunkIndex].push(node); + return acc; + }, []) + .forEach((repeat: SyntaxNode[]) => { + // check individual args + diagnostics.push( + ...validateAndLintArguments(repeatDef.arguments ?? [], repeat, command, text, stemText, variables), + ); + }); + } } } - } - } - }); - return diagnostics; -} + break; + } + return diagnostics; + } -function validateModel(commandNode: SyntaxNode): Diagnostic[] { - const models = commandNode.getChild('Models')?.getChildren('Model'); - if (!models) { - return []; + function validateId(commandNode: SyntaxNode, text: string): Diagnostic[] { + const diagnostics: Diagnostic[] = []; + const idNodes = commandNode.getChildren('IdDeclaration'); + if (idNodes.length) { + const idNode = idNodes[0]; + const idValNode = idNode.firstChild; + if (idValNode?.name === 'Enum' || idValNode?.name === 'Number') { + const { from, to } = getFromAndTo([idValNode]); + const idVal = text.slice(from, to); + diagnostics.push({ + actions: idValNode + ? [ + { + apply(view, from, to) { + view.dispatch({ changes: { from, insert: quoteEscape(idVal), to } }); + }, + name: `Quote ${idVal}`, + }, + ] + : [], + from, + message: `@ID directives must include double quoted string e.g. '@ID "sequence.name"'`, + severity: 'error', + to, + }); + } else if (!idValNode) { + diagnostics.push({ + ...getFromAndTo([idNode]), + message: `@ID directives must include a double quoted string e.g. '@ID "sequence.name"`, + severity: 'error', + }); + } + } + diagnostics.push( + ...idNodes.slice(1).map( + idNode => + ({ + ...getFromAndTo([idNode]), + message: 'Only one @ID directive is allowed per sequence', + severity: 'error', + }) as const, + ), + ); + return diagnostics; } - const diagnostics: Diagnostic[] = []; + /** + * Validates the metadata of a command node and returns an array of diagnostics. + * @param commandNode - The command node to validate. + * @returns An array of diagnostic objects. + */ + function validateMetadata(commandNode: SyntaxNode): Diagnostic[] { + // Get the metadata node of the command node + const metadataNode = commandNode.getChild('Metadata'); + // If there is no metadata node, return an empty array + if (!metadataNode) { + return []; + } + // Get the metadata entry nodes of the metadata node + const metadataEntry = metadataNode.getChildren('MetaEntry'); + // If there are no metadata entry nodes, return an empty array + if (!metadataEntry) { + return []; + } - models.forEach(model => { - const modelChildren = getChildrenNode(model); - if (modelChildren.length > 3) { - diagnostics.push({ - from: model.from, - message: `Should only have 'Variable', 'value', and 'Offset'`, - severity: 'error', - to: model.to, - }); - } else { - const modelTemplate = ['Variable', 'Value', 'Offset']; - for (let i = 0; i < modelTemplate.length; i++) { - const templateName = modelTemplate[i]; - const modelNode = modelChildren[i]; - if (!modelNode) { - diagnostics.push({ - from: model.from, - message: `Missing ${templateName}`, - severity: 'error', - to: model.to, - }); - } + const diagnostics: Diagnostic[] = []; - if (modelNode.name !== templateName) { - const deepestNodeName = getDeepestNode(modelNode).name; - if (deepestNodeName === TOKEN_ERROR) { + // Iterate over each metadata entry node + metadataEntry.forEach(entry => { + // Get the children nodes of the metadata entry node + const metadataNodeChildren = getChildrenNode(entry); + + if (metadataNodeChildren.length > 2) { + diagnostics.push({ + actions: [], + from: entry.from, + message: `Should only have a 'key' and a 'value'`, + severity: 'error', + to: entry.to, + }); + } else { + // Define the template for metadata nodes + const metadataTemplate = ['Key', 'Value']; + // Iterate over each template node + for (let i = 0; i < metadataTemplate.length; i++) { + // Get the name of the template node + const templateName = metadataTemplate[i]; + // Get the metadata node of the current template node + const metadataNode = metadataNodeChildren[i]; + + // If there is no metadata node, add a diagnostic + if (!metadataNode) { diagnostics.push({ - from: model.from, + actions: [], + from: entry.from, message: `Missing ${templateName}`, severity: 'error', - to: model.to, + to: entry.to, }); break; - } else { - if (templateName === 'Variable' || templateName === 'Offset') { - if (deepestNodeName !== 'String') { + } + + // If the name of the metadata node is not the template node name + if (metadataNode.name !== templateName) { + // Get the name of the deepest node of the metadata node + const deepestNodeName = getDeepestNode(metadataNode).name; + // Add a diagnostic based on the name of the deepest node + switch (deepestNodeName) { + case 'String': + break; // do nothing as it is a string + case 'Number': + case 'Enum': + case 'Boolean': diagnostics.push({ - from: modelNode.from, + from: metadataNode.from, message: `Incorrect type - expected 'String' but got ${deepestNodeName}`, severity: 'error', - to: modelNode.to, + to: metadataNode.to, }); break; - } - } else { - // Value - if (deepestNodeName !== 'Number' && deepestNodeName !== 'String' && deepestNodeName !== 'Boolean') { + default: diagnostics.push({ - from: modelNode.from, - message: `Incorrect type - expected 'Number', 'String', or 'Boolean' but got ${deepestNodeName}`, + from: entry.from, + message: `Missing ${templateName}`, severity: 'error', - to: modelNode.to, + to: entry.to, }); - break; - } } } } } + }); + + return diagnostics; + } + + function validateModel(commandNode: SyntaxNode): Diagnostic[] { + const models = commandNode.getChild('Models')?.getChildren('Model'); + if (!models) { + return []; } - }); - return diagnostics; + const diagnostics: Diagnostic[] = []; + + models.forEach(model => { + const modelChildren = getChildrenNode(model); + if (modelChildren.length > 3) { + diagnostics.push({ + from: model.from, + message: `Should only have 'Variable', 'value', and 'Offset'`, + severity: 'error', + to: model.to, + }); + } else { + const modelTemplate = ['Variable', 'Value', 'Offset']; + for (let i = 0; i < modelTemplate.length; i++) { + const templateName = modelTemplate[i]; + const modelNode = modelChildren[i]; + if (!modelNode) { + diagnostics.push({ + from: model.from, + message: `Missing ${templateName}`, + severity: 'error', + to: model.to, + }); + } + + if (modelNode.name !== templateName) { + const deepestNodeName = getDeepestNode(modelNode).name; + if (deepestNodeName === TOKEN_ERROR) { + diagnostics.push({ + from: model.from, + message: `Missing ${templateName}`, + severity: 'error', + to: model.to, + }); + break; + } else { + if (templateName === 'Variable' || templateName === 'Offset') { + if (deepestNodeName !== 'String') { + diagnostics.push({ + from: modelNode.from, + message: `Incorrect type - expected 'String' but got ${deepestNodeName}`, + severity: 'error', + to: modelNode.to, + }); + break; + } + } else { + // Value + if (deepestNodeName !== 'Number' && deepestNodeName !== 'String' && deepestNodeName !== 'Boolean') { + diagnostics.push({ + from: modelNode.from, + message: `Incorrect type - expected 'Number', 'String', or 'Boolean' but got ${deepestNodeName}`, + severity: 'error', + to: modelNode.to, + }); + break; + } + } + } + } + } + } + }); + + return diagnostics; + } } From 71f31e63ca082a5b40d74f67f93cb6b461a92926 Mon Sep 17 00:00:00 2001 From: Cody Hansen Date: Mon, 30 Sep 2024 09:09:49 -1000 Subject: [PATCH 2/5] Added Chet's block linting back in --- .../sequence-editor/sequence-linter.ts | 161 +++++------------- 1 file changed, 41 insertions(+), 120 deletions(-) diff --git a/src/utilities/sequence-editor/sequence-linter.ts b/src/utilities/sequence-editor/sequence-linter.ts index 2d3ff89741..e4a13fd046 100644 --- a/src/utilities/sequence-editor/sequence-linter.ts +++ b/src/utilities/sequence-editor/sequence-linter.ts @@ -1,6 +1,7 @@ import { syntaxTree } from '@codemirror/language'; import { linter, type Diagnostic } from '@codemirror/lint'; import type { Extension } from '@codemirror/state'; +import { EditorState } from '@codemirror/state'; import type { SyntaxNode, Tree } from '@lezer/common'; import type { ChannelDictionary, @@ -21,6 +22,7 @@ import { TimeTypes } from '../../enums/time'; import { getGlobals, sequenceAdaptation } from '../../stores/sequence-adaptation'; import { CustomErrorCodes } from '../../workers/customCodes'; import { addDefaultArgs, isHexValue, parseNumericArg, quoteEscape } from '../codemirror/codemirror-utils'; +import { closeSuggestion, computeBlocks, openSuggestion } from '../codemirror/custom-folder'; import { getBalancedDuration, getDoyTime, @@ -56,18 +58,6 @@ function closestStrings(value: string, potentialMatches: string[], n: number) { return distances.slice(0, n).map(pair => pair.s); } -type WhileOpener = { - command: SyntaxNode; - from: number; - stemToClose: string; - to: number; - word: string; -}; - -type IfOpener = WhileOpener & { - hasElse: boolean; -}; - type VariableMap = { [name: string]: VariableDeclaration; }; @@ -136,7 +126,7 @@ export function sequenceLinter( ); diagnostics.push( - ...conditionalAndLoopKeywordsLinter(treeNode.getChild('Commands')?.getChildren(TOKEN_COMMAND) || [], docText), + ...conditionalAndLoopKeywordsLinter(treeNode.getChild('Commands')?.getChildren(TOKEN_COMMAND) || [], view.state), ); const inputLinter = get(sequenceAdaptation)?.inputFormat.linter; @@ -173,118 +163,49 @@ export function sequenceLinter( return diagnostics; } - function conditionalAndLoopKeywordsLinter(commandNodes: SyntaxNode[], text: string): Diagnostic[] { + function conditionalAndLoopKeywordsLinter(commandNodes: SyntaxNode[], state: EditorState): Diagnostic[] { const diagnostics: Diagnostic[] = []; - const conditionalStack: IfOpener[] = []; - const loopStack: WhileOpener[] = []; - const conditionalKeywords = []; - const loopKeywords = []; - const sequenceAdaptationConditionalKeywords = get(sequenceAdaptation)?.conditionalKeywords; - const sequenceAdaptationLoopKeywords = get(sequenceAdaptation)?.loopKeywords; - - conditionalKeywords.push( - sequenceAdaptationConditionalKeywords?.else, - ...(sequenceAdaptationConditionalKeywords?.elseIf ?? []), - sequenceAdaptationConditionalKeywords?.endIf, - ); - loopKeywords.push( - sequenceAdaptationLoopKeywords?.break, - sequenceAdaptationLoopKeywords?.continue, - sequenceAdaptationLoopKeywords?.endWhileLoop, - ); - - for (const command of commandNodes) { - const stem = command.getChild('Stem'); - if (stem) { - const word = text.slice(stem.from, stem.to); + const blocks = computeBlocks(state); - if (sequenceAdaptationConditionalKeywords?.if.includes(word)) { - conditionalStack.push({ - command, - from: stem.from, - hasElse: false, - stemToClose: sequenceAdaptationConditionalKeywords.endIf, - to: stem.to, - word, + if (blocks) { + const pairs = Object.values(blocks); + pairs.forEach(pair => { + if (!pair.start && pair.end) { + const stem = state.sliceDoc(pair.end.from, pair.end.to); + diagnostics.push({ + from: pair.end.from, + message: `${stem} must match a preceding ${openSuggestion(stem)}`, + severity: 'error', + to: pair.end.to, }); - } - - if (conditionalKeywords.includes(word)) { - if (conditionalStack.length === 0) { - diagnostics.push({ - from: stem.from, - message: `${word} doesn't match a preceding ${sequenceAdaptationConditionalKeywords?.if.join(', ')}.`, - severity: 'error', - to: stem.to, - }); - } else if (word === sequenceAdaptationConditionalKeywords?.else) { - if (!conditionalStack[conditionalStack.length - 1].hasElse) { - conditionalStack[conditionalStack.length - 1].hasElse = true; - } else { - diagnostics.push({ - from: stem.from, - message: `${word} doesn't match a preceding ${sequenceAdaptationConditionalKeywords?.if.join(', ')}.`, - severity: 'error', - to: stem.to, - }); - } - } else if (word === sequenceAdaptationConditionalKeywords?.endIf) { - conditionalStack.pop(); - } - } - - if (sequenceAdaptationLoopKeywords?.whileLoop.includes(word)) { - loopStack.push({ - command, - from: stem.from, - stemToClose: sequenceAdaptationLoopKeywords.endWhileLoop, - to: stem.to, - word, + } else if (pair.start && !pair.end) { + const stem = state.sliceDoc(pair.start.from, pair.start.to); + const suggestion = closeSuggestion(stem); + diagnostics.push({ + actions: [ + { + apply(view: EditorView) { + if (pair.start?.parent) { + view.dispatch({ + changes: { + from: pair.start?.parent.to, + insert: `\nC ${suggestion}\n`, + }, + }); + } + }, + name: `Insert ${suggestion}`, + }, + ], + from: pair.start.from, + message: `Block opened by ${stem} is not closed`, + severity: 'error', + to: pair.start.to, }); } - - if (loopKeywords.includes(word)) { - if (loopStack.length === 0) { - diagnostics.push({ - from: stem.from, - message: `${word} doesn't match a preceding ${sequenceAdaptationLoopKeywords?.whileLoop.join(', ')}.`, - severity: 'error', - to: stem.to, - }); - } - - if (word === sequenceAdaptationLoopKeywords?.endWhileLoop) { - loopStack.pop(); - } - } - } + }); } - // Anything left on the stack is unclosed - diagnostics.push( - ...[...loopStack, ...conditionalStack].map(block => { - return { - actions: [ - { - apply(view: EditorView, _from: number, _to: number) { - view.dispatch({ - changes: { - from: block.command.to, - insert: `\nC ${block.stemToClose}\n`, - }, - }); - }, - name: `Insert ${block.stemToClose}`, - }, - ], - from: block.from, - message: `Unclosed ${block.word}`, - severity: 'error', - to: block.to, - } as const; - }), - ); - return diagnostics; } @@ -518,7 +439,7 @@ export function sequenceLinter( function insertAction(name: string, insert: string) { return { - apply(view: EditorView, from: number, _to: number) { + apply(view: EditorView, from: number) { view.dispatch({ changes: { from, insert } }); }, name, @@ -992,7 +913,7 @@ export function sequenceLinter( diagnostics.push({ actions: [ { - apply(view, _from, _to) { + apply(view) { if (commandDictionary) { addDefaultArgs(commandDictionary, view, command, dictArgs.slice(argNode.length)); } From 4ec0a1f4b9133c0d47faa9c13cee68afdd93e800 Mon Sep 17 00:00:00 2001 From: Cody Hansen Date: Mon, 30 Sep 2024 10:42:04 -1000 Subject: [PATCH 3/5] Refactored the sequence-linter again --- .../sequence-editor/sequence-linter.ts | 2391 +++++++++-------- 1 file changed, 1246 insertions(+), 1145 deletions(-) diff --git a/src/utilities/sequence-editor/sequence-linter.ts b/src/utilities/sequence-editor/sequence-linter.ts index e4a13fd046..e043aaf569 100644 --- a/src/utilities/sequence-editor/sequence-linter.ts +++ b/src/utilities/sequence-editor/sequence-linter.ts @@ -1,6 +1,5 @@ import { syntaxTree } from '@codemirror/language'; -import { linter, type Diagnostic } from '@codemirror/lint'; -import type { Extension } from '@codemirror/state'; +import { type Diagnostic } from '@codemirror/lint'; import { EditorState } from '@codemirror/state'; import type { SyntaxNode, Tree } from '@lezer/common'; import type { @@ -16,10 +15,9 @@ import { closest, distance } from 'fastest-levenshtein'; import type { VariableDeclaration } from '@nasa-jpl/seq-json-schema/types'; import type { EditorView } from 'codemirror'; -import { get } from 'svelte/store'; import { TOKEN_COMMAND, TOKEN_ERROR, TOKEN_REPEAT_ARG, TOKEN_REQUEST } from '../../constants/seq-n-grammar-constants'; import { TimeTypes } from '../../enums/time'; -import { getGlobals, sequenceAdaptation } from '../../stores/sequence-adaptation'; +import { getGlobals } from '../../stores/sequence-adaptation'; import { CustomErrorCodes } from '../../workers/customCodes'; import { addDefaultArgs, isHexValue, parseNumericArg, quoteEscape } from '../codemirror/codemirror-utils'; import { closeSuggestion, computeBlocks, openSuggestion } from '../codemirror/custom-folder'; @@ -67,924 +65,1015 @@ type VariableMap = { * Can be optionally called with a command dictionary so it's available during linting. */ export function sequenceLinter( + view: EditorView, channelDictionary: ChannelDictionary | null = null, commandDictionary: CommandDictionary | null = null, parameterDictionaries: ParameterDictionary[] = [], -): Extension { - return linter(view => { - const tree = syntaxTree(view.state); - const treeNode = tree.topNode; - const docText = view.state.doc.toString(); - let diagnostics: Diagnostic[] = []; +): Diagnostic[] { + const tree = syntaxTree(view.state); + const treeNode = tree.topNode; + const docText = view.state.doc.toString(); + const diagnostics: Diagnostic[] = []; - diagnostics.push(...validateParserErrors(tree)); + diagnostics.push(...validateParserErrors(tree)); - // TODO: Get identify type mapping to use - const variables: VariableDeclaration[] = [ - ...(getGlobals().map(g => ({ name: g.name, type: 'STRING' }) as const) ?? []), - ]; + // TODO: Get identify type mapping to use + const variables: VariableDeclaration[] = [ + ...(getGlobals().map(g => ({ name: g.name, type: 'STRING' }) as const) ?? []), + ]; - // Validate top level metadata - diagnostics.push(...validateMetadata(treeNode)); + // Validate top level metadata + diagnostics.push(...validateMetadata(treeNode)); - diagnostics.push(...validateId(treeNode, docText)); + diagnostics.push(...validateId(treeNode, docText)); - const localsValidation = validateLocals(treeNode.getChildren('LocalDeclaration'), docText); - variables.push(...localsValidation.variables); - diagnostics.push(...localsValidation.diagnostics); + const localsValidation = validateLocals(treeNode.getChildren('LocalDeclaration'), docText); + variables.push(...localsValidation.variables); + diagnostics.push(...localsValidation.diagnostics); - const parameterValidation = validateParameters(treeNode.getChildren('ParameterDeclaration'), docText); - variables.push(...parameterValidation.variables); - diagnostics.push(...parameterValidation.diagnostics); + const parameterValidation = validateParameters(treeNode.getChildren('ParameterDeclaration'), docText); + variables.push(...parameterValidation.variables); + diagnostics.push(...parameterValidation.diagnostics); - const variableMap: VariableMap = {}; - for (const variable of variables) { - variableMap[variable.name] = variable; - } - - // Validate command type mixing - diagnostics.push(...validateCommandTypeMixing(treeNode)); + const variableMap: VariableMap = {}; + for (const variable of variables) { + variableMap[variable.name] = variable; + } - diagnostics.push(...validateCustomDirectives(treeNode, docText)); + // Validate command type mixing + diagnostics.push(...validateCommandTypeMixing(treeNode)); - const commandsNode = treeNode.getChild('Commands'); - if (commandsNode) { - diagnostics.push(...commandLinter(commandsNode.getChildren(TOKEN_COMMAND), docText, variableMap)); - diagnostics.push(...validateRequests(commandsNode.getChildren(TOKEN_REQUEST), docText, variableMap)); - } + diagnostics.push(...validateCustomDirectives(treeNode, docText)); + const commandsNode = treeNode.getChild('Commands'); + if (commandsNode) { diagnostics.push( - ...immediateCommandLinter( - treeNode.getChild('ImmediateCommands')?.getChildren(TOKEN_COMMAND) || [], + ...commandLinter( + commandsNode.getChildren(TOKEN_COMMAND), docText, variableMap, + commandDictionary, + channelDictionary, + parameterDictionaries, ), ); - - diagnostics.push( - ...hardwareCommandLinter(treeNode.getChild('HardwareCommands')?.getChildren(TOKEN_COMMAND) || [], docText), - ); - diagnostics.push( - ...conditionalAndLoopKeywordsLinter(treeNode.getChild('Commands')?.getChildren(TOKEN_COMMAND) || [], view.state), + ...validateRequests( + commandsNode.getChildren(TOKEN_REQUEST), + docText, + variableMap, + commandDictionary, + channelDictionary, + parameterDictionaries, + ), ); + } - const inputLinter = get(sequenceAdaptation)?.inputFormat.linter; - - if (inputLinter !== undefined && commandDictionary !== null) { - diagnostics = inputLinter(diagnostics, commandDictionary, view, treeNode); - } + diagnostics.push( + ...immediateCommandLinter( + treeNode.getChild('ImmediateCommands')?.getChildren(TOKEN_COMMAND) || [], + docText, + variableMap, + commandDictionary, + channelDictionary, + parameterDictionaries, + ), + ); + + diagnostics.push( + ...hardwareCommandLinter( + treeNode.getChild('HardwareCommands')?.getChildren(TOKEN_COMMAND) || [], + docText, + commandDictionary, + channelDictionary, + parameterDictionaries, + ), + ); + + diagnostics.push( + ...conditionalAndLoopKeywordsLinter(treeNode.getChild('Commands')?.getChildren(TOKEN_COMMAND) || [], view.state), + ); + + return diagnostics; +} - return diagnostics; +/** + * Checks for unexpected tokens. + * + * @param tree + * @returns + */ +function validateParserErrors(tree: Tree) { + const diagnostics: Diagnostic[] = []; + const MAX_PARSER_ERRORS = 100; + tree.iterate({ + enter: node => { + if (node.name === TOKEN_ERROR && diagnostics.length < MAX_PARSER_ERRORS) { + const { from, to } = node; + diagnostics.push({ + from, + message: `Unexpected token`, + severity: 'error', + to, + }); + } + }, }); + return diagnostics; +} - /** - * Checks for unexpected tokens. - * - * @param tree - * @returns - */ - function validateParserErrors(tree: Tree) { - const diagnostics: Diagnostic[] = []; - const MAX_PARSER_ERRORS = 100; - tree.iterate({ - enter: node => { - if (node.name === TOKEN_ERROR && diagnostics.length < MAX_PARSER_ERRORS) { - const { from, to } = node; - diagnostics.push({ - from, - message: `Unexpected token`, - severity: 'error', - to, - }); - } - }, - }); - return diagnostics; - } - - function conditionalAndLoopKeywordsLinter(commandNodes: SyntaxNode[], state: EditorState): Diagnostic[] { - const diagnostics: Diagnostic[] = []; - const blocks = computeBlocks(state); +function conditionalAndLoopKeywordsLinter(commandNodes: SyntaxNode[], state: EditorState): Diagnostic[] { + const diagnostics: Diagnostic[] = []; + const blocks = computeBlocks(state); - if (blocks) { - const pairs = Object.values(blocks); - pairs.forEach(pair => { - if (!pair.start && pair.end) { - const stem = state.sliceDoc(pair.end.from, pair.end.to); - diagnostics.push({ - from: pair.end.from, - message: `${stem} must match a preceding ${openSuggestion(stem)}`, - severity: 'error', - to: pair.end.to, - }); - } else if (pair.start && !pair.end) { - const stem = state.sliceDoc(pair.start.from, pair.start.to); - const suggestion = closeSuggestion(stem); - diagnostics.push({ - actions: [ - { - apply(view: EditorView) { - if (pair.start?.parent) { - view.dispatch({ - changes: { - from: pair.start?.parent.to, - insert: `\nC ${suggestion}\n`, - }, - }); - } - }, - name: `Insert ${suggestion}`, + if (blocks) { + const pairs = Object.values(blocks); + pairs.forEach(pair => { + if (!pair.start && pair.end) { + const stem = state.sliceDoc(pair.end.from, pair.end.to); + diagnostics.push({ + from: pair.end.from, + message: `${stem} must match a preceding ${openSuggestion(stem)}`, + severity: 'error', + to: pair.end.to, + }); + } else if (pair.start && !pair.end) { + const stem = state.sliceDoc(pair.start.from, pair.start.to); + const suggestion = closeSuggestion(stem); + diagnostics.push({ + actions: [ + { + apply(view: EditorView) { + if (pair.start?.parent) { + view.dispatch({ + changes: { + from: pair.start?.parent.to, + insert: `\nC ${suggestion}\n`, + }, + }); + } }, - ], - from: pair.start.from, - message: `Block opened by ${stem} is not closed`, - severity: 'error', - to: pair.start.to, - }); - } - }); - } - - return diagnostics; + name: `Insert ${suggestion}`, + }, + ], + from: pair.start.from, + message: `Block opened by ${stem} is not closed`, + severity: 'error', + to: pair.start.to, + }); + } + }); } - function validateRequests(requestNodes: SyntaxNode[], text: string, variables: VariableMap): Diagnostic[] { - const diagnostics: Diagnostic[] = []; + return diagnostics; +} - for (const request of requestNodes) { - // Get the TimeTag node for the current command - diagnostics.push(...validateTimeTags(request, text)); - } +function validateRequests( + requestNodes: SyntaxNode[], + text: string, + variables: VariableMap, + commandDictionary: CommandDictionary | null, + channelDictionary: ChannelDictionary | null, + parameterDictionaries: ParameterDictionary[], +): Diagnostic[] { + const diagnostics: Diagnostic[] = []; + + for (const request of requestNodes) { + // Get the TimeTag node for the current command + diagnostics.push(...validateTimeTags(request, text)); + } - diagnostics.push( - ...requestNodes.flatMap(request => - commandLinter(request.getChild('Steps')?.getChildren(TOKEN_COMMAND) ?? [], text, variables), + diagnostics.push( + ...requestNodes.flatMap(request => + commandLinter( + request.getChild('Steps')?.getChildren(TOKEN_COMMAND) ?? [], + text, + variables, + commandDictionary, + channelDictionary, + parameterDictionaries, ), - ); + ), + ); - return diagnostics; - } + return diagnostics; +} - /** - * Validates that a syntax node does not mix different command types. - * - * @param {SyntaxNode} node - The syntax node to validate. - * @return {Diagnostic[]} An array of diagnostics. - */ - function validateCommandTypeMixing(node: SyntaxNode): Diagnostic[] { - // Get the child nodes for Commands, ImmediateCommands, and HardwareCommands. - const commands = node.getChild('Commands'); - const immediateCommands = node.getChild('ImmediateCommands'); - const hardwareCommands = node.getChild('HardwareCommands'); - const lgo = commands?.getChild('LoadAndGoDirective') ?? null; - - // Check if each command type exists and has at least one child node. - const hasCommands = commands !== null && (commands?.getChildren(TOKEN_COMMAND).length > 0 || lgo !== null); - const hasImmediateCommands = immediateCommands !== null; - const hasHardwareCommands = hardwareCommands !== null; - - const diagnostics: Diagnostic[] = []; - - // Get the start. - const { from, to } = getFromAndTo([commands, immediateCommands, hardwareCommands]); - - // If there is a mix of command types, push a diagnostic. - if ( - (hasCommands && (hasImmediateCommands || hasHardwareCommands)) || - (hasImmediateCommands && hasHardwareCommands) - ) { - if (lgo) { - diagnostics.push({ - from, - message: `Directive 'LOAD_AND_GO' cannot be used with 'Immediate Commands' or 'Hardware Commands'.`, - severity: 'error', - to, - }); - } +/** + * Validates that a syntax node does not mix different command types. + * + * @param {SyntaxNode} node - The syntax node to validate. + * @return {Diagnostic[]} An array of diagnostics. + */ +function validateCommandTypeMixing(node: SyntaxNode): Diagnostic[] { + // Get the child nodes for Commands, ImmediateCommands, and HardwareCommands. + const commands = node.getChild('Commands'); + const immediateCommands = node.getChild('ImmediateCommands'); + const hardwareCommands = node.getChild('HardwareCommands'); + const lgo = commands?.getChild('LoadAndGoDirective') ?? null; + + // Check if each command type exists and has at least one child node. + const hasCommands = commands !== null && (commands?.getChildren(TOKEN_COMMAND).length > 0 || lgo !== null); + const hasImmediateCommands = immediateCommands !== null; + const hasHardwareCommands = hardwareCommands !== null; + + const diagnostics: Diagnostic[] = []; + + // Get the start. + const { from, to } = getFromAndTo([commands, immediateCommands, hardwareCommands]); + + // If there is a mix of command types, push a diagnostic. + if ((hasCommands && (hasImmediateCommands || hasHardwareCommands)) || (hasImmediateCommands && hasHardwareCommands)) { + if (lgo) { diagnostics.push({ from, - message: 'Cannot mix different command types in one Sequence.', + message: `Directive 'LOAD_AND_GO' cannot be used with 'Immediate Commands' or 'Hardware Commands'.`, severity: 'error', to, }); } - return diagnostics; + diagnostics.push({ + from, + message: 'Cannot mix different command types in one Sequence.', + severity: 'error', + to, + }); } + return diagnostics; +} - function validateLocals(locals: SyntaxNode[], text: string) { - const variables: VariableDeclaration[] = []; - const diagnostics: Diagnostic[] = []; - diagnostics.push( - ...locals.slice(1).map( - local => - ({ - ...getFromAndTo([local]), - message: 'There is a maximum of one @LOCALS directive per sequence', - severity: 'error', - }) as const, - ), - ); - locals.forEach(local => { - let child = local.firstChild; - while (child) { - if (child.name !== 'Enum') { - diagnostics.push({ - from: child.from, - message: `@LOCALS values are required to be Enums`, - severity: 'error', - to: child.to, - }); - } else { - variables.push({ - name: text.slice(child.from, child.to), - // TODO - hook to check mission specific nomenclature - type: 'STRING', - }); - } - child = child.nextSibling; +function validateLocals(locals: SyntaxNode[], text: string) { + const variables: VariableDeclaration[] = []; + const diagnostics: Diagnostic[] = []; + diagnostics.push( + ...locals.slice(1).map( + local => + ({ + ...getFromAndTo([local]), + message: 'There is a maximum of one @LOCALS directive per sequence', + severity: 'error', + }) as const, + ), + ); + locals.forEach(local => { + let child = local.firstChild; + while (child) { + if (child.name !== 'Enum') { + diagnostics.push({ + from: child.from, + message: `@LOCALS values are required to be Enums`, + severity: 'error', + to: child.to, + }); + } else { + variables.push({ + name: text.slice(child.from, child.to), + // TODO - hook to check mission specific nomenclature + type: 'STRING', + }); } - }); - return { - diagnostics, - variables, - }; - } + child = child.nextSibling; + } + }); + return { + diagnostics, + variables, + }; +} - function validateParameters(inputParams: SyntaxNode[], text: string) { - const variables: VariableDeclaration[] = []; - const diagnostics: Diagnostic[] = []; - diagnostics.push( - ...inputParams.slice(1).map( - inputParam => - ({ - ...getFromAndTo([inputParam]), - message: 'There is a maximum of @INPUT_PARAMS directive per sequence', - severity: 'error', - }) as const, - ), - ); +function validateParameters(inputParams: SyntaxNode[], text: string) { + const variables: VariableDeclaration[] = []; + const diagnostics: Diagnostic[] = []; + diagnostics.push( + ...inputParams.slice(1).map( + inputParam => + ({ + ...getFromAndTo([inputParam]), + message: 'There is a maximum of @INPUT_PARAMS directive per sequence', + severity: 'error', + }) as const, + ), + ); - inputParams.forEach(inputParam => { - let child = inputParam.firstChild; + inputParams.forEach(inputParam => { + let child = inputParam.firstChild; - while (child) { - if (child.name !== 'Enum' && child.name !== 'Object') { - diagnostics.push({ - from: child.from, - message: `@INPUT_PARAMS values are required to be Enums`, - severity: 'error', - to: child.to, - }); - } else { - const variable = { - name: text.slice(child.from, child.to), - type: 'STRING', - } as VariableDeclaration; - - variables.push(variable); - - const metadata: SyntaxNode | null = child?.nextSibling; - - if (metadata !== null) { - const properties = metadata.getChildren('Property'); - let allowableRanges: string | undefined = undefined; - let isEnum = false; - let isString = false; - let enumName: string | undefined = undefined; - - properties.forEach(property => { - const propertyNameNode = property.getChild('PropertyName'); - const propertyValueNode = propertyNameNode?.nextSibling; - - if (propertyNameNode !== null && propertyValueNode !== null && propertyValueNode !== undefined) { - const propertyName = text.slice(propertyNameNode.from, propertyNameNode.to); - const propertyValue = text.slice(propertyValueNode.from, propertyValueNode.to); - - switch (propertyName.toLowerCase()) { - case '"allowable_ranges"': - allowableRanges = propertyValue; - break; - case '"enum_name"': - enumName = propertyValue; - break; - case '"type"': - isEnum = propertyValue === '"ENUM"'; - isString = propertyValue === '"STRING"'; - break; - } - } - }); + while (child) { + if (child.name !== 'Enum' && child.name !== 'Object') { + diagnostics.push({ + from: child.from, + message: `@INPUT_PARAMS values are required to be Enums`, + severity: 'error', + to: child.to, + }); + } else { + const variable = { + name: text.slice(child.from, child.to), + type: 'STRING', + } as VariableDeclaration; - if (isEnum && enumName === undefined) { - diagnostics.push({ - from: child.from, - message: '"enum_name" is required for ENUM type.', - severity: 'error', - to: child.to, - }); - } else if (!isEnum && enumName !== undefined) { - diagnostics.push({ - from: child.from, - message: `"enum_name": ${enumName} is not required for non-ENUM type.`, - severity: 'error', - to: child.to, - }); - } else if (isString && allowableRanges !== undefined) { - diagnostics.push({ - from: child.from, - message: `'allowable_ranges' is not required for STRING type.`, - severity: 'error', - to: child.to, - }); + variables.push(variable); + + const metadata: SyntaxNode | null = child?.nextSibling; + + if (metadata !== null) { + const properties = metadata.getChildren('Property'); + let allowableRanges: string | undefined = undefined; + let isEnum = false; + let isString = false; + let enumName: string | undefined = undefined; + + properties.forEach(property => { + const propertyNameNode = property.getChild('PropertyName'); + const propertyValueNode = propertyNameNode?.nextSibling; + + if (propertyNameNode !== null && propertyValueNode !== null && propertyValueNode !== undefined) { + const propertyName = text.slice(propertyNameNode.from, propertyNameNode.to); + const propertyValue = text.slice(propertyValueNode.from, propertyValueNode.to); + + switch (propertyName.toLowerCase()) { + case '"allowable_ranges"': + allowableRanges = propertyValue; + break; + case '"enum_name"': + enumName = propertyValue; + break; + case '"type"': + isEnum = propertyValue === '"ENUM"'; + isString = propertyValue === '"STRING"'; + break; + } } + }); + + if (isEnum && enumName === undefined) { + diagnostics.push({ + from: child.from, + message: '"enum_name" is required for ENUM type.', + severity: 'error', + to: child.to, + }); + } else if (!isEnum && enumName !== undefined) { + diagnostics.push({ + from: child.from, + message: `"enum_name": ${enumName} is not required for non-ENUM type.`, + severity: 'error', + to: child.to, + }); + } else if (isString && allowableRanges !== undefined) { + diagnostics.push({ + from: child.from, + message: `'allowable_ranges' is not required for STRING type.`, + severity: 'error', + to: child.to, + }); } } - child = child.nextSibling; } - }); + child = child.nextSibling; + } + }); - return { - diagnostics, - variables, - }; - } + return { + diagnostics, + variables, + }; +} - function validateCustomDirectives(node: SyntaxNode, text: string): Diagnostic[] { - const diagnostics: Diagnostic[] = []; - node.getChildren('GenericDirective').forEach(directiveNode => { - const child = directiveNode.firstChild; - // use first token as directive, preserve remainder of line - const { from, to } = { ...getFromAndTo([directiveNode]), ...(child ? { to: child.from } : {}) }; - const custom = text.slice(from, to).trim(); - const guess = closest(custom, KNOWN_DIRECTIVES); - const insert = guess + (child ? ' ' : '\n'); - diagnostics.push({ - actions: [ - { - apply(view, from, to) { - view.dispatch({ changes: { from, insert, to } }); - }, - name: `Change to ${guess}`, +function validateCustomDirectives(node: SyntaxNode, text: string): Diagnostic[] { + const diagnostics: Diagnostic[] = []; + node.getChildren('GenericDirective').forEach(directiveNode => { + const child = directiveNode.firstChild; + // use first token as directive, preserve remainder of line + const { from, to } = { ...getFromAndTo([directiveNode]), ...(child ? { to: child.from } : {}) }; + const custom = text.slice(from, to).trim(); + const guess = closest(custom, KNOWN_DIRECTIVES); + const insert = guess + (child ? ' ' : '\n'); + diagnostics.push({ + actions: [ + { + apply(view, from, to) { + view.dispatch({ changes: { from, insert, to } }); }, - ], - from, - message: `Unknown Directive ${custom}, did you mean ${guess}`, - severity: 'error', - to, - }); + name: `Change to ${guess}`, + }, + ], + from, + message: `Unknown Directive ${custom}, did you mean ${guess}`, + severity: 'error', + to, }); - return diagnostics; - } - - function insertAction(name: string, insert: string) { - return { - apply(view: EditorView, from: number) { - view.dispatch({ changes: { from, insert } }); - }, - name, - }; - } + }); + return diagnostics; +} - /** - * Function to generate diagnostics based on Commands section in the parse tree. - * - * @param {SyntaxNode[] | undefined} commandNodes - nodes representing commands - * @param {string} text - the text to validate against - * @return {Diagnostic[]} an array of diagnostics - */ - function commandLinter(commandNodes: SyntaxNode[] | undefined, text: string, variables: VariableMap): Diagnostic[] { - // If there are no command nodes, return an empty array of diagnostics - if (!commandNodes) { - return []; - } +function insertAction(name: string, insert: string) { + return { + apply(view: EditorView, from: number) { + view.dispatch({ changes: { from, insert } }); + }, + name, + }; +} - // Initialize an empty array to hold diagnostics - const diagnostics: Diagnostic[] = []; +/** + * Function to generate diagnostics based on Commands section in the parse tree. + * + * @param {SyntaxNode[] | undefined} commandNodes - nodes representing commands + * @param {string} text - the text to validate against + * @return {Diagnostic[]} an array of diagnostics + */ +function commandLinter( + commandNodes: SyntaxNode[] | undefined, + text: string, + variables: VariableMap, + commandDictionary: CommandDictionary | null, + channelDictionary: ChannelDictionary | null, + parameterDictionaries: ParameterDictionary[], +): Diagnostic[] { + // If there are no command nodes, return an empty array of diagnostics + if (!commandNodes) { + return []; + } - // Iterate over each command node - for (const command of commandNodes) { - // Get the TimeTag node for the current command - diagnostics.push(...validateTimeTags(command, text)); + // Initialize an empty array to hold diagnostics + const diagnostics: Diagnostic[] = []; - // Validate the command and push the generated diagnostics to the array - diagnostics.push(...validateCommand(command, text, 'command', variables)); + // Iterate over each command node + for (const command of commandNodes) { + // Get the TimeTag node for the current command + diagnostics.push(...validateTimeTags(command, text)); - // Lint the metadata and models - diagnostics.push(...validateMetadata(command)); - diagnostics.push(...validateModel(command)); - } + // Validate the command and push the generated diagnostics to the array + diagnostics.push( + ...validateCommand( + command, + text, + 'command', + variables, + commandDictionary, + channelDictionary, + parameterDictionaries, + ), + ); - // Return the array of diagnostics - return diagnostics; + // Lint the metadata and models + diagnostics.push(...validateMetadata(command)); + diagnostics.push(...validateModel(command)); } - function validateTimeTags(command: SyntaxNode, text: string): Diagnostic[] { - const diagnostics: Diagnostic[] = []; - const timeTagNode = command.getChild('TimeTag'); + // Return the array of diagnostics + return diagnostics; +} - // If the TimeTag node is missing, create a diagnostic - if (!timeTagNode) { +function validateTimeTags(command: SyntaxNode, text: string): Diagnostic[] { + const diagnostics: Diagnostic[] = []; + const timeTagNode = command.getChild('TimeTag'); + + // If the TimeTag node is missing, create a diagnostic + if (!timeTagNode) { + diagnostics.push({ + actions: [insertAction(`Insert 'C' (command complete)`, 'C '), insertAction(`Insert 'R1' (relative 1)`, 'R ')], + from: command.from, + message: "Missing 'Time Tag' for command", + severity: 'error', + to: command.to, + }); + } else { + // Commands can't have a ground epoch time tag + if (command.name === TOKEN_COMMAND && timeTagNode.getChild('TimeGroundEpoch')) { diagnostics.push({ - actions: [insertAction(`Insert 'C' (command complete)`, 'C '), insertAction(`Insert 'R1' (relative 1)`, 'R ')], - from: command.from, - message: "Missing 'Time Tag' for command", + actions: [], + from: timeTagNode.from, + message: 'Ground Epoch Time Tags are not allowed for commands', severity: 'error', - to: command.to, + to: timeTagNode.to, }); - } else { - // Commands can't have a ground epoch time tag - if (command.name === TOKEN_COMMAND && timeTagNode.getChild('TimeGroundEpoch')) { + } + + const timeTagAbsoluteNode = timeTagNode?.getChild('TimeAbsolute'); + const timeTagEpochNode = timeTagNode?.getChild('TimeEpoch') ?? timeTagNode.getChild('TimeGroundEpoch'); + const timeTagRelativeNode = timeTagNode?.getChild('TimeRelative'); + + if (timeTagAbsoluteNode) { + const absoluteText = text.slice(timeTagAbsoluteNode.from + 1, timeTagAbsoluteNode.to).trim(); + + const isValid = validateTime(absoluteText, TimeTypes.ABSOLUTE); + if (!isValid) { diagnostics.push({ actions: [], - from: timeTagNode.from, - message: 'Ground Epoch Time Tags are not allowed for commands', + from: timeTagAbsoluteNode.from, + message: CustomErrorCodes.InvalidAbsoluteTime().message, severity: 'error', - to: timeTagNode.to, + to: timeTagAbsoluteNode.to, }); - } - - const timeTagAbsoluteNode = timeTagNode?.getChild('TimeAbsolute'); - const timeTagEpochNode = timeTagNode?.getChild('TimeEpoch') ?? timeTagNode.getChild('TimeGroundEpoch'); - const timeTagRelativeNode = timeTagNode?.getChild('TimeRelative'); - - if (timeTagAbsoluteNode) { - const absoluteText = text.slice(timeTagAbsoluteNode.from + 1, timeTagAbsoluteNode.to).trim(); - - const isValid = validateTime(absoluteText, TimeTypes.ABSOLUTE); - if (!isValid) { + } else { + if (isTimeMax(absoluteText, TimeTypes.ABSOLUTE)) { diagnostics.push({ actions: [], from: timeTagAbsoluteNode.from, - message: CustomErrorCodes.InvalidAbsoluteTime().message, + message: CustomErrorCodes.MaxAbsoluteTime().message, severity: 'error', to: timeTagAbsoluteNode.to, }); } else { - if (isTimeMax(absoluteText, TimeTypes.ABSOLUTE)) { + if (!isTimeBalanced(absoluteText, TimeTypes.ABSOLUTE)) { diagnostics.push({ actions: [], from: timeTagAbsoluteNode.from, - message: CustomErrorCodes.MaxAbsoluteTime().message, - severity: 'error', + message: CustomErrorCodes.UnbalancedTime(getDoyTime(new Date(getUnixEpochTime(absoluteText)))).message, + severity: 'warning', to: timeTagAbsoluteNode.to, }); - } else { - if (!isTimeBalanced(absoluteText, TimeTypes.ABSOLUTE)) { - diagnostics.push({ - actions: [], - from: timeTagAbsoluteNode.from, - message: CustomErrorCodes.UnbalancedTime(getDoyTime(new Date(getUnixEpochTime(absoluteText)))).message, - severity: 'warning', - to: timeTagAbsoluteNode.to, - }); - } } } - } else if (timeTagEpochNode) { - const epochText = text.slice(timeTagEpochNode.from + 1, timeTagEpochNode.to).trim(); - const isValid = validateTime(epochText, TimeTypes.EPOCH) || validateTime(epochText, TimeTypes.EPOCH_SIMPLE); - if (!isValid) { - diagnostics.push({ - actions: [], - from: timeTagEpochNode.from, - message: CustomErrorCodes.InvalidEpochTime().message, - severity: 'error', - to: timeTagEpochNode.to, - }); - } else { - if (validateTime(epochText, TimeTypes.EPOCH)) { - if (isTimeMax(epochText, TimeTypes.EPOCH)) { + } + } else if (timeTagEpochNode) { + const epochText = text.slice(timeTagEpochNode.from + 1, timeTagEpochNode.to).trim(); + const isValid = validateTime(epochText, TimeTypes.EPOCH) || validateTime(epochText, TimeTypes.EPOCH_SIMPLE); + if (!isValid) { + diagnostics.push({ + actions: [], + from: timeTagEpochNode.from, + message: CustomErrorCodes.InvalidEpochTime().message, + severity: 'error', + to: timeTagEpochNode.to, + }); + } else { + if (validateTime(epochText, TimeTypes.EPOCH)) { + if (isTimeMax(epochText, TimeTypes.EPOCH)) { + diagnostics.push({ + actions: [], + from: timeTagEpochNode.from, + message: CustomErrorCodes.MaxEpochTime(parseDurationString(epochText, 'seconds').isNegative).message, + severity: 'error', + to: timeTagEpochNode.to, + }); + } else { + if (!isTimeBalanced(epochText, TimeTypes.EPOCH)) { diagnostics.push({ actions: [], from: timeTagEpochNode.from, - message: CustomErrorCodes.MaxEpochTime(parseDurationString(epochText, 'seconds').isNegative).message, - severity: 'error', + message: CustomErrorCodes.UnbalancedTime(getBalancedDuration(epochText)).message, + severity: 'warning', to: timeTagEpochNode.to, }); - } else { - if (!isTimeBalanced(epochText, TimeTypes.EPOCH)) { - diagnostics.push({ - actions: [], - from: timeTagEpochNode.from, - message: CustomErrorCodes.UnbalancedTime(getBalancedDuration(epochText)).message, - severity: 'warning', - to: timeTagEpochNode.to, - }); - } } } } - } else if (timeTagRelativeNode) { - const relativeText = text.slice(timeTagRelativeNode.from + 1, timeTagRelativeNode.to).trim(); - const isValid = - validateTime(relativeText, TimeTypes.RELATIVE) || validateTime(relativeText, TimeTypes.RELATIVE_SIMPLE); - if (!isValid) { - diagnostics.push({ - actions: [], - from: timeTagRelativeNode.from, - message: CustomErrorCodes.InvalidRelativeTime().message, - severity: 'error', - to: timeTagRelativeNode.to, - }); - } else { - if (validateTime(relativeText, TimeTypes.RELATIVE)) { - if (isTimeMax(relativeText, TimeTypes.RELATIVE)) { + } + } else if (timeTagRelativeNode) { + const relativeText = text.slice(timeTagRelativeNode.from + 1, timeTagRelativeNode.to).trim(); + const isValid = + validateTime(relativeText, TimeTypes.RELATIVE) || validateTime(relativeText, TimeTypes.RELATIVE_SIMPLE); + if (!isValid) { + diagnostics.push({ + actions: [], + from: timeTagRelativeNode.from, + message: CustomErrorCodes.InvalidRelativeTime().message, + severity: 'error', + to: timeTagRelativeNode.to, + }); + } else { + if (validateTime(relativeText, TimeTypes.RELATIVE)) { + if (isTimeMax(relativeText, TimeTypes.RELATIVE)) { + diagnostics.push({ + actions: [], + from: timeTagRelativeNode.from, + message: CustomErrorCodes.MaxRelativeTime().message, + severity: 'error', + to: timeTagRelativeNode.to, + }); + } else { + if (!isTimeBalanced(relativeText, TimeTypes.EPOCH)) { diagnostics.push({ actions: [], from: timeTagRelativeNode.from, - message: CustomErrorCodes.MaxRelativeTime().message, + message: CustomErrorCodes.UnbalancedTime(getBalancedDuration(relativeText)).message, severity: 'error', to: timeTagRelativeNode.to, }); - } else { - if (!isTimeBalanced(relativeText, TimeTypes.EPOCH)) { - diagnostics.push({ - actions: [], - from: timeTagRelativeNode.from, - message: CustomErrorCodes.UnbalancedTime(getBalancedDuration(relativeText)).message, - severity: 'error', - to: timeTagRelativeNode.to, - }); - } } } } } } - return diagnostics; } + return diagnostics; +} - /** - * Function to generate diagnostics for immediate commands in the parse tree. - * - * @param {SyntaxNode[] | undefined} commandNodes - Array of command nodes or undefined. - * @param {string} text - Text of the sequence. - * @return {Diagnostic[]} Array of diagnostics. - */ - function immediateCommandLinter( - commandNodes: SyntaxNode[] | undefined, - text: string, - variables: VariableMap, - ): Diagnostic[] { - // If there are no command nodes, return the empty array - if (!commandNodes) { - return []; - } - // Initialize an array to hold diagnostics +/** + * Function to generate diagnostics for immediate commands in the parse tree. + * + * @param {SyntaxNode[] | undefined} commandNodes - Array of command nodes or undefined. + * @param {string} text - Text of the sequence. + * @return {Diagnostic[]} Array of diagnostics. + */ +function immediateCommandLinter( + commandNodes: SyntaxNode[] | undefined, + text: string, + variables: VariableMap, + commandDictionary: CommandDictionary | null, + channelDictionary: ChannelDictionary | null, + parameterDictionaries: ParameterDictionary[], +): Diagnostic[] { + // If there are no command nodes, return the empty array + if (!commandNodes) { + return []; + } + // Initialize an array to hold diagnostics - const diagnostics: Diagnostic[] = []; + const diagnostics: Diagnostic[] = []; - // Iterate over each command node - for (const command of commandNodes) { - // Get the TimeTag node for the current command - const timeTagNode = command.getChild('TimeTag'); + // Iterate over each command node + for (const command of commandNodes) { + // Get the TimeTag node for the current command + const timeTagNode = command.getChild('TimeTag'); - // If the TimeTag node exists, create a diagnostic - if (timeTagNode) { - diagnostics.push({ - actions: [], - from: command.from, - message: "Immediate commands can't have a time tag", - severity: 'error', - to: command.to, - }); - } + // If the TimeTag node exists, create a diagnostic + if (timeTagNode) { + diagnostics.push({ + actions: [], + from: command.from, + message: "Immediate commands can't have a time tag", + severity: 'error', + to: command.to, + }); + } - // Validate the command and push the generated diagnostics to the array - diagnostics.push(...validateCommand(command, text, 'immediate', variables)); + // Validate the command and push the generated diagnostics to the array + diagnostics.push( + ...validateCommand( + command, + text, + 'immediate', + variables, + commandDictionary, + channelDictionary, + parameterDictionaries, + ), + ); - // Lint the metadata - diagnostics.push(...validateMetadata(command)); + // Lint the metadata + diagnostics.push(...validateMetadata(command)); - // immediate commands don't have models - const modelsNode = command.getChild('Models'); - if (modelsNode) { - diagnostics.push({ - from: modelsNode.from, - message: "Immediate commands can't have models", - severity: 'error', - to: modelsNode.to, - }); - } + // immediate commands don't have models + const modelsNode = command.getChild('Models'); + if (modelsNode) { + diagnostics.push({ + from: modelsNode.from, + message: "Immediate commands can't have models", + severity: 'error', + to: modelsNode.to, + }); } - - // Return the array of diagnostics - return diagnostics; } - /** - * Function to generate diagnostics based on HardwareCommands section in the parse tree. - * - * @param {SyntaxNode[] | undefined} commands - nodes representing hardware commands - * @param {string} text - the text to validate against - * @return {Diagnostic[]} an array of diagnostics - */ - function hardwareCommandLinter(commands: SyntaxNode[] | undefined, text: string): Diagnostic[] { - // Initialize an empty array to hold diagnostics - const diagnostics: Diagnostic[] = []; - - // If there are no command nodes, return an empty array of diagnostics - if (!commands) { - return diagnostics; - } - - // Iterate over each command node - for (const command of commands) { - // Get the TimeTag node for the current command - const timeTag = command.getChild('TimeTag'); - - // If the TimeTag node exists, create a diagnostic - if (timeTag) { - // Push a diagnostic to the array indicating that time tags are not allowed for hardware commands - diagnostics.push({ - actions: [], - from: command.from, - message: 'Time tag is not allowed for hardware commands', - severity: 'error', - to: command.to, - }); - } + // Return the array of diagnostics + return diagnostics; +} - // Validate the command and push the generated diagnostics to the array - diagnostics.push(...validateCommand(command, text, 'hardware', {})); +/** + * Function to generate diagnostics based on HardwareCommands section in the parse tree. + * + * @param {SyntaxNode[] | undefined} commands - nodes representing hardware commands + * @param {string} text - the text to validate against + * @return {Diagnostic[]} an array of diagnostics + */ +function hardwareCommandLinter( + commands: SyntaxNode[] | undefined, + text: string, + commandDictionary: CommandDictionary | null, + channelDictionary: ChannelDictionary | null, + parameterDictionaries: ParameterDictionary[], +): Diagnostic[] { + // Initialize an empty array to hold diagnostics + const diagnostics: Diagnostic[] = []; + + // If there are no command nodes, return an empty array of diagnostics + if (!commands) { + return diagnostics; + } - // Lint the metadata - diagnostics.push(...validateMetadata(command)); + // Iterate over each command node + for (const command of commands) { + // Get the TimeTag node for the current command + const timeTag = command.getChild('TimeTag'); - // hardware commands don't have models - const modelsNode = command.getChild('Models'); - if (modelsNode) { - diagnostics.push({ - actions: [], - from: modelsNode.from, - message: "Immediate commands can't have models", - severity: 'error', - to: modelsNode.to, - }); - } + // If the TimeTag node exists, create a diagnostic + if (timeTag) { + // Push a diagnostic to the array indicating that time tags are not allowed for hardware commands + diagnostics.push({ + actions: [], + from: command.from, + message: 'Time tag is not allowed for hardware commands', + severity: 'error', + to: command.to, + }); } - // Return the array of diagnostics - return diagnostics; - } + // Validate the command and push the generated diagnostics to the array + diagnostics.push( + ...validateCommand(command, text, 'hardware', {}, commandDictionary, channelDictionary, parameterDictionaries), + ); - /** - * Validates a command by validating its stem and arguments. - * - * @param command - The SyntaxNode representing the command. - * @param text - The text of the whole command. - * @returns An array of Diagnostic objects representing the validation errors. - */ - function validateCommand( - command: SyntaxNode, - text: string, - type: 'command' | 'immediate' | 'hardware' = 'command', - variables: VariableMap, - ): Diagnostic[] { - // If the command dictionary is not initialized, return an empty array of diagnostics. - if (!commandDictionary) { - return []; - } + // Lint the metadata + diagnostics.push(...validateMetadata(command)); - // Get the stem node of the command. - const stem = command.getChild('Stem'); - // If the stem node is null, return an empty array of diagnostics. - if (stem === null) { - return []; + // hardware commands don't have models + const modelsNode = command.getChild('Models'); + if (modelsNode) { + diagnostics.push({ + actions: [], + from: modelsNode.from, + message: "Immediate commands can't have models", + severity: 'error', + to: modelsNode.to, + }); } + } - const stemText = text.slice(stem.from, stem.to); + // Return the array of diagnostics + return diagnostics; +} - // Initialize an array to store the diagnostic errors. - const diagnostics: Diagnostic[] = []; +/** + * Validates a command by validating its stem and arguments. + * + * @param command - The SyntaxNode representing the command. + * @param text - The text of the whole command. + * @returns An array of Diagnostic objects representing the validation errors. + */ +function validateCommand( + command: SyntaxNode, + text: string, + type: 'command' | 'immediate' | 'hardware' = 'command', + variables: VariableMap, + commandDictionary: CommandDictionary | null, + channelDictionary: ChannelDictionary | null, + parameterDictionaries: ParameterDictionary[], +): Diagnostic[] { + // If the command dictionary is not initialized, return an empty array of diagnostics. + if (!commandDictionary) { + return []; + } - // Validate the stem of the command. - const result = validateStem(stem, stemText, type); - // No command dictionary return []. - if (result === null) { - return []; - } + // Get the stem node of the command. + const stem = command.getChild('Stem'); + // If the stem node is null, return an empty array of diagnostics. + if (stem === null) { + return []; + } - // Stem was invalid. - else if (typeof result === 'object' && 'message' in result) { - diagnostics.push(result); - return diagnostics; - } + const stemText = text.slice(stem.from, stem.to); - const argNode = command.getChild('Args'); - const dictArgs = (result as FswCommand).arguments ?? []; + // Initialize an array to store the diagnostic errors. + const diagnostics: Diagnostic[] = []; - // Lint the arguments of the command. - diagnostics.push( - ...validateAndLintArguments( - dictArgs, - argNode ? getChildrenNode(argNode) : null, - command, - text, - stemText, - variables, - ), - ); + // Validate the stem of the command. + const result = validateStem(stem, stemText, type, commandDictionary); + // No command dictionary return []. + if (result === null) { + return []; + } - // Return the array of diagnostics. + // Stem was invalid. + else if (typeof result === 'object' && 'message' in result) { + diagnostics.push(result); return diagnostics; } - /** - * Validates the stem of a command. - * @param stem - The SyntaxNode representing the stem of the command. - * @param stemText - The command name - * @param type - The type of command (default: 'command'). - * @returns A Diagnostic if the stem is invalid, a FswCommand if the stem is valid, or null if the command dictionary is not initialized. - */ - function validateStem( - stem: SyntaxNode, - stemText: string, - type: 'command' | 'immediate' | 'hardware' = 'command', - ): Diagnostic | FswCommand | HwCommand | null { - if (commandDictionary === null) { - return null; - } - const { fswCommandMap, fswCommands, hwCommandMap, hwCommands } = commandDictionary; - - const dictionaryCommand: FswCommand | HwCommand | null = fswCommandMap[stemText] - ? fswCommandMap[stemText] - : hwCommandMap[stemText] - ? hwCommandMap[stemText] - : null; - - if (!dictionaryCommand) { - const ALL_STEMS = [...fswCommands.map(cmd => cmd.stem), ...hwCommands.map(cmd => cmd.stem)]; - return { - actions: closestStrings(stemText.toUpperCase(), ALL_STEMS, 3).map(guess => ({ - apply(view, from, to) { - view.dispatch({ changes: { from, insert: guess, to } }); - }, - name: `Change to ${guess}`, - })), - from: stem.from, - message: `Command '${stemText}' not found`, - severity: 'error', - to: stem.to, - }; - } + const argNode = command.getChild('Args'); + const dictArgs = (result as FswCommand).arguments ?? []; + + // Lint the arguments of the command. + diagnostics.push( + ...validateAndLintArguments( + dictArgs, + argNode ? getChildrenNode(argNode) : null, + command, + text, + stemText, + variables, + commandDictionary, + channelDictionary, + parameterDictionaries, + ), + ); + + // Return the array of diagnostics. + return diagnostics; +} - switch (type) { - case 'command': - case 'immediate': - if (!fswCommandMap[stemText]) { - return { - from: stem.from, - message: 'Command must be a fsw command', - severity: 'error', - to: stem.to, - }; - } - break; - case 'hardware': - if (!hwCommandMap[stemText]) { - return { - from: stem.from, - message: 'Command must be a hardware command', - severity: 'error', - to: stem.to, - }; - } - break; - } +/** + * Validates the stem of a command. + * @param stem - The SyntaxNode representing the stem of the command. + * @param stemText - The command name + * @param type - The type of command (default: 'command'). + * @returns A Diagnostic if the stem is invalid, a FswCommand if the stem is valid, or null if the command dictionary is not initialized. + */ +function validateStem( + stem: SyntaxNode, + stemText: string, + type: 'command' | 'immediate' | 'hardware' = 'command', + commandDictionary: CommandDictionary | null, +): Diagnostic | FswCommand | HwCommand | null { + if (commandDictionary === null) { + return null; + } + const { fswCommandMap, fswCommands, hwCommandMap, hwCommands } = commandDictionary; + + const dictionaryCommand: FswCommand | HwCommand | null = fswCommandMap[stemText] + ? fswCommandMap[stemText] + : hwCommandMap[stemText] + ? hwCommandMap[stemText] + : null; - return dictionaryCommand; + if (!dictionaryCommand) { + const ALL_STEMS = [...fswCommands.map(cmd => cmd.stem), ...hwCommands.map(cmd => cmd.stem)]; + return { + actions: closestStrings(stemText.toUpperCase(), ALL_STEMS, 3).map(guess => ({ + apply(view, from, to) { + view.dispatch({ changes: { from, insert: guess, to } }); + }, + name: `Change to ${guess}`, + })), + from: stem.from, + message: `Command '${stemText}' not found`, + severity: 'error', + to: stem.to, + }; } - /** - * Validates and lints the command arguments based on the dictionary of command arguments. - * @param dictArgs - The dictionary of command arguments. - * @param argNode - The SyntaxNode representing the arguments of the command. - * @param command - The SyntaxNode representing the command. - * @param text - The text of the document. - * @returns An array of Diagnostic objects representing the validation errors. - */ - function validateAndLintArguments( - dictArgs: FswCommandArgument[], - argNode: SyntaxNode[] | null, - command: SyntaxNode, - text: string, - stem: string, - variables: VariableMap, - ): Diagnostic[] { - // Initialize an array to store the validation errors - let diagnostics: Diagnostic[] = []; - - // Validate argument presence based on dictionary definition - if (dictArgs.length > 0) { - if (!argNode || argNode.length === 0) { - diagnostics.push({ - actions: [], - from: command.from, - message: 'The command is missing arguments.', + switch (type) { + case 'command': + case 'immediate': + if (!fswCommandMap[stemText]) { + return { + from: stem.from, + message: 'Command must be a fsw command', severity: 'error', - to: command.to, - }); - return diagnostics; + to: stem.to, + }; } - - if (argNode.length > dictArgs.length) { - const extraArgs = argNode.slice(dictArgs.length); - const { from, to } = getFromAndTo(extraArgs); - diagnostics.push({ - actions: [ - { - apply(view, from, to) { - view.dispatch({ changes: { from, to } }); - }, - name: `Remove ${extraArgs.length} extra argument${extraArgs.length > 1 ? 's' : ''}`, - }, - ], - from, - message: `Extra arguments, definition has ${dictArgs.length}, but ${argNode.length} are present`, + break; + case 'hardware': + if (!hwCommandMap[stemText]) { + return { + from: stem.from, + message: 'Command must be a hardware command', severity: 'error', - to, - }); - return diagnostics; - } else if (argNode.length < dictArgs.length) { - const { from, to } = getFromAndTo(argNode); - const pluralS = dictArgs.length > argNode.length + 1 ? 's' : ''; - diagnostics.push({ - actions: [ - { - apply(view) { - if (commandDictionary) { - addDefaultArgs(commandDictionary, view, command, dictArgs.slice(argNode.length)); - } - }, - name: `Add default missing argument${pluralS}`, - }, - ], - from, - message: `Missing argument${pluralS}, definition has ${argNode.length}, but ${dictArgs.length} are present`, - severity: 'error', - to, - }); - return diagnostics; + to: stem.to, + }; } - } else if (argNode && argNode.length > 0) { - const { from, to } = getFromAndTo(argNode); + break; + } + + return dictionaryCommand; +} + +/** + * Validates and lints the command arguments based on the dictionary of command arguments. + * @param dictArgs - The dictionary of command arguments. + * @param argNode - The SyntaxNode representing the arguments of the command. + * @param command - The SyntaxNode representing the command. + * @param text - The text of the document. + * @returns An array of Diagnostic objects representing the validation errors. + */ +function validateAndLintArguments( + dictArgs: FswCommandArgument[], + argNode: SyntaxNode[] | null, + command: SyntaxNode, + text: string, + stem: string, + variables: VariableMap, + commandDictionary: CommandDictionary | null, + channelDictionary: ChannelDictionary | null, + parameterDictionaries: ParameterDictionary[], +): Diagnostic[] { + // Initialize an array to store the validation errors + let diagnostics: Diagnostic[] = []; + + // Validate argument presence based on dictionary definition + if (dictArgs.length > 0) { + if (!argNode || argNode.length === 0) { + diagnostics.push({ + actions: [], + from: command.from, + message: 'The command is missing arguments.', + severity: 'error', + to: command.to, + }); + return diagnostics; + } + + if (argNode.length > dictArgs.length) { + const extraArgs = argNode.slice(dictArgs.length); + const { from, to } = getFromAndTo(extraArgs); diagnostics.push({ actions: [ { apply(view, from, to) { view.dispatch({ changes: { from, to } }); }, - name: `Remove argument${argNode.length > 1 ? 's' : ''}`, + name: `Remove ${extraArgs.length} extra argument${extraArgs.length > 1 ? 's' : ''}`, }, ], - from: from, - message: 'The command should not have arguments', + from, + message: `Extra arguments, definition has ${dictArgs.length}, but ${argNode.length} are present`, severity: 'error', - to: to, + to, }); return diagnostics; - } - - // don't check any further as there are no arguments in the command dictionary - if (dictArgs.length === 0) { + } else if (argNode.length < dictArgs.length) { + const { from, to } = getFromAndTo(argNode); + const pluralS = dictArgs.length > argNode.length + 1 ? 's' : ''; + diagnostics.push({ + actions: [ + { + apply(view) { + if (commandDictionary) { + addDefaultArgs(commandDictionary, view, command, dictArgs.slice(argNode.length)); + } + }, + name: `Add default missing argument${pluralS}`, + }, + ], + from, + message: `Missing argument${pluralS}, definition has ${argNode.length}, but ${dictArgs.length} are present`, + severity: 'error', + to, + }); return diagnostics; } + } else if (argNode && argNode.length > 0) { + const { from, to } = getFromAndTo(argNode); + diagnostics.push({ + actions: [ + { + apply(view, from, to) { + view.dispatch({ changes: { from, to } }); + }, + name: `Remove argument${argNode.length > 1 ? 's' : ''}`, + }, + ], + from: from, + message: 'The command should not have arguments', + severity: 'error', + to: to, + }); + return diagnostics; + } - const argValues = argNode?.map(arg => text.slice(arg.from, arg.to)) ?? []; + // don't check any further as there are no arguments in the command dictionary + if (dictArgs.length === 0) { + return diagnostics; + } - // grab the first argument node - // let node = argNode?.firstChild ?? null; + const argValues = argNode?.map(arg => text.slice(arg.from, arg.to)) ?? []; - // Iterate through the dictionary of command arguments - for (let i = 0; i < dictArgs.length; i++) { - const dictArg = dictArgs[i]; // Get the current dictionary argument - const arg = argNode?.[i]; // Get the current argument node - // Check if there are no more argument nodes - if (!arg) { - // Push a diagnostic error for missing argument - diagnostics.push({ - actions: [], - from: command.from, - message: `Missing argument #${i + 1}, '${dictArg.name}' of type '${dictArg.arg_type}'`, - severity: 'error', - to: command.to, - }); - break; - } + // grab the first argument node + // let node = argNode?.firstChild ?? null; - // Validate and lint the current argument node - diagnostics = diagnostics.concat( - ...validateArgument(dictArg, arg, command, text, stem, argValues.slice(0, i), variables), - ); + // Iterate through the dictionary of command arguments + for (let i = 0; i < dictArgs.length; i++) { + const dictArg = dictArgs[i]; // Get the current dictionary argument + const arg = argNode?.[i]; // Get the current argument node + // Check if there are no more argument nodes + if (!arg) { + // Push a diagnostic error for missing argument + diagnostics.push({ + actions: [], + from: command.from, + message: `Missing argument #${i + 1}, '${dictArg.name}' of type '${dictArg.arg_type}'`, + severity: 'error', + to: command.to, + }); + break; } - // Return the array of validation errors - return diagnostics; + // Validate and lint the current argument node + diagnostics = diagnostics.concat( + ...validateArgument( + dictArg, + arg, + command, + text, + stem, + argValues.slice(0, i), + variables, + commandDictionary, + channelDictionary, + parameterDictionaries, + ), + ); } - /** + // Return the array of validation errors + return diagnostics; +} + +/** + * Validates the given FSW command argument against the provided syntax node, + * and generates diagnostics if the validation fails. + * @@ -994,465 +1083,477 @@ export function sequenceLinter( + * @param text The full text of the document. + * @returns An array of diagnostics generated during the validation. + */ - function validateArgument( - dictArg: FswCommandArgument, - argNode: SyntaxNode, - command: SyntaxNode, - text: string, - stemText: string, - precedingArgValues: string[], - variables: VariableMap, - ): Diagnostic[] { - dictArg = getCustomArgDef(stemText, dictArg, precedingArgValues, parameterDictionaries, channelDictionary); - - const diagnostics: Diagnostic[] = []; - - const dictArgType = dictArg.arg_type; - const argType = argNode.name; - const argText = text.slice(argNode.from, argNode.to); - - switch (dictArgType) { - case 'enum': - if (argType === 'Enum') { - if (!variables[argText]) { - // TODO -- potentially check that variable types match usage +function validateArgument( + dictArg: FswCommandArgument, + argNode: SyntaxNode, + command: SyntaxNode, + text: string, + stemText: string, + precedingArgValues: string[], + variables: VariableMap, + commandDictionary: CommandDictionary | null, + channelDictionary: ChannelDictionary | null, + parameterDictionaries: ParameterDictionary[], +): Diagnostic[] { + dictArg = getCustomArgDef(stemText, dictArg, precedingArgValues, parameterDictionaries, channelDictionary); + + const diagnostics: Diagnostic[] = []; + + const dictArgType = dictArg.arg_type; + const argType = argNode.name; + const argText = text.slice(argNode.from, argNode.to); + + switch (dictArgType) { + case 'enum': + if (argType === 'Enum') { + if (!variables[argText]) { + // TODO -- potentially check that variable types match usage + diagnostics.push({ + from: argNode.from, + message: `Unrecognized variable name ${argText}`, + severity: 'error', + to: argNode.to, + }); + } + } else if (argType !== 'String') { + diagnostics.push({ + actions: [], + from: argNode.from, + message: `Incorrect type - expected double quoted 'enum' but got ${argType}`, + severity: 'error', + to: argNode.to, + }); + } else { + if (commandDictionary) { + const symbols = getAllEnumSymbols(commandDictionary?.enumMap, dictArg.enum_name) ?? dictArg.range ?? []; + const unquotedArgText = argText.replace(/^"|"$/g, ''); + if (!symbols.includes(unquotedArgText)) { + const guess = closest(unquotedArgText.toUpperCase(), symbols); diagnostics.push({ + actions: [ + { + apply(view, from, to) { + view.dispatch({ changes: { from, insert: `"${guess}"`, to } }); + }, + name: `Change to ${guess}`, + }, + ], from: argNode.from, - message: `Unrecognized variable name ${argText}`, + message: `Enum should be "${symbols.slice(0, MAX_ENUMS_TO_SHOW).join(' | ')}${symbols.length > MAX_ENUMS_TO_SHOW ? ' ...' : ''}"\n`, severity: 'error', to: argNode.to, }); + break; } - } else if (argType !== 'String') { + } + } + break; + case 'boolean': + if (argType !== 'Boolean') { + diagnostics.push({ + actions: [], + from: argNode.from, + message: `Incorrect type - expected 'Boolean' but got ${argType}`, + severity: 'error', + to: argNode.to, + }); + } + if (['true', 'false'].includes(argText) === false) { + diagnostics.push({ + actions: [], + from: argNode.from, + message: `Incorrect value - expected true or false but got ${argText}`, + severity: 'error', + to: argNode.to, + }); + } + break; + case 'float': + case 'integer': + case 'numeric': + case 'unsigned': + if (argType === 'Number') { + if (dictArg.range === null) { + break; + } + const { max, min } = dictArg.range; + const nodeTextAsNumber = parseNumericArg(argText, dictArgType); + const isHex = isHexValue(argText); + if (nodeTextAsNumber < min || nodeTextAsNumber > max) { + const numFormat = (n: number) => (isHex ? `0x${n.toString(16).toUpperCase()}` : n); + const message = + max !== min + ? `Number out of range. Range is between ${numFormat(min)} and ${numFormat(max)} inclusive.` + : `Number out of range. Range is ${numFormat(min)}.`; diagnostics.push({ - actions: [], + actions: + max === min + ? [ + { + apply(view, from, to) { + view.dispatch({ changes: { from, insert: `${min}`, to } }); + }, + name: `Change to ${min}`, + }, + ] + : [], from: argNode.from, - message: `Incorrect type - expected double quoted 'enum' but got ${argType}`, + message, severity: 'error', to: argNode.to, }); - } else { - if (commandDictionary) { - const symbols = getAllEnumSymbols(commandDictionary?.enumMap, dictArg.enum_name) ?? dictArg.range ?? []; - const unquotedArgText = argText.replace(/^"|"$/g, ''); - if (!symbols.includes(unquotedArgText)) { - const guess = closest(unquotedArgText.toUpperCase(), symbols); - diagnostics.push({ - actions: [ - { - apply(view, from, to) { - view.dispatch({ changes: { from, insert: `"${guess}"`, to } }); - }, - name: `Change to ${guess}`, - }, - ], - from: argNode.from, - message: `Enum should be "${symbols.slice(0, MAX_ENUMS_TO_SHOW).join(' | ')}${symbols.length > MAX_ENUMS_TO_SHOW ? ' ...' : ''}"\n`, - severity: 'error', - to: argNode.to, - }); - break; - } - } } - break; - case 'boolean': - if (argType !== 'Boolean') { + } else if (argType === 'Enum') { + if (!variables[argText]) { diagnostics.push({ - actions: [], from: argNode.from, - message: `Incorrect type - expected 'Boolean' but got ${argType}`, + message: `Unrecognized variable name ${argText}`, severity: 'error', to: argNode.to, }); } - if (['true', 'false'].includes(argText) === false) { + } else { + diagnostics.push({ + from: argNode.from, + message: `Incorrect type - expected 'Number' but got ${argType}`, + severity: 'error', + to: argNode.to, + }); + } + break; + case 'fixed_string': + case 'var_string': + if (argType === 'Enum') { + if (!variables[argText]) { + const insert = closest(argText, Object.keys(variables)); diagnostics.push({ - actions: [], + actions: [ + { + apply(view, from, to) { + view.dispatch({ changes: { from, insert, to } }); + }, + name: `Change to ${insert}`, + }, + ], from: argNode.from, - message: `Incorrect value - expected true or false but got ${argText}`, + message: `Unrecognized variable name ${argText}`, severity: 'error', to: argNode.to, }); } - break; - case 'float': - case 'integer': - case 'numeric': - case 'unsigned': - if (argType === 'Number') { - if (dictArg.range === null) { - break; - } - const { max, min } = dictArg.range; - const nodeTextAsNumber = parseNumericArg(argText, dictArgType); - const isHex = isHexValue(argText); - if (nodeTextAsNumber < min || nodeTextAsNumber > max) { - const numFormat = (n: number) => (isHex ? `0x${n.toString(16).toUpperCase()}` : n); - const message = - max !== min - ? `Number out of range. Range is between ${numFormat(min)} and ${numFormat(max)} inclusive.` - : `Number out of range. Range is ${numFormat(min)}.`; + } else if (argType !== 'String') { + diagnostics.push({ + from: argNode.from, + message: `Incorrect type - expected 'String' but got ${argType}`, + severity: 'error', + to: argNode.to, + }); + } + break; + case 'repeat': + if (argType !== TOKEN_REPEAT_ARG) { + diagnostics.push({ + from: argNode.from, + message: `Incorrect type - expected '${TOKEN_REPEAT_ARG}' but got ${argType}`, + severity: 'error', + to: argNode.to, + }); + } else { + const repeatNodes = argNode.getChildren('Arguments'); + const repeatDef = dictArg.repeat; + if (repeatDef) { + const repeatLength = repeatDef.arguments.length; + const minSets = repeatDef.min ?? 0; + const maxSets = repeatDef.max ?? Infinity; + const minCount = repeatLength * minSets; + const maxCount = repeatLength * maxSets; + if (minCount > repeatNodes.length) { diagnostics.push({ - actions: - max === min - ? [ - { - apply(view, from, to) { - view.dispatch({ changes: { from, insert: `${min}`, to } }); - }, - name: `Change to ${min}`, - }, - ] - : [], + actions: [], from: argNode.from, - message, + message: `Repeat argument should have at least ${minCount} value${minCount !== 0 ? 's' : ''} but has ${ + repeatNodes.length + }`, severity: 'error', to: argNode.to, }); - } - } else if (argType === 'Enum') { - if (!variables[argText]) { + } else if (maxCount < repeatNodes.length) { diagnostics.push({ + actions: [], from: argNode.from, - message: `Unrecognized variable name ${argText}`, + message: `Repeat argument should have at most ${maxCount} value${maxCount !== 0 ? 's' : ''} but has ${ + repeatNodes.length + }`, severity: 'error', to: argNode.to, }); - } - } else { - diagnostics.push({ - from: argNode.from, - message: `Incorrect type - expected 'Number' but got ${argType}`, - severity: 'error', - to: argNode.to, - }); - } - break; - case 'fixed_string': - case 'var_string': - if (argType === 'Enum') { - if (!variables[argText]) { - const insert = closest(argText, Object.keys(variables)); + } else if (repeatNodes.length % repeatLength !== 0) { + const allowedValues: number[] = []; + for (let i = minSets; i <= Math.min(maxSets, minSets + 2); i++) { + allowedValues.push(i * repeatLength); + } + let showEllipses = false; + if (allowedValues.length) { + const lastVal = allowedValues[allowedValues.length - 1]; + if (maxCount > lastVal) { + if (maxCount > lastVal + repeatLength) { + showEllipses = true; + } + allowedValues.push(maxCount); + } + } + const valStrings = allowedValues.map(i => i.toString()); + if (showEllipses) { + valStrings.splice(allowedValues.length - 1, 0, '...'); + } + diagnostics.push({ - actions: [ - { - apply(view, from, to) { - view.dispatch({ changes: { from, insert, to } }); - }, - name: `Change to ${insert}`, - }, - ], + actions: [], from: argNode.from, - message: `Unrecognized variable name ${argText}`, + message: `Repeat argument should have [${valStrings.join(', ')}] values`, severity: 'error', to: argNode.to, }); + } else { + repeatNodes + .reduce((acc, node, i) => { + const chunkIndex = Math.floor(i / repeatLength); + if (!acc[chunkIndex]) { + acc[chunkIndex] = []; + } + acc[chunkIndex].push(node); + return acc; + }, []) + .forEach((repeat: SyntaxNode[]) => { + // check individual args + diagnostics.push( + ...validateAndLintArguments( + repeatDef.arguments ?? [], + repeat, + command, + text, + stemText, + variables, + commandDictionary, + channelDictionary, + parameterDictionaries, + ), + ); + }); } - } else if (argType !== 'String') { - diagnostics.push({ - from: argNode.from, - message: `Incorrect type - expected 'String' but got ${argType}`, - severity: 'error', - to: argNode.to, - }); } - break; - case 'repeat': - if (argType !== TOKEN_REPEAT_ARG) { + } + + break; + } + return diagnostics; +} + +function validateId(commandNode: SyntaxNode, text: string): Diagnostic[] { + const diagnostics: Diagnostic[] = []; + const idNodes = commandNode.getChildren('IdDeclaration'); + if (idNodes.length) { + const idNode = idNodes[0]; + const idValNode = idNode.firstChild; + if (idValNode?.name === 'Enum' || idValNode?.name === 'Number') { + const { from, to } = getFromAndTo([idValNode]); + const idVal = text.slice(from, to); + diagnostics.push({ + actions: idValNode + ? [ + { + apply(view, from, to) { + view.dispatch({ changes: { from, insert: quoteEscape(idVal), to } }); + }, + name: `Quote ${idVal}`, + }, + ] + : [], + from, + message: `@ID directives must include double quoted string e.g. '@ID "sequence.name"'`, + severity: 'error', + to, + }); + } else if (!idValNode) { + diagnostics.push({ + ...getFromAndTo([idNode]), + message: `@ID directives must include a double quoted string e.g. '@ID "sequence.name"`, + severity: 'error', + }); + } + } + diagnostics.push( + ...idNodes.slice(1).map( + idNode => + ({ + ...getFromAndTo([idNode]), + message: 'Only one @ID directive is allowed per sequence', + severity: 'error', + }) as const, + ), + ); + return diagnostics; +} + +/** + * Validates the metadata of a command node and returns an array of diagnostics. + * @param commandNode - The command node to validate. + * @returns An array of diagnostic objects. + */ +function validateMetadata(commandNode: SyntaxNode): Diagnostic[] { + // Get the metadata node of the command node + const metadataNode = commandNode.getChild('Metadata'); + // If there is no metadata node, return an empty array + if (!metadataNode) { + return []; + } + // Get the metadata entry nodes of the metadata node + const metadataEntry = metadataNode.getChildren('MetaEntry'); + // If there are no metadata entry nodes, return an empty array + if (!metadataEntry) { + return []; + } + + const diagnostics: Diagnostic[] = []; + + // Iterate over each metadata entry node + metadataEntry.forEach(entry => { + // Get the children nodes of the metadata entry node + const metadataNodeChildren = getChildrenNode(entry); + + if (metadataNodeChildren.length > 2) { + diagnostics.push({ + actions: [], + from: entry.from, + message: `Should only have a 'key' and a 'value'`, + severity: 'error', + to: entry.to, + }); + } else { + // Define the template for metadata nodes + const metadataTemplate = ['Key', 'Value']; + // Iterate over each template node + for (let i = 0; i < metadataTemplate.length; i++) { + // Get the name of the template node + const templateName = metadataTemplate[i]; + // Get the metadata node of the current template node + const metadataNode = metadataNodeChildren[i]; + + // If there is no metadata node, add a diagnostic + if (!metadataNode) { diagnostics.push({ - from: argNode.from, - message: `Incorrect type - expected '${TOKEN_REPEAT_ARG}' but got ${argType}`, + actions: [], + from: entry.from, + message: `Missing ${templateName}`, severity: 'error', - to: argNode.to, + to: entry.to, }); - } else { - const repeatNodes = argNode.getChildren('Arguments'); - const repeatDef = dictArg.repeat; - if (repeatDef) { - const repeatLength = repeatDef.arguments.length; - const minSets = repeatDef.min ?? 0; - const maxSets = repeatDef.max ?? Infinity; - const minCount = repeatLength * minSets; - const maxCount = repeatLength * maxSets; - if (minCount > repeatNodes.length) { - diagnostics.push({ - actions: [], - from: argNode.from, - message: `Repeat argument should have at least ${minCount} value${minCount !== 0 ? 's' : ''} but has ${ - repeatNodes.length - }`, - severity: 'error', - to: argNode.to, - }); - } else if (maxCount < repeatNodes.length) { + break; + } + + // If the name of the metadata node is not the template node name + if (metadataNode.name !== templateName) { + // Get the name of the deepest node of the metadata node + const deepestNodeName = getDeepestNode(metadataNode).name; + // Add a diagnostic based on the name of the deepest node + switch (deepestNodeName) { + case 'String': + break; // do nothing as it is a string + case 'Number': + case 'Enum': + case 'Boolean': diagnostics.push({ - actions: [], - from: argNode.from, - message: `Repeat argument should have at most ${maxCount} value${maxCount !== 0 ? 's' : ''} but has ${ - repeatNodes.length - }`, + from: metadataNode.from, + message: `Incorrect type - expected 'String' but got ${deepestNodeName}`, severity: 'error', - to: argNode.to, + to: metadataNode.to, }); - } else if (repeatNodes.length % repeatLength !== 0) { - const allowedValues: number[] = []; - for (let i = minSets; i <= Math.min(maxSets, minSets + 2); i++) { - allowedValues.push(i * repeatLength); - } - let showEllipses = false; - if (allowedValues.length) { - const lastVal = allowedValues[allowedValues.length - 1]; - if (maxCount > lastVal) { - if (maxCount > lastVal + repeatLength) { - showEllipses = true; - } - allowedValues.push(maxCount); - } - } - const valStrings = allowedValues.map(i => i.toString()); - if (showEllipses) { - valStrings.splice(allowedValues.length - 1, 0, '...'); - } - + break; + default: diagnostics.push({ - actions: [], - from: argNode.from, - message: `Repeat argument should have [${valStrings.join(', ')}] values`, + from: entry.from, + message: `Missing ${templateName}`, severity: 'error', - to: argNode.to, + to: entry.to, }); - } else { - repeatNodes - .reduce((acc, node, i) => { - const chunkIndex = Math.floor(i / repeatLength); - if (!acc[chunkIndex]) { - acc[chunkIndex] = []; - } - acc[chunkIndex].push(node); - return acc; - }, []) - .forEach((repeat: SyntaxNode[]) => { - // check individual args - diagnostics.push( - ...validateAndLintArguments(repeatDef.arguments ?? [], repeat, command, text, stemText, variables), - ); - }); - } } } - - break; - } - return diagnostics; - } - - function validateId(commandNode: SyntaxNode, text: string): Diagnostic[] { - const diagnostics: Diagnostic[] = []; - const idNodes = commandNode.getChildren('IdDeclaration'); - if (idNodes.length) { - const idNode = idNodes[0]; - const idValNode = idNode.firstChild; - if (idValNode?.name === 'Enum' || idValNode?.name === 'Number') { - const { from, to } = getFromAndTo([idValNode]); - const idVal = text.slice(from, to); - diagnostics.push({ - actions: idValNode - ? [ - { - apply(view, from, to) { - view.dispatch({ changes: { from, insert: quoteEscape(idVal), to } }); - }, - name: `Quote ${idVal}`, - }, - ] - : [], - from, - message: `@ID directives must include double quoted string e.g. '@ID "sequence.name"'`, - severity: 'error', - to, - }); - } else if (!idValNode) { - diagnostics.push({ - ...getFromAndTo([idNode]), - message: `@ID directives must include a double quoted string e.g. '@ID "sequence.name"`, - severity: 'error', - }); } } - diagnostics.push( - ...idNodes.slice(1).map( - idNode => - ({ - ...getFromAndTo([idNode]), - message: 'Only one @ID directive is allowed per sequence', - severity: 'error', - }) as const, - ), - ); - return diagnostics; - } + }); - /** - * Validates the metadata of a command node and returns an array of diagnostics. - * @param commandNode - The command node to validate. - * @returns An array of diagnostic objects. - */ - function validateMetadata(commandNode: SyntaxNode): Diagnostic[] { - // Get the metadata node of the command node - const metadataNode = commandNode.getChild('Metadata'); - // If there is no metadata node, return an empty array - if (!metadataNode) { - return []; - } - // Get the metadata entry nodes of the metadata node - const metadataEntry = metadataNode.getChildren('MetaEntry'); - // If there are no metadata entry nodes, return an empty array - if (!metadataEntry) { - return []; - } + return diagnostics; +} + +function validateModel(commandNode: SyntaxNode): Diagnostic[] { + const models = commandNode.getChild('Models')?.getChildren('Model'); + if (!models) { + return []; + } - const diagnostics: Diagnostic[] = []; + const diagnostics: Diagnostic[] = []; - // Iterate over each metadata entry node - metadataEntry.forEach(entry => { - // Get the children nodes of the metadata entry node - const metadataNodeChildren = getChildrenNode(entry); + models.forEach(model => { + const modelChildren = getChildrenNode(model); + if (modelChildren.length > 3) { + diagnostics.push({ + from: model.from, + message: `Should only have 'Variable', 'value', and 'Offset'`, + severity: 'error', + to: model.to, + }); + } else { + const modelTemplate = ['Variable', 'Value', 'Offset']; + for (let i = 0; i < modelTemplate.length; i++) { + const templateName = modelTemplate[i]; + const modelNode = modelChildren[i]; + if (!modelNode) { + diagnostics.push({ + from: model.from, + message: `Missing ${templateName}`, + severity: 'error', + to: model.to, + }); + } - if (metadataNodeChildren.length > 2) { - diagnostics.push({ - actions: [], - from: entry.from, - message: `Should only have a 'key' and a 'value'`, - severity: 'error', - to: entry.to, - }); - } else { - // Define the template for metadata nodes - const metadataTemplate = ['Key', 'Value']; - // Iterate over each template node - for (let i = 0; i < metadataTemplate.length; i++) { - // Get the name of the template node - const templateName = metadataTemplate[i]; - // Get the metadata node of the current template node - const metadataNode = metadataNodeChildren[i]; - - // If there is no metadata node, add a diagnostic - if (!metadataNode) { + if (modelNode.name !== templateName) { + const deepestNodeName = getDeepestNode(modelNode).name; + if (deepestNodeName === TOKEN_ERROR) { diagnostics.push({ - actions: [], - from: entry.from, + from: model.from, message: `Missing ${templateName}`, severity: 'error', - to: entry.to, + to: model.to, }); break; - } - - // If the name of the metadata node is not the template node name - if (metadataNode.name !== templateName) { - // Get the name of the deepest node of the metadata node - const deepestNodeName = getDeepestNode(metadataNode).name; - // Add a diagnostic based on the name of the deepest node - switch (deepestNodeName) { - case 'String': - break; // do nothing as it is a string - case 'Number': - case 'Enum': - case 'Boolean': + } else { + if (templateName === 'Variable' || templateName === 'Offset') { + if (deepestNodeName !== 'String') { diagnostics.push({ - from: metadataNode.from, + from: modelNode.from, message: `Incorrect type - expected 'String' but got ${deepestNodeName}`, severity: 'error', - to: metadataNode.to, + to: modelNode.to, }); break; - default: + } + } else { + // Value + if (deepestNodeName !== 'Number' && deepestNodeName !== 'String' && deepestNodeName !== 'Boolean') { diagnostics.push({ - from: entry.from, - message: `Missing ${templateName}`, + from: modelNode.from, + message: `Incorrect type - expected 'Number', 'String', or 'Boolean' but got ${deepestNodeName}`, severity: 'error', - to: entry.to, + to: modelNode.to, }); - } - } - } - } - }); - - return diagnostics; - } - - function validateModel(commandNode: SyntaxNode): Diagnostic[] { - const models = commandNode.getChild('Models')?.getChildren('Model'); - if (!models) { - return []; - } - - const diagnostics: Diagnostic[] = []; - - models.forEach(model => { - const modelChildren = getChildrenNode(model); - if (modelChildren.length > 3) { - diagnostics.push({ - from: model.from, - message: `Should only have 'Variable', 'value', and 'Offset'`, - severity: 'error', - to: model.to, - }); - } else { - const modelTemplate = ['Variable', 'Value', 'Offset']; - for (let i = 0; i < modelTemplate.length; i++) { - const templateName = modelTemplate[i]; - const modelNode = modelChildren[i]; - if (!modelNode) { - diagnostics.push({ - from: model.from, - message: `Missing ${templateName}`, - severity: 'error', - to: model.to, - }); - } - - if (modelNode.name !== templateName) { - const deepestNodeName = getDeepestNode(modelNode).name; - if (deepestNodeName === TOKEN_ERROR) { - diagnostics.push({ - from: model.from, - message: `Missing ${templateName}`, - severity: 'error', - to: model.to, - }); - break; - } else { - if (templateName === 'Variable' || templateName === 'Offset') { - if (deepestNodeName !== 'String') { - diagnostics.push({ - from: modelNode.from, - message: `Incorrect type - expected 'String' but got ${deepestNodeName}`, - severity: 'error', - to: modelNode.to, - }); - break; - } - } else { - // Value - if (deepestNodeName !== 'Number' && deepestNodeName !== 'String' && deepestNodeName !== 'Boolean') { - diagnostics.push({ - from: modelNode.from, - message: `Incorrect type - expected 'Number', 'String', or 'Boolean' but got ${deepestNodeName}`, - severity: 'error', - to: modelNode.to, - }); - break; - } + break; } } } } } - }); + } + }); - return diagnostics; - } + return diagnostics; } From 5bcac5bbe9167727f533780879ea942320f713f9 Mon Sep 17 00:00:00 2001 From: Cody Hansen Date: Mon, 30 Sep 2024 10:43:41 -1000 Subject: [PATCH 4/5] Fixed some problems with null checking workspace ids --- src/components/sequencing/Sequences.svelte | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/sequencing/Sequences.svelte b/src/components/sequencing/Sequences.svelte index 76e3a73cae..f236f0ebc4 100644 --- a/src/components/sequencing/Sequences.svelte +++ b/src/components/sequencing/Sequences.svelte @@ -51,7 +51,7 @@ workspaceId = event.detail; if (browser) { - setQueryParam(SearchParameters.WORKSPACE_ID, `${workspaceId}` ?? null); + setQueryParam(SearchParameters.WORKSPACE_ID, `${workspaceId ?? null}`); } } @@ -78,9 +78,8 @@ }} disabled={workspace === undefined} on:click={() => { - goto( - `${base}/sequencing/new${'?' + SearchParameters.WORKSPACE_ID + '=' + getSearchParameterNumber(SearchParameters.WORKSPACE_ID) ?? ''}`, - ); + const workspaceId = getSearchParameterNumber(SearchParameters.WORKSPACE_ID); + goto(`${base}/sequencing/new${workspaceId ? '?' + SearchParameters.WORKSPACE_ID + '=' + workspaceId : ''}`); }} > New Sequence From 4f04633c3fb118b31c7b19eab550156dc40e939d Mon Sep 17 00:00:00 2001 From: Cody Hansen Date: Mon, 30 Sep 2024 12:42:44 -1000 Subject: [PATCH 5/5] Moved numFormat to its own function, cleaned up workspaceId URL handling --- src/components/sequencing/Sequences.svelte | 12 +++++++----- src/utilities/sequence-editor/sequence-linter.ts | 10 ++++++---- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/components/sequencing/Sequences.svelte b/src/components/sequencing/Sequences.svelte index f236f0ebc4..a9bbca94da 100644 --- a/src/components/sequencing/Sequences.svelte +++ b/src/components/sequencing/Sequences.svelte @@ -51,9 +51,14 @@ workspaceId = event.detail; if (browser) { - setQueryParam(SearchParameters.WORKSPACE_ID, `${workspaceId ?? null}`); + setQueryParam(SearchParameters.WORKSPACE_ID, workspaceId !== null ? `${workspaceId}` : null); } } + + function navigateToNewSequence(): void { + const workspaceId = getSearchParameterNumber(SearchParameters.WORKSPACE_ID); + goto(`${base}/sequencing/new${workspaceId ? `?${SearchParameters.WORKSPACE_ID}=${workspaceId}` : ''}`); + } @@ -77,10 +82,7 @@ permissionError: 'You do not have permission to create a new sequence', }} disabled={workspace === undefined} - on:click={() => { - const workspaceId = getSearchParameterNumber(SearchParameters.WORKSPACE_ID); - goto(`${base}/sequencing/new${workspaceId ? '?' + SearchParameters.WORKSPACE_ID + '=' + workspaceId : ''}`); - }} + on:click={navigateToNewSequence} > New Sequence diff --git a/src/utilities/sequence-editor/sequence-linter.ts b/src/utilities/sequence-editor/sequence-linter.ts index e043aaf569..a5c13eabe8 100644 --- a/src/utilities/sequence-editor/sequence-linter.ts +++ b/src/utilities/sequence-editor/sequence-linter.ts @@ -1178,13 +1178,11 @@ function validateArgument( } const { max, min } = dictArg.range; const nodeTextAsNumber = parseNumericArg(argText, dictArgType); - const isHex = isHexValue(argText); if (nodeTextAsNumber < min || nodeTextAsNumber > max) { - const numFormat = (n: number) => (isHex ? `0x${n.toString(16).toUpperCase()}` : n); const message = max !== min - ? `Number out of range. Range is between ${numFormat(min)} and ${numFormat(max)} inclusive.` - : `Number out of range. Range is ${numFormat(min)}.`; + ? `Number out of range. Range is between ${numFormat(argText, min)} and ${numFormat(argText, max)} inclusive.` + : `Number out of range. Range is ${numFormat(argText, min)}.`; diagnostics.push({ actions: max === min @@ -1349,6 +1347,10 @@ function validateArgument( return diagnostics; } +function numFormat(argText: string, num: number): number | string { + return isHexValue(argText) ? `0x${num.toString(16).toUpperCase()}` : num; +} + function validateId(commandNode: SyntaxNode, text: string): Diagnostic[] { const diagnostics: Diagnostic[] = []; const idNodes = commandNode.getChildren('IdDeclaration');