From f86c45d87fb2ba99d07ec6ceed0d60d6869ac244 Mon Sep 17 00:00:00 2001 From: rrgoetz Date: Tue, 5 Nov 2024 12:11:23 -1000 Subject: [PATCH] Hook up the linter with library sequences. * The linter will typechecking the library sequences parameters/arguments --- src/utilities/codemirror/codemirror-utils.ts | 43 ++ src/utilities/codemirror/seq-n-tree-utils.ts | 6 +- .../sequence-editor/extension-points.ts | 5 +- .../sequence-editor/sequence-linter.ts | 399 ++++++++++++++---- src/utilities/sequence-editor/to-seq-json.ts | 2 +- 5 files changed, 378 insertions(+), 77 deletions(-) diff --git a/src/utilities/codemirror/codemirror-utils.ts b/src/utilities/codemirror/codemirror-utils.ts index 70c0c56abd..55d04cd93c 100644 --- a/src/utilities/codemirror/codemirror-utils.ts +++ b/src/utilities/codemirror/codemirror-utils.ts @@ -12,6 +12,7 @@ import type { FswCommandArgumentUnsigned, FswCommandArgumentVarString, } from '@nasa-jpl/aerie-ampcs'; +import type { VariableDeclaration } from '@nasa-jpl/seq-json-schema/types'; import type { EditorView } from 'codemirror'; import { fswCommandArgDefault } from '../sequence-editor/command-dictionary'; import type { CommandInfoMapper } from './commandInfoMapper'; @@ -107,6 +108,48 @@ export function getMissingArgDefs(argInfoArray: ArgTextDef[]): FswCommandArgumen .map(argInfo => argInfo.argDef); } +export function getDefaultVariableArgs(parameters: VariableDeclaration[]): string[] { + return parameters.map(parameter => { + switch (parameter.type) { + case 'STRING': + return `"${parameter.name}"`; + case 'FLOAT': + return parameter.allowable_ranges && parameter.allowable_ranges.length > 0 + ? parameter.allowable_ranges[0].min + : '0'; + case 'INT': + case 'UINT': + return parameter.allowable_ranges && parameter.allowable_ranges.length > 0 + ? parameter.allowable_ranges[0].min + : '0'; + case 'ENUM': + return parameter.allowable_values && parameter.allowable_values.length > 0 + ? `"${parameter.allowable_values[0]}"` + : parameter.enum_name + ? `${parameter.enum_name}` + : 'UNKNOWN'; + default: + throw Error(`unknown argument type ${parameter.type}`); + } + }) as string[]; +} + +export function addDefaultVariableArgs( + parameters: VariableDeclaration[], + view: EditorView, + commandNode: SyntaxNode, + commandInfoMapper: CommandInfoMapper, +) { + const insertPosition = commandInfoMapper.getArgumentAppendPosition(commandNode); + if (insertPosition !== undefined) { + const str = commandInfoMapper.formatArgumentArray(getDefaultVariableArgs(parameters), commandNode); + const transaction = view.state.update({ + changes: { from: insertPosition, insert: str }, + }); + view.dispatch(transaction); + } +} + export function isQuoted(s: string): boolean { return s.startsWith('"') && s.endsWith('"'); } diff --git a/src/utilities/codemirror/seq-n-tree-utils.ts b/src/utilities/codemirror/seq-n-tree-utils.ts index 12045baa5a..9a2fdcd46c 100644 --- a/src/utilities/codemirror/seq-n-tree-utils.ts +++ b/src/utilities/codemirror/seq-n-tree-utils.ts @@ -58,7 +58,11 @@ export class SeqNCommandInfoMapper implements CommandInfoMapper { } getArgumentAppendPosition(commandOrRepeatArgNode: SyntaxNode | null): number | undefined { - if (commandOrRepeatArgNode?.name === RULE_COMMAND) { + if ( + commandOrRepeatArgNode?.name === RULE_COMMAND || + commandOrRepeatArgNode?.name === TOKEN_ACTIVATE || + commandOrRepeatArgNode?.name === TOKEN_LOAD + ) { const argsNode = commandOrRepeatArgNode.getChild('Args'); const stemNode = commandOrRepeatArgNode.getChild('Stem'); return getFromAndTo([stemNode, argsNode]).to; diff --git a/src/utilities/sequence-editor/extension-points.ts b/src/utilities/sequence-editor/extension-points.ts index ac7700aec1..cb5e782ee2 100644 --- a/src/utilities/sequence-editor/extension-points.ts +++ b/src/utilities/sequence-editor/extension-points.ts @@ -9,7 +9,7 @@ import { } from '@nasa-jpl/aerie-ampcs'; import { get } from 'svelte/store'; import { inputFormat, sequenceAdaptation } from '../../stores/sequence-adaptation'; -import type { IOutputFormat } from '../../types/sequencing'; +import type { IOutputFormat, LibrarySequence } from '../../types/sequencing'; import { seqJsonLinter } from './seq-json-linter'; import { sequenceLinter } from './sequence-linter'; @@ -81,6 +81,7 @@ export function inputLinter( channelDictionary: ChannelDictionary | null = null, commandDictionary: CommandDictionary | null = null, parameterDictionaries: ParameterDictionary[] = [], + librarySequences: LibrarySequence[] = [], ): Extension { return linter(view => { const inputLinter = get(sequenceAdaptation).inputFormat.linter; @@ -88,7 +89,7 @@ export function inputLinter( const treeNode = tree.topNode; let diagnostics: Diagnostic[]; - diagnostics = sequenceLinter(view, channelDictionary, commandDictionary, parameterDictionaries); + diagnostics = sequenceLinter(view, channelDictionary, commandDictionary, parameterDictionaries, librarySequences); if (inputLinter !== undefined && commandDictionary !== null) { diagnostics = inputLinter(diagnostics, commandDictionary, view, treeNode); diff --git a/src/utilities/sequence-editor/sequence-linter.ts b/src/utilities/sequence-editor/sequence-linter.ts index 2ccc4952b8..87f90b74d1 100644 --- a/src/utilities/sequence-editor/sequence-linter.ts +++ b/src/utilities/sequence-editor/sequence-linter.ts @@ -15,11 +15,27 @@ import { closest, distance } from 'fastest-levenshtein'; import type { VariableDeclaration } from '@nasa-jpl/seq-json-schema/types'; import type { EditorView } from 'codemirror'; -import { TOKEN_COMMAND, TOKEN_ERROR, TOKEN_REPEAT_ARG, TOKEN_REQUEST } from '../../constants/seq-n-grammar-constants'; +import { + RULE_ARGS, + RULE_SEQUENCE_NAME, + TOKEN_ACTIVATE, + TOKEN_COMMAND, + TOKEN_ERROR, + TOKEN_LOAD, + TOKEN_REPEAT_ARG, + TOKEN_REQUEST, +} from '../../constants/seq-n-grammar-constants'; import { TimeTypes } from '../../enums/time'; import { getGlobals } from '../../stores/sequence-adaptation'; +import type { LibrarySequence } from '../../types/sequencing'; import { CustomErrorCodes } from '../../workers/customCodes'; -import { addDefaultArgs, isHexValue, parseNumericArg, quoteEscape } from '../codemirror/codemirror-utils'; +import { + addDefaultArgs, + addDefaultVariableArgs, + isHexValue, + parseNumericArg, + quoteEscape, +} from '../codemirror/codemirror-utils'; import { closeSuggestion, computeBlocks, openSuggestion } from '../codemirror/custom-folder'; import { SeqNCommandInfoMapper } from '../codemirror/seq-n-tree-utils'; import { @@ -72,6 +88,7 @@ export function sequenceLinter( channelDictionary: ChannelDictionary | null = null, commandDictionary: CommandDictionary | null = null, parameterDictionaries: ParameterDictionary[] = [], + librarySequences: LibrarySequence[] = [], ): Diagnostic[] { const tree = syntaxTree(view.state); const treeNode = tree.topNode; @@ -130,6 +147,10 @@ export function sequenceLinter( parameterDictionaries, ), ); + diagnostics.push( + ...validateActivateLoad(commandsNode.getChildren(TOKEN_ACTIVATE), 'Activate', docText, librarySequences), + ...validateActivateLoad(commandsNode.getChildren(TOKEN_LOAD), 'Load', docText, librarySequences), + ); } diagnostics.push( @@ -473,6 +494,141 @@ function getVariableInfo( }; } +function validateActivateLoad( + node: SyntaxNode[], + type: 'Activate' | 'Load', + text: string, + librarySequences: LibrarySequence[], +): Diagnostic[] { + if (node.length === 0) { + return []; + } + + const diagnostics: Diagnostic[] = []; + + node.forEach(activate => { + const sequenceName = activate.getChild(RULE_SEQUENCE_NAME); + const argNode = activate.getChild(RULE_ARGS); + + if (sequenceName === null || argNode === null) { + return; + } + const library = librarySequences.find( + library => library.name === text.slice(sequenceName.from, sequenceName.to).replace(/^"|"$/g, ''), + ); + const argsNode = getChildrenNode(argNode); + if (!library) { + diagnostics.push({ + from: sequenceName.from, + message: `Sequence doesn't exist ${text.slice(sequenceName.from, sequenceName.to)}`, + severity: 'warning', + to: sequenceName.to, + }); + } else { + const structureError = validateCommandStructure(activate, argsNode, library.parameters.length, (view: any) => { + addDefaultVariableArgs(library.parameters.slice(argsNode.length), view, activate, new SeqNCommandInfoMapper()); + }); + if (structureError) { + diagnostics.push(structureError); + return diagnostics; + } + + library?.parameters.forEach((parameter, index) => { + const arg = argsNode[index]; + switch (parameter.type) { + case 'STRING': { + if (arg.name !== 'String') { + diagnostics.push({ + from: arg.from, + message: `"${parameter.name}" must be a string`, + severity: 'error', + to: arg.to, + }); + } + break; + } + case 'FLOAT': + case 'INT': + case 'UINT': + { + let value = 0; + const num = text.slice(arg.from, arg.to); + if (parameter.type === 'FLOAT') { + value = parseFloat(num); + } else { + value = parseInt(num); + } + parameter.allowable_ranges?.forEach(range => { + if (value < range.min || value > range.max) { + diagnostics.push({ + from: arg.from, + message: `Value must be between ${range.min} and ${range.max}`, + severity: 'error', + to: arg.to, + }); + } + }); + + if (parameter.type === 'UINT') { + if (value < 0) { + diagnostics.push({ + from: arg.from, + message: `UINT must be greater than or equal to zero`, + severity: 'error', + to: arg.to, + }); + } + } + if (arg.name !== 'Number') { + diagnostics.push({ + from: arg.from, + message: `"${parameter.name}" must be a number`, + severity: 'error', + to: arg.to, + }); + } + } + break; + case 'ENUM': + { + if (arg.name === 'Number' || arg.name === 'Boolean') { + diagnostics.push({ + from: arg.from, + message: `"${parameter.name}" must be an enum`, + severity: 'error', + to: arg.to, + }); + } else if (arg.name !== 'String') { + diagnostics.push({ + actions: [], + from: argNode.from, + message: `Incorrect type - expected double quoted 'enum' but got ${arg.name}`, + severity: 'error', + to: argNode.to, + }); + } + const enumValue = text.slice(arg.from, arg.to).replace(/^"|"$/g, ''); + if (parameter.allowable_values?.indexOf(enumValue) === -1) { + diagnostics.push({ + from: arg.from, + message: `Enum should be "${parameter.allowable_values?.slice(0, MAX_ENUMS_TO_SHOW).join(' | ')}${parameter.allowable_values!.length > MAX_ENUMS_TO_SHOW ? '...' : ''}"`, + severity: 'error', + to: arg.to, + }); + } + } + + break; + default: + break; + } + }); + } + }); + + return diagnostics; +} + function validateCustomDirectives(node: SyntaxNode, text: string): Diagnostic[] { const diagnostics: Diagnostic[] = []; node.getChildren('GenericDirective').forEach(directiveNode => { @@ -988,79 +1144,96 @@ function validateAndLintArguments( 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 (dictArgs.length > 0) { + // if (!argNode || argNode.length === 0) { + // diagnostics.push({ + // actions: [], + // from: command.from, + // message: '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 ${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) { + // if (commandDictionary) { + // addDefaultArgs( + // commandDictionary, + // view, + // command, + // dictArgs.slice(argNode.length), + // new SeqNCommandInfoMapper(), + // ); + // } + // }, + // 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 structureError = validateCommandStructure(command, argNode, dictArgs.length, (view: any) => { + if (commandDictionary) { + addDefaultArgs( + commandDictionary, + view, + command, + dictArgs.slice(argNode ? argNode.length : 0), + new SeqNCommandInfoMapper(), + ); } + }); - 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) { - if (commandDictionary) { - addDefaultArgs( - commandDictionary, - view, - command, - dictArgs.slice(argNode.length), - new SeqNCommandInfoMapper(), - ); - } - }, - 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, - }); + if (structureError) { + diagnostics.push(structureError); return diagnostics; } @@ -1112,6 +1285,86 @@ function validateAndLintArguments( return diagnostics; } +/** + * Validates the command structure. + * @param stemNode - The SyntaxNode representing the command stem. + * @param argsNode - The SyntaxNode representing the command arguments. + * @param exactArgSize - The expected number of arguments. + * @param addDefault - The function to add default arguments. + * @returns A Diagnostic object representing the validation error, or undefined if there is no error. + */ +function validateCommandStructure( + stemNode: SyntaxNode, + argsNode: SyntaxNode[] | null, + exactArgSize: number, + addDefault: (view: any) => any, +): Diagnostic | undefined { + if (arguments.length > 0) { + if (!argsNode || argsNode.length === 0) { + return { + actions: [], + from: stemNode.from, + message: `The stem is missing arguments.`, + severity: 'error', + to: stemNode.to, + }; + } + if (argsNode.length > exactArgSize) { + const extraArgs = argsNode.slice(exactArgSize); + const { from, to } = getFromAndTo(extraArgs); + return { + 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 ${exactArgSize}, but ${argsNode.length} are present`, + severity: 'error', + to, + }; + } + if (argsNode.length < exactArgSize) { + const { from, to } = getFromAndTo(argsNode); + const pluralS = exactArgSize > argsNode.length + 1 ? 's' : ''; + return { + actions: [ + { + apply(view) { + addDefault(view); + }, + name: `Add default missing argument${pluralS}`, + }, + ], + from, + message: `Missing argument${pluralS}, definition has ${argsNode.length}, but ${exactArgSize} are present`, + severity: 'error', + to, + }; + } + } else if (argsNode && argsNode.length > 0) { + const { from, to } = getFromAndTo(argsNode); + return { + actions: [ + { + apply(view, from, to) { + view.dispatch({ changes: { from, to } }); + }, + name: `Remove argument${argsNode.length > 1 ? 's' : ''}`, + }, + ], + from: from, + message: 'The command should not have arguments', + severity: 'error', + to: to, + }; + } + return undefined; +} + /** + * Validates the given FSW command argument against the provided syntax node, + * and generates diagnostics if the validation fails. diff --git a/src/utilities/sequence-editor/to-seq-json.ts b/src/utilities/sequence-editor/to-seq-json.ts index bb863b5d23..a885e9d98a 100644 --- a/src/utilities/sequence-editor/to-seq-json.ts +++ b/src/utilities/sequence-editor/to-seq-json.ts @@ -442,7 +442,7 @@ function parseTime(commandNode: SyntaxNode, text: string): Time { // min length of one type VariableDeclarationArray = [VariableDeclaration, ...VariableDeclaration[]]; -function parseVariables( +export function parseVariables( node: SyntaxNode, text: string, type: 'LocalDeclaration' | 'ParameterDeclaration' = 'LocalDeclaration',