diff --git a/src/components/sequencing/SequenceEditor.svelte b/src/components/sequencing/SequenceEditor.svelte index 3df1d85d38..a666f61a4b 100644 --- a/src/components/sequencing/SequenceEditor.svelte +++ b/src/components/sequencing/SequenceEditor.svelte @@ -39,10 +39,7 @@ import { setupLanguageSupport } from '../../utilities/codemirror'; import effects from '../../utilities/effects'; import { downloadBlob, downloadJSON } from '../../utilities/generic'; - import { seqJsonLinter } from '../../utilities/sequence-editor/seq-json-linter'; - import { sequenceAutoIndent } from '../../utilities/sequence-editor/sequence-autoindent'; - import { sequenceCompletion } from '../../utilities/sequence-editor/sequence-completion'; - import { sequenceLinter } from '../../utilities/sequence-editor/sequence-linter'; + import { inputLinter, outputLinter } from '../../utilities/sequence-editor/extension-points'; import { sequenceTooltip } from '../../utilities/sequence-editor/sequence-tooltip'; import { showFailureToast, showSuccessToast } from '../../utilities/toast'; import { tooltip } from '../../utilities/tooltip'; @@ -73,6 +70,7 @@ let compartmentSeqLanguage: Compartment; let compartmentSeqLinter: Compartment; let compartmentSeqTooltip: Compartment; + let compartmentSeqAutocomplete: Compartment; let channelDictionary: ChannelDictionary | null; let commandDictionary: CommandDictionary | null; let disableCopyAndExport: boolean = true; @@ -88,11 +86,6 @@ let selectedOutputFormat: IOutputFormat | undefined; let toggleSeqJsonPreview: boolean = false; - $: { - outputFormats = $outputFormat; - selectedOutputFormat = outputFormats[0]; - } - $: { loadSequenceAdaptation(parcel?.sequence_adaptation_id); } @@ -154,26 +147,29 @@ // Reconfigure sequence editor. editorSequenceView.dispatch({ - effects: compartmentSeqLanguage.reconfigure( - setupLanguageSupport( - sequenceCompletion(parsedChannelDictionary, parsedCommandDictionary, nonNullParsedParameterDictionaries), + effects: [ + compartmentSeqLanguage.reconfigure( + setupLanguageSupport( + $sequenceAdaptation.autoComplete( + parsedChannelDictionary, + parsedCommandDictionary, + nonNullParsedParameterDictionaries, + ), + ), ), - ), - }); - editorSequenceView.dispatch({ - effects: compartmentSeqLinter.reconfigure( - sequenceLinter(parsedChannelDictionary, parsedCommandDictionary, nonNullParsedParameterDictionaries), - ), - }); - editorSequenceView.dispatch({ - effects: compartmentSeqTooltip.reconfigure( - sequenceTooltip(parsedChannelDictionary, parsedCommandDictionary, nonNullParsedParameterDictionaries), - ), + compartmentSeqLinter.reconfigure( + inputLinter(parsedChannelDictionary, parsedCommandDictionary, nonNullParsedParameterDictionaries), + ), + compartmentSeqTooltip.reconfigure( + sequenceTooltip(parsedChannelDictionary, parsedCommandDictionary, nonNullParsedParameterDictionaries), + ), + compartmentSeqAutocomplete.reconfigure(indentService.of($sequenceAdaptation.autoIndent())), + ], }); // Reconfigure seq JSON editor. editorOutputView.dispatch({ - effects: compartmentSeqJsonLinter.reconfigure(seqJsonLinter(parsedCommandDictionary, selectedOutputFormat)), + effects: compartmentSeqJsonLinter.reconfigure(outputLinter(parsedCommandDictionary, selectedOutputFormat)), }); }); } @@ -184,6 +180,7 @@ compartmentSeqLanguage = new Compartment(); compartmentSeqLinter = new Compartment(); compartmentSeqTooltip = new Compartment(); + compartmentSeqAutocomplete = new Compartment(); editorSequenceView = new EditorView({ doc: sequenceDefinition, @@ -192,12 +189,12 @@ EditorView.lineWrapping, EditorView.theme({ '.cm-gutter': { 'min-height': `${clientHeightGridRightTop}px` } }), lintGutter(), - compartmentSeqLanguage.of(setupLanguageSupport(sequenceCompletion(null, null, []))), - compartmentSeqLinter.of(sequenceLinter()), + compartmentSeqLanguage.of(setupLanguageSupport($sequenceAdaptation.autoComplete(null, null, []))), + compartmentSeqLinter.of(inputLinter()), compartmentSeqTooltip.of(sequenceTooltip()), EditorView.updateListener.of(debounce(sequenceUpdateListener, 250)), EditorView.updateListener.of(selectedCommandUpdateListener), - indentService.of(sequenceAutoIndent()), + compartmentSeqAutocomplete.of(indentService.of($sequenceAdaptation.autoIndent())), EditorState.readOnly.of(readOnly), ], parent: editorSequenceDiv, @@ -212,7 +209,7 @@ EditorView.editable.of(false), lintGutter(), json(), - compartmentSeqJsonLinter.of(seqJsonLinter()), + compartmentSeqJsonLinter.of(outputLinter()), EditorState.readOnly.of(readOnly), ], parent: editorOutputDiv, @@ -238,6 +235,9 @@ } else { resetSequenceAdaptation(); } + + outputFormats = $outputFormat; + selectedOutputFormat = outputFormats[0]; } function resetSequenceAdaptation(): void { @@ -278,14 +278,14 @@ const fileExtension = `${sequenceName}.${selectedOutputFormat?.fileExtension}`; if (outputFormat?.fileExtension === 'json') { - downloadJSON(editorOutputView.state.doc.toJSON(), fileExtension); + downloadJSON(JSON.parse(editorOutputView.state.doc.toString()), fileExtension); } else { downloadBlob(new Blob([editorOutputView.state.doc.toString()], { type: 'text/plain' }), fileExtension); } } function downloadInputFormat() { - downloadBlob(new Blob([editorOutputView.state.doc.toString()], { type: 'text/plain' }), `${sequenceName}.txt`); + downloadBlob(new Blob([editorSequenceView.state.doc.toString()], { type: 'text/plain' }), `${sequenceName}.txt`); } async function copyOutputFormatToClipboard() { diff --git a/src/components/sequencing/Sequences.svelte b/src/components/sequencing/Sequences.svelte index 61eb9b139d..76e3a73cae 100644 --- a/src/components/sequencing/Sequences.svelte +++ b/src/components/sequencing/Sequences.svelte @@ -23,7 +23,7 @@ export let user: User | null; - let filterText = ''; + let filterText: string = ''; let parcel: Parcel | null; let selectedSequence: UserSequence | null = null; let workspace: Workspace | undefined; @@ -32,7 +32,7 @@ $: parcel = $parcels.find(p => p.id === selectedSequence?.parcel_id) ?? null; $: workspace = $workspaces.find(workspace => workspace.id === workspaceId); $: if (selectedSequence !== null) { - const found = $userSequences.findIndex(sequence => sequence.id === selectedSequence?.id); + const found: number = $userSequences.findIndex(sequence => sequence.id === selectedSequence?.id); if (found === -1) { selectedSequence = null; diff --git a/src/components/workspace/WorkspaceTable.svelte b/src/components/workspace/WorkspaceTable.svelte index 93009ffae0..1f70aadf35 100644 --- a/src/components/workspace/WorkspaceTable.svelte +++ b/src/components/workspace/WorkspaceTable.svelte @@ -24,7 +24,7 @@ export let user: User | null; let baseColumnDefs: DataGridColumnDef[]; - let filterText = ''; + let filterText: string = ''; let filteredWorkspaces: Workspace[] = []; let selectedWorkspace: Workspace | null = null; diff --git a/src/stores/sequence-adaptation.ts b/src/stores/sequence-adaptation.ts index 6bb23adb7a..e70d301f71 100644 --- a/src/stores/sequence-adaptation.ts +++ b/src/stores/sequence-adaptation.ts @@ -3,12 +3,47 @@ import type { GlobalType } from '../types/global-type'; import type { ISequenceAdaptation, SequenceAdaptation } from '../types/sequencing'; import gql from '../utilities/gql'; import { seqJsonToSequence } from '../utilities/sequence-editor/from-seq-json'; +import { sequenceAutoIndent } from '../utilities/sequence-editor/sequence-autoindent'; +import { sequenceCompletion } from '../utilities/sequence-editor/sequence-completion'; import { sequenceToSeqJson } from '../utilities/sequence-editor/to-seq-json'; import { gqlSubscribable } from './subscribable'; +const defaultAdaptation: ISequenceAdaptation = { + argDelegator: undefined, + autoComplete: sequenceCompletion, + autoIndent: sequenceAutoIndent, + conditionalKeywords: { + else: 'CMD_ELSE', + elseIf: ['CMD_ELSE_IF'], + endIf: 'CMD_END_IF', + if: ['CMD_IF'], + }, + globals: [], + inputFormat: { + linter: undefined, + name: 'SeqN', + toInputFormat: seqJsonToSequence, + }, + loopKeywords: { + break: 'CMD_BREAK', + continue: 'CMD_CONTINUE', + endWhileLoop: 'CMD_END_WHILE_LOOP', + whileLoop: ['CMD_WHILE_LOOP', 'CMD_WHILE_LOOP_OR'], + }, + modifyOutput: undefined, + modifyOutputParse: undefined, + outputFormat: [ + { + fileExtension: 'json', + name: 'Seq JSON', + toOutputFormat: sequenceToSeqJson, + }, + ], +}; + /* Writeable */ -export const sequenceAdaptation: Writable = writable(undefined); +export const sequenceAdaptation: Writable = writable(defaultAdaptation); /* Subscriptions. */ @@ -26,38 +61,34 @@ export const outputFormat = derived( /* Helpers */ export function getGlobals(): GlobalType[] { - return get(sequenceAdaptation)?.globals ?? []; + return get(sequenceAdaptation).globals ?? []; } export function setSequenceAdaptation(newSequenceAdaptation: ISequenceAdaptation | undefined): void { sequenceAdaptation.set({ - argDelegator: newSequenceAdaptation?.argDelegator ?? undefined, + argDelegator: newSequenceAdaptation?.argDelegator ?? defaultAdaptation.argDelegator, + autoComplete: newSequenceAdaptation?.autoComplete ?? defaultAdaptation.autoComplete, + autoIndent: newSequenceAdaptation?.autoIndent ?? defaultAdaptation.autoIndent, conditionalKeywords: { - else: newSequenceAdaptation?.conditionalKeywords?.else ?? 'CMD_ELSE', - elseIf: newSequenceAdaptation?.conditionalKeywords?.elseIf ?? ['CMD_ELSE_IF'], - endIf: newSequenceAdaptation?.conditionalKeywords?.endIf ?? 'CMD_END_IF', - if: newSequenceAdaptation?.conditionalKeywords?.if ?? ['CMD_IF'], + else: newSequenceAdaptation?.conditionalKeywords?.else ?? defaultAdaptation.conditionalKeywords.else, + elseIf: newSequenceAdaptation?.conditionalKeywords?.elseIf ?? defaultAdaptation.conditionalKeywords.elseIf, + endIf: newSequenceAdaptation?.conditionalKeywords?.endIf ?? defaultAdaptation.conditionalKeywords.endIf, + if: newSequenceAdaptation?.conditionalKeywords?.if ?? defaultAdaptation.conditionalKeywords.if, }, - globals: newSequenceAdaptation?.globals ?? [], + globals: newSequenceAdaptation?.globals ?? defaultAdaptation.globals, inputFormat: { - linter: newSequenceAdaptation?.inputFormat?.linter ?? undefined, - name: newSequenceAdaptation?.inputFormat?.name ?? 'SeqN', - toInputFormat: newSequenceAdaptation?.inputFormat?.toInputFormat ?? seqJsonToSequence, + linter: newSequenceAdaptation?.inputFormat?.linter ?? defaultAdaptation.inputFormat.linter, + name: newSequenceAdaptation?.inputFormat?.name ?? defaultAdaptation.inputFormat.name, + toInputFormat: newSequenceAdaptation?.inputFormat?.toInputFormat ?? defaultAdaptation.inputFormat.toInputFormat, }, loopKeywords: { - break: newSequenceAdaptation?.loopKeywords?.break ?? 'CMD_BREAK', - continue: newSequenceAdaptation?.loopKeywords?.continue ?? 'CMD_CONTINUE', - endWhileLoop: newSequenceAdaptation?.loopKeywords?.endWhileLoop ?? 'CMD_END_WHILE_LOOP', - whileLoop: newSequenceAdaptation?.loopKeywords?.whileLoop ?? ['CMD_WHILE_LOOP', 'CMD_WHILE_LOOP_OR'], + break: newSequenceAdaptation?.loopKeywords?.break ?? defaultAdaptation.loopKeywords.break, + continue: newSequenceAdaptation?.loopKeywords?.continue ?? defaultAdaptation.loopKeywords.continue, + endWhileLoop: newSequenceAdaptation?.loopKeywords?.endWhileLoop ?? defaultAdaptation.loopKeywords.endWhileLoop, + whileLoop: newSequenceAdaptation?.loopKeywords?.whileLoop ?? defaultAdaptation.loopKeywords.whileLoop, }, - modifyOutput: newSequenceAdaptation?.modifyOutput ?? undefined, - modifyOutputParse: newSequenceAdaptation?.modifyOutputParse ?? undefined, - outputFormat: newSequenceAdaptation?.outputFormat ?? [ - { - fileExtension: 'json', - name: 'Seq JSON', - toOutputFormat: sequenceToSeqJson, - }, - ], + modifyOutput: newSequenceAdaptation?.modifyOutput ?? defaultAdaptation.modifyOutput, + modifyOutputParse: newSequenceAdaptation?.modifyOutputParse ?? defaultAdaptation.modifyOutputParse, + outputFormat: newSequenceAdaptation?.outputFormat ?? defaultAdaptation.outputFormat, }); } diff --git a/src/types/sequencing.ts b/src/types/sequencing.ts index 0dcd26ef31..ae3bbf1828 100644 --- a/src/types/sequencing.ts +++ b/src/types/sequencing.ts @@ -1,3 +1,5 @@ +import type { CompletionContext, CompletionResult } from '@codemirror/autocomplete'; +import type { IndentContext } from '@codemirror/language'; import type { Diagnostic } from '@codemirror/lint'; import type { SyntaxNode } from '@lezer/common'; import type { @@ -57,6 +59,12 @@ export interface IOutputFormat { export interface ISequenceAdaptation { argDelegator?: ArgDelegator; + autoComplete: ( + channelDictionary: AmpcsChannelDictionary | null, + commandDictionary: AmpcsCommandDictionary | null, + parameterDictionaries: AmpcsParameterDictionary[], + ) => (context: CompletionContext) => CompletionResult | null; + autoIndent: () => (context: IndentContext, pos: number) => number | null | undefined; conditionalKeywords: { else: string; elseIf: string[]; endIf: string; if: string[] }; globals?: GlobalType[]; inputFormat: { diff --git a/src/utilities/sequence-editor/extension-points.ts b/src/utilities/sequence-editor/extension-points.ts index 4e81a5956e..225bca51a8 100644 --- a/src/utilities/sequence-editor/extension-points.ts +++ b/src/utilities/sequence-editor/extension-points.ts @@ -1,6 +1,17 @@ -import { type ChannelDictionary, type FswCommandArgument, type ParameterDictionary } from '@nasa-jpl/aerie-ampcs'; +import { syntaxTree } from '@codemirror/language'; +import { linter, type Diagnostic } from '@codemirror/lint'; +import type { Extension } from '@codemirror/state'; +import { + type ChannelDictionary, + type CommandDictionary, + type FswCommandArgument, + type ParameterDictionary, +} from '@nasa-jpl/aerie-ampcs'; import { get } from 'svelte/store'; import { inputFormat, sequenceAdaptation } from '../../stores/sequence-adaptation'; +import type { IOutputFormat } from '../../types/sequencing'; +import { seqJsonLinter } from './seq-json-linter'; +import { sequenceLinter } from './sequence-linter'; // TODO: serialization // replace parameter names with hex ids @@ -30,8 +41,8 @@ export function getCustomArgDef( ) { let delegate = undefined; - if (get(sequenceAdaptation)?.argDelegator !== undefined) { - delegate = get(sequenceAdaptation)?.argDelegator?.[stem]?.[dictArg.name]; + if (get(sequenceAdaptation).argDelegator !== undefined) { + delegate = get(sequenceAdaptation).argDelegator?.[stem]?.[dictArg.name]; } return delegate?.(dictArg, parameterDictionaries, channelDictionary, precedingArgs) ?? dictArg; @@ -42,7 +53,7 @@ export async function toInputFormat( parameterDictionaries: ParameterDictionary[], channelDictionary: ChannelDictionary | null, ) { - const modifyOutputParse = get(sequenceAdaptation)?.modifyOutputParse; + const modifyOutputParse = get(sequenceAdaptation).modifyOutputParse; let modifiedOutput = null; if (modifyOutputParse !== undefined) { @@ -53,3 +64,44 @@ export async function toInputFormat( return input; } + +export function inputLinter( + channelDictionary: ChannelDictionary | null = null, + commandDictionary: CommandDictionary | null = null, + parameterDictionaries: ParameterDictionary[] = [], +): Extension { + return linter(view => { + const inputLinter = get(sequenceAdaptation).inputFormat.linter; + const tree = syntaxTree(view.state); + const treeNode = tree.topNode; + let diagnostics: Diagnostic[]; + + diagnostics = sequenceLinter(view, channelDictionary, commandDictionary, parameterDictionaries); + + if (inputLinter !== undefined && commandDictionary !== null) { + diagnostics = inputLinter(diagnostics, commandDictionary, view, treeNode); + } + + return diagnostics; + }); +} + +export function outputLinter( + commandDictionary: CommandDictionary | null = null, + outputFormat: IOutputFormat | undefined = undefined, +): Extension { + return linter(view => { + const tree = syntaxTree(view.state); + const treeNode = tree.topNode; + const outputLinter = outputFormat?.linter; + let diagnostics: Diagnostic[]; + + diagnostics = seqJsonLinter(view, commandDictionary); + + if (outputLinter !== undefined && commandDictionary !== null) { + diagnostics = outputLinter(diagnostics, commandDictionary, view, treeNode); + } + + return diagnostics; + }); +} diff --git a/src/utilities/sequence-editor/seq-json-linter.ts b/src/utilities/sequence-editor/seq-json-linter.ts index ce46d259a9..b27240dec9 100644 --- a/src/utilities/sequence-editor/seq-json-linter.ts +++ b/src/utilities/sequence-editor/seq-json-linter.ts @@ -1,10 +1,9 @@ -import { syntaxTree } from '@codemirror/language'; -import { linter, type Diagnostic } from '@codemirror/lint'; -import type { Extension, Text } from '@codemirror/state'; +import { type Diagnostic } from '@codemirror/lint'; +import type { Text } from '@codemirror/state'; import type { CommandDictionary } from '@nasa-jpl/aerie-ampcs'; +import type { EditorView } from 'codemirror'; // @ts-expect-error library does not include type declarations import { parse as jsonSourceMapParse } from 'json-source-map'; -import type { IOutputFormat } from '../../types/sequencing'; type JsonSourceMapPointerPosition = { column: number; @@ -41,62 +40,49 @@ function getErrorPosition(error: SyntaxError, doc: Text): number { * Linter function that returns a Code Mirror extension function. * Can be optionally called with a command dictionary so it's available during linting. */ -export function seqJsonLinter( - commandDictionary: CommandDictionary | null = null, - outputFormat: IOutputFormat | undefined = undefined, -): Extension { - return linter(view => { - let diagnostics: Diagnostic[] = []; - const tree = syntaxTree(view.state); - const treeNode = tree.topNode; +export function seqJsonLinter(view: EditorView, commandDictionary: CommandDictionary | null = null): Diagnostic[] { + const diagnostics: Diagnostic[] = []; - try { - const text = view.state.doc.toString(); - const sourceMap = jsonSourceMapParse(text); + try { + const text = view.state.doc.toString(); + const sourceMap = jsonSourceMapParse(text); - if (commandDictionary) { - for (const [key, pointer] of Object.entries(sourceMap.pointers)) { - const stemMatch = key.match(/\/steps\/\d+\/stem/); + if (commandDictionary) { + for (const [key, pointer] of Object.entries(sourceMap.pointers)) { + const stemMatch = key.match(/\/steps\/\d+\/stem/); - if (stemMatch) { - const stemValue = view.state.doc.sliceString(pointer.value.pos, pointer.valueEnd.pos); - const stemValueNoQuotes = stemValue.replaceAll('"', ''); - const hasFswCommand = commandDictionary.fswCommandMap[stemValueNoQuotes] ?? false; - const hasHwCommand = commandDictionary.hwCommandMap[stemValueNoQuotes] ?? false; - const hasCommand = hasFswCommand || hasHwCommand; + if (stemMatch) { + const stemValue = view.state.doc.sliceString(pointer.value.pos, pointer.valueEnd.pos); + const stemValueNoQuotes = stemValue.replaceAll('"', ''); + const hasFswCommand = commandDictionary.fswCommandMap[stemValueNoQuotes] ?? false; + const hasHwCommand = commandDictionary.hwCommandMap[stemValueNoQuotes] ?? false; + const hasCommand = hasFswCommand || hasHwCommand; - if (!hasCommand) { - diagnostics.push({ - actions: [], - from: pointer.value.pos, - message: 'Command not found', - severity: 'error', - to: pointer.valueEnd.pos, - }); - } + if (!hasCommand) { + diagnostics.push({ + actions: [], + from: pointer.value.pos, + message: 'Command not found', + severity: 'error', + to: pointer.valueEnd.pos, + }); } } } - } catch (e) { - if (!(e instanceof SyntaxError)) { - throw e; - } - const pos = getErrorPosition(e, view.state.doc); - - diagnostics.push({ - from: pos, - message: e.message, - severity: 'error', - to: pos, - }); } - - const outputLinter = outputFormat?.linter; - - if (outputLinter !== undefined && commandDictionary !== null) { - diagnostics = outputLinter(diagnostics, commandDictionary, view, treeNode); + } catch (e) { + if (!(e instanceof SyntaxError)) { + throw e; } + const pos = getErrorPosition(e, view.state.doc); + + diagnostics.push({ + from: pos, + message: e.message, + severity: 'error', + to: pos, + }); + } - return diagnostics; - }); + return diagnostics; } diff --git a/src/utilities/sequence-editor/sequence-linter.ts b/src/utilities/sequence-editor/sequence-linter.ts index 7e0624e9e5..2f93024068 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 type { SyntaxNode, Tree } from '@lezer/common'; import type { ChannelDictionary, @@ -20,7 +19,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, parseNumericArg, isHexValue } from '../codemirror/codemirror-utils'; +import { addDefaultArgs, quoteEscape } from '../codemirror/codemirror-utils'; import { getBalancedDuration, getDoyTime, @@ -77,993 +76,1090 @@ 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(); + 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; - } - - // 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) || [], docText), + ...validateRequests( + commandsNode.getChildren(TOKEN_REQUEST), + docText, + variableMap, + commandDictionary, + channelDictionary, + parameterDictionaries, + ), ); + } - const inputLinter = get(sequenceAdaptation)?.inputFormat.linter; + 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) || [], docText), + ); + + const inputLinter = get(sequenceAdaptation)?.inputFormat.linter; + + if (inputLinter !== undefined && commandDictionary !== null) { + diagnostics = inputLinter(diagnostics, commandDictionary, view, treeNode); + } - if (inputLinter !== undefined && commandDictionary !== null) { - diagnostics = inputLinter(diagnostics, commandDictionary, view, treeNode); - } + 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[], 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, - ); - - for (const command of commandNodes) { - const stem = command.getChild('Stem'); - if (stem) { - const word = text.slice(stem.from, stem.to); +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, + ); + + 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 (sequenceAdaptationConditionalKeywords?.if.includes(word)) { - conditionalStack.push({ - command, + if (conditionalKeywords.includes(word)) { + if (conditionalStack.length === 0) { + diagnostics.push({ from: stem.from, - hasElse: false, - stemToClose: sequenceAdaptationConditionalKeywords.endIf, + message: `${word} doesn't match a preceding ${sequenceAdaptationConditionalKeywords?.if.join(', ')}.`, + severity: 'error', to: stem.to, - word, }); - } - - if (conditionalKeywords.includes(word)) { - if (conditionalStack.length === 0) { + } 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?.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(); } + } 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 (sequenceAdaptationLoopKeywords?.whileLoop.includes(word)) { - loopStack.push({ - command, + if (loopKeywords.includes(word)) { + if (loopStack.length === 0) { + diagnostics.push({ from: stem.from, - stemToClose: sequenceAdaptationLoopKeywords.endWhileLoop, + message: `${word} doesn't match a preceding ${sequenceAdaptationLoopKeywords?.whileLoop.join(', ')}.`, + severity: 'error', 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(); - } + 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}`, + // 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`, + }, + }); }, - ], - from: block.from, - message: `Unclosed ${block.word}`, - severity: 'error', - to: block.to, - } as const; - }), - ); - - return diagnostics; - } + name: `Insert ${block.stemToClose}`, + }, + ], + from: block.from, + message: `Unclosed ${block.word}`, + severity: 'error', + to: block.to, + } as const; + }), + ); - 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, _to: 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, _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, + 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 (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(absoluteText, TimeTypes.ABSOLUTE)) { + if (!isTimeBalanced(epochText, TimeTypes.EPOCH)) { diagnostics.push({ actions: [], - from: timeTagAbsoluteNode.from, - message: CustomErrorCodes.UnbalancedTime(getDoyTime(new Date(getUnixEpochTime(absoluteText)))).message, + from: timeTagEpochNode.from, + message: CustomErrorCodes.UnbalancedTime(getBalancedDuration(epochText)).message, severity: 'warning', - to: timeTagAbsoluteNode.to, + to: timeTagEpochNode.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, - 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)) { + } + } 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; + 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; +} - 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 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; - 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; - } + 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`, - 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, _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`, + break; + case 'hardware': + if (!hwCommandMap[stemText]) { + return { + from: stem.from, + message: 'Command must be a hardware command', 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, _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; } + } 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. + * @@ -1073,465 +1169,476 @@ 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 = 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}.`; 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; +} - const diagnostics: Diagnostic[] = []; +function validateModel(commandNode: SyntaxNode): Diagnostic[] { + const models = commandNode.getChild('Models')?.getChildren('Model'); + if (!models) { + return []; + } - // Iterate over each metadata entry node - metadataEntry.forEach(entry => { - // Get the children nodes of the metadata entry node - const metadataNodeChildren = getChildrenNode(entry); + const diagnostics: Diagnostic[] = []; - 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) { + 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({ - 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; }