From 8a3f0bc3005f200d3bbd71a369efbcf53818321e Mon Sep 17 00:00:00 2001 From: MaxAtoms <7847075+MaxAtoms@users.noreply.github.com> Date: Thu, 23 Oct 2025 10:21:35 +0200 Subject: [PATCH 01/20] feat-fix(repl): respect new arg in queries Previously, all queries would be returned when trying to complete ':query @dataflow '. --- src/cli/repl/core.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cli/repl/core.ts b/src/cli/repl/core.ts index c48101c3648..c94ce66a339 100644 --- a/src/cli/repl/core.ts +++ b/src/cli/repl/core.ts @@ -53,7 +53,7 @@ export function replCompleter(line: string, config: FlowrConfigOptions): [string const options = scripts[commandName as keyof typeof scripts].options; completions.push(...getValidOptionsForCompletion(options, splitLine).map(o => `${o} `)); } else if(commandName.startsWith('query')) { - completions.push(...replQueryCompleter(splitLine, config)); + completions.push(...replQueryCompleter(splitLine, startingNewArg, config)); } else { // autocomplete command arguments (specifically, autocomplete the file:// protocol) completions.push(fileProtocol); @@ -68,11 +68,11 @@ export function replCompleter(line: string, config: FlowrConfigOptions): [string return [replCompleterKeywords().filter(k => k.startsWith(line)).map(k => `${k} `), line]; } -function replQueryCompleter(splitLine: readonly string[], config: FlowrConfigOptions): string[] { +function replQueryCompleter(splitLine: readonly string[], startingNewArg: boolean, config: FlowrConfigOptions): string[] { const nonEmpty = splitLine.slice(1).map(s => s.trim()).filter(s => s.length > 0); const queryShorts = Object.keys(SupportedQueries).map(q => `@${q}`).concat(['help']); let candidates: string[] = []; - if(nonEmpty.length == 0 || (nonEmpty.length == 1 && queryShorts.some(q => q.startsWith(nonEmpty[0]) && nonEmpty[0] !== q))) { + if(nonEmpty.length == 0 || (nonEmpty.length == 1 && queryShorts.some(q => q.startsWith(nonEmpty[0]) && nonEmpty[0] !== q && !startingNewArg))) { candidates = candidates.concat(queryShorts.map(q => `${q} `)); } else { const q = nonEmpty[0].slice(1); From 1406e28e871370bcb20f507a666d58e70f95240e Mon Sep 17 00:00:00 2001 From: MaxAtoms <7847075+MaxAtoms@users.noreply.github.com> Date: Thu, 23 Oct 2025 12:23:31 +0200 Subject: [PATCH 02/20] feat-fix(repl): parse help command correctly --- src/cli/repl/commands/repl-query.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/repl/commands/repl-query.ts b/src/cli/repl/commands/repl-query.ts index 427d6bb12b3..fd2af83323d 100644 --- a/src/cli/repl/commands/repl-query.ts +++ b/src/cli/repl/commands/repl-query.ts @@ -79,7 +79,7 @@ async function processQueryArgs(output: ReplOutput, analyzer: FlowrAnalysisProvi function parseArgs(line: string) { const args = splitAtEscapeSensitive(line); return { - input: args[1].trim() === 'help' ? '' : args.slice(1).join(' ').trim(), + input: args[0].trim() === 'help' ? '' : args.slice(1).join(' ').trim(), remaining: args }; } From 167041d5f47d85f5bb2eee90f43d69a1e90c1e0b Mon Sep 17 00:00:00 2001 From: MaxAtoms <7847075+MaxAtoms@users.noreply.github.com> Date: Thu, 23 Oct 2025 15:43:49 +0200 Subject: [PATCH 03/20] feat(repl): add line parser for linter --- src/cli/repl/commands/repl-query.ts | 3 +- .../linter-query/linter-query-format.ts | 29 +++++++++++++++++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/cli/repl/commands/repl-query.ts b/src/cli/repl/commands/repl-query.ts index fd2af83323d..aa816ad4972 100644 --- a/src/cli/repl/commands/repl-query.ts +++ b/src/cli/repl/commands/repl-query.ts @@ -79,7 +79,8 @@ async function processQueryArgs(output: ReplOutput, analyzer: FlowrAnalysisProvi function parseArgs(line: string) { const args = splitAtEscapeSensitive(line); return { - input: args[0].trim() === 'help' ? '' : args.slice(1).join(' ').trim(), + // TODO Discuss future solution so that parentheses around code are not needed + input: args[0].trim() === 'help' ? '' : args[args.length - 1], remaining: args }; } diff --git a/src/queries/catalog/linter-query/linter-query-format.ts b/src/queries/catalog/linter-query/linter-query-format.ts index 4fb9e1ff9cc..c864fb71b75 100644 --- a/src/queries/catalog/linter-query/linter-query-format.ts +++ b/src/queries/catalog/linter-query/linter-query-format.ts @@ -2,10 +2,15 @@ import type { BaseQueryFormat, BaseQueryResult } from '../../base-query-format'; import type { QueryResults, SupportedQuery } from '../../query'; import Joi from 'joi'; import { executeLinterQuery } from './linter-query-executor'; -import type { LintingRuleConfig, LintingRuleMetadata, LintingRuleNames, LintingRuleResult } from '../../../linter/linter-rules'; +import type { + LintingRuleConfig, + LintingRuleMetadata, + LintingRuleNames, + LintingRuleResult +} from '../../../linter/linter-rules'; import { LintingRules } from '../../../linter/linter-rules'; import type { ConfiguredLintingRule, LintingResults, LintingRule } from '../../../linter/linter-format'; -import { isLintingResultsError, LintingPrettyPrintContext , LintingResultCertainty } from '../../../linter/linter-format'; +import { isLintingResultsError, LintingPrettyPrintContext, LintingResultCertainty } from '../../../linter/linter-format'; import { bold } from '../../../util/text/ansi'; import { printAsMs } from '../../../util/text/time'; @@ -27,6 +32,23 @@ export interface LinterQueryResult extends BaseQueryResult { readonly results: { [L in LintingRuleNames]?: LintingResults} } +function linterQueryLineParser(line: readonly string[], _config: unknown): [LinterQuery] { + if(line.length > 0 && line[0].startsWith('rules:')) { + const rulesPart = line[0].slice('rules:'.length).split(','); + const rules: (LintingRuleNames | ConfiguredLintingRule)[] = []; + for(const ruleEntry of rulesPart) { + const ruleName = ruleEntry.trim(); + if(!(ruleName in LintingRules)) { + console.error(`Unknown linting rule '${ruleName}'`); + continue; + } + rules.push(ruleName as LintingRuleNames); + } + return [{ type: 'linter', rules }]; + } + console.error('Invalid linter query syntax, expected "linter rules:rule1,rule2,..."'); + return [{ type: 'linter' }]; +} export const LinterQueryDefinition = { executor: executeLinterQuery, @@ -38,7 +60,8 @@ export const LinterQueryDefinition = { } return true; }, - schema: Joi.object({ + fromLine: linterQueryLineParser, + schema: Joi.object({ type: Joi.string().valid('linter').required().description('The type of the query.'), rules: Joi.array().items( Joi.string().valid(...Object.keys(LintingRules)), From edfca5960c4037a6b6c3b158ba248c65920ab140 Mon Sep 17 00:00:00 2001 From: MaxAtoms <7847075+MaxAtoms@users.noreply.github.com> Date: Fri, 24 Oct 2025 11:59:27 +0200 Subject: [PATCH 04/20] feat(repl): parse input for each query separately --- src/cli/repl/commands/repl-main.ts | 2 +- src/cli/repl/commands/repl-query.ts | 11 ++++-- .../config-query/config-query-format.ts | 3 +- .../linter-query/linter-query-format.ts | 39 ++++++++++++------- src/queries/query.ts | 5 +-- 5 files changed, 38 insertions(+), 22 deletions(-) diff --git a/src/cli/repl/commands/repl-main.ts b/src/cli/repl/commands/repl-main.ts index 11ce49ab4e8..fb093962463 100644 --- a/src/cli/repl/commands/repl-main.ts +++ b/src/cli/repl/commands/repl-main.ts @@ -86,5 +86,5 @@ export interface ReplCodeCommand extends ReplBaseCommand { /** * Argument parser function which handles the input given after the repl command */ - argsParser: (remainingLine: string) => { input: string | undefined, remaining: string[]} + argsParser: (remainingLine: string) => { input?: string | undefined, remaining: string[]} } diff --git a/src/cli/repl/commands/repl-query.ts b/src/cli/repl/commands/repl-query.ts index aa816ad4972..662e0ae4abe 100644 --- a/src/cli/repl/commands/repl-query.ts +++ b/src/cli/repl/commands/repl-query.ts @@ -1,4 +1,4 @@ -import { fileProtocol } from '../../../r-bridge/retriever'; +import { fileProtocol, requestFromInput } from '../../../r-bridge/retriever'; import type { ReplCodeCommand, ReplOutput } from './repl-main'; import { splitAtEscapeSensitive } from '../../../util/text/args'; import { ansiFormatter, italic } from '../../../util/text/ansi'; @@ -38,10 +38,15 @@ async function processQueryArgs(output: ReplOutput, analyzer: FlowrAnalysisProvi const queryName = query.slice(1); const queryObj = SupportedQueries[queryName as keyof typeof SupportedQueries] as SupportedQuery; if(queryObj?.fromLine) { - const q = queryObj.fromLine(remainingArgs, analyzer.flowrConfig); + const q = queryObj.fromLine(remainingArgs, analyzer); parsedQuery = q ? (Array.isArray(q) ? q : [q]) : []; } else { parsedQuery = [{ type: query.slice(1) as SupportedQueryTypes } as Query]; + const input = remainingArgs.join(' ').trim(); + if(input) { + analyzer.reset(); + analyzer.context().addRequest(requestFromInput(input)); + } } const validationResult = QueriesSchema().validate(parsedQuery); if(validationResult.error) { @@ -79,8 +84,6 @@ async function processQueryArgs(output: ReplOutput, analyzer: FlowrAnalysisProvi function parseArgs(line: string) { const args = splitAtEscapeSensitive(line); return { - // TODO Discuss future solution so that parentheses around code are not needed - input: args[0].trim() === 'help' ? '' : args[args.length - 1], remaining: args }; } diff --git a/src/queries/catalog/config-query/config-query-format.ts b/src/queries/catalog/config-query/config-query-format.ts index c179ee6f4f4..0f608b65070 100644 --- a/src/queries/catalog/config-query/config-query-format.ts +++ b/src/queries/catalog/config-query/config-query-format.ts @@ -7,6 +7,7 @@ import type { FlowrConfigOptions } from '../../../config'; import { jsonReplacer } from '../../../util/json'; import type { DeepPartial } from 'ts-essentials'; import type { SupportedQuery } from '../../query'; +import type { FlowrAnalysisProvider } from '../../../project/flowr-analyzer'; export interface ConfigQuery extends BaseQueryFormat { readonly type: 'config'; @@ -46,7 +47,7 @@ function configReplCompleter(partialLine: readonly string[], config: FlowrConfig return []; } -function configQueryLineParser(line: readonly string[], _config: FlowrConfigOptions): [ConfigQuery] { +function configQueryLineParser(line: readonly string[], _analyzer: FlowrAnalysisProvider): [ConfigQuery] { if(line.length > 0 && line[0].startsWith('+')) { const [pathPart, ...valueParts] = line[0].slice(1).split('='); // build the update object diff --git a/src/queries/catalog/linter-query/linter-query-format.ts b/src/queries/catalog/linter-query/linter-query-format.ts index c864fb71b75..4c70368ea92 100644 --- a/src/queries/catalog/linter-query/linter-query-format.ts +++ b/src/queries/catalog/linter-query/linter-query-format.ts @@ -15,6 +15,8 @@ import { isLintingResultsError, LintingPrettyPrintContext, LintingResultCertaint import { bold } from '../../../util/text/ansi'; import { printAsMs } from '../../../util/text/time'; import { codeInline } from '../../../documentation/doc-util/doc-code'; +import type { FlowrAnalysisProvider } from '../../../project/flowr-analyzer'; +import { requestFromInput } from '../../../r-bridge/retriever'; export interface LinterQuery extends BaseQueryFormat { readonly type: 'linter'; @@ -32,22 +34,33 @@ export interface LinterQueryResult extends BaseQueryResult { readonly results: { [L in LintingRuleNames]?: LintingResults} } -function linterQueryLineParser(line: readonly string[], _config: unknown): [LinterQuery] { +function rulesFromInput(rulesPart: string[]): (LintingRuleNames | ConfiguredLintingRule)[] { + return rulesPart.map(rule => { + const ruleName = rule.trim(); + if(!(ruleName in LintingRules)) { + console.error(`Unknown linting rule '${ruleName}'`); + return; + } + return ruleName as LintingRuleNames; + }).filter(r => !(r === undefined)); +} + +function linterQueryLineParser(line: readonly string[], analyzer: FlowrAnalysisProvider): [LinterQuery] { + let rules: (LintingRuleNames | ConfiguredLintingRule)[] | undefined = undefined; + let input: string | undefined = undefined; if(line.length > 0 && line[0].startsWith('rules:')) { const rulesPart = line[0].slice('rules:'.length).split(','); - const rules: (LintingRuleNames | ConfiguredLintingRule)[] = []; - for(const ruleEntry of rulesPart) { - const ruleName = ruleEntry.trim(); - if(!(ruleName in LintingRules)) { - console.error(`Unknown linting rule '${ruleName}'`); - continue; - } - rules.push(ruleName as LintingRuleNames); - } - return [{ type: 'linter', rules }]; + rules = rulesFromInput(rulesPart); + input = line[1]; + } else if(line.length > 0) { + input = line[0]; + } + + if(input) { + analyzer.reset(); + analyzer.context().addRequest(requestFromInput(input)); } - console.error('Invalid linter query syntax, expected "linter rules:rule1,rule2,..."'); - return [{ type: 'linter' }]; + return [{ type: 'linter', rules: rules }]; } export const LinterQueryDefinition = { diff --git a/src/queries/query.ts b/src/queries/query.ts index bbc566c4269..eab1c523a35 100644 --- a/src/queries/query.ts +++ b/src/queries/query.ts @@ -46,8 +46,7 @@ import type { DfShapeQuery } from './catalog/df-shape-query/df-shape-query-forma import { DfShapeQueryDefinition } from './catalog/df-shape-query/df-shape-query-format'; import type { AsyncOrSync, Writable } from 'ts-essentials'; import type { FlowrConfigOptions } from '../config'; -import type { - InspectHigherOrderQuery } from './catalog/inspect-higher-order-query/inspect-higher-order-query-format'; +import type { InspectHigherOrderQuery } from './catalog/inspect-higher-order-query/inspect-higher-order-query-format'; import { InspectHigherOrderQueryDefinition } from './catalog/inspect-higher-order-query/inspect-higher-order-query-format'; @@ -94,7 +93,7 @@ export interface SupportedQuery string[] /** optional query construction from an, e.g., repl line */ - fromLine?: (splitLine: readonly string[], config: FlowrConfigOptions) => Query | Query[] | undefined + fromLine?: (splitLine: readonly string[], analyzer: FlowrAnalysisProvider) => Query | Query[] | undefined asciiSummarizer: (formatter: OutputFormatter, analyzer: FlowrAnalysisProvider, queryResults: BaseQueryResult, resultStrings: string[], query: readonly Query[]) => AsyncOrSync schema: Joi.ObjectSchema /** From 6c3355e3ad3e0831d57dfb2b34b87ff2167c1ff0 Mon Sep 17 00:00:00 2001 From: MaxAtoms <7847075+MaxAtoms@users.noreply.github.com> Date: Fri, 24 Oct 2025 12:23:48 +0200 Subject: [PATCH 05/20] feat(repl): centralize input processing for queries --- src/cli/repl/commands/repl-query.ts | 17 +++++++++++------ .../catalog/config-query/config-query-format.ts | 16 ++++++---------- .../catalog/linter-query/linter-query-format.ts | 14 ++++---------- src/queries/query.ts | 7 ++++++- 4 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/cli/repl/commands/repl-query.ts b/src/cli/repl/commands/repl-query.ts index 662e0ae4abe..cfe74a443c2 100644 --- a/src/cli/repl/commands/repl-query.ts +++ b/src/cli/repl/commands/repl-query.ts @@ -34,19 +34,18 @@ async function processQueryArgs(output: ReplOutput, analyzer: FlowrAnalysisProvi } let parsedQuery: Query[]; + let input: string | undefined; if(query.startsWith('@')) { const queryName = query.slice(1); const queryObj = SupportedQueries[queryName as keyof typeof SupportedQueries] as SupportedQuery; if(queryObj?.fromLine) { - const q = queryObj.fromLine(remainingArgs, analyzer); + const parseResult = queryObj.fromLine(remainingArgs, analyzer.flowrConfig); + const q = parseResult.query; parsedQuery = q ? (Array.isArray(q) ? q : [q]) : []; + input = parseResult.input; } else { parsedQuery = [{ type: query.slice(1) as SupportedQueryTypes } as Query]; - const input = remainingArgs.join(' ').trim(); - if(input) { - analyzer.reset(); - analyzer.context().addRequest(requestFromInput(input)); - } + input = remainingArgs.join(' ').trim(); } const validationResult = QueriesSchema().validate(parsedQuery); if(validationResult.error) { @@ -62,10 +61,16 @@ async function processQueryArgs(output: ReplOutput, analyzer: FlowrAnalysisProvi printHelp(output); return; } + input = remainingArgs.join(' ').trim(); } else { parsedQuery = [{ type: 'call-context', callName: query }]; } + if(input) { + analyzer.reset(); + analyzer.context().addRequest(requestFromInput(input)); + } + return { query: await executeQueries({ analyzer, diff --git a/src/queries/catalog/config-query/config-query-format.ts b/src/queries/catalog/config-query/config-query-format.ts index 0f608b65070..3d81be42a0a 100644 --- a/src/queries/catalog/config-query/config-query-format.ts +++ b/src/queries/catalog/config-query/config-query-format.ts @@ -6,8 +6,7 @@ import Joi from 'joi'; import type { FlowrConfigOptions } from '../../../config'; import { jsonReplacer } from '../../../util/json'; import type { DeepPartial } from 'ts-essentials'; -import type { SupportedQuery } from '../../query'; -import type { FlowrAnalysisProvider } from '../../../project/flowr-analyzer'; +import type { ParsedQueryLine, SupportedQuery } from '../../query'; export interface ConfigQuery extends BaseQueryFormat { readonly type: 'config'; @@ -47,7 +46,7 @@ function configReplCompleter(partialLine: readonly string[], config: FlowrConfig return []; } -function configQueryLineParser(line: readonly string[], _analyzer: FlowrAnalysisProvider): [ConfigQuery] { +function configQueryLineParser(line: readonly string[], _config: FlowrConfigOptions): ParsedQueryLine { if(line.length > 0 && line[0].startsWith('+')) { const [pathPart, ...valueParts] = line[0].slice(1).split('='); // build the update object @@ -74,15 +73,12 @@ function configQueryLineParser(line: readonly string[], _analyzer: FlowrAnalysis current = current[key] as Record; } } - return [{ - type: 'config', - update - }]; + return { query: [{ type: 'config', update }] + }; } } - return [{ - type: 'config' - }]; + return { query: [{ type: 'config' }] + }; } export const ConfigQueryDefinition = { diff --git a/src/queries/catalog/linter-query/linter-query-format.ts b/src/queries/catalog/linter-query/linter-query-format.ts index 4c70368ea92..a2b0bf21215 100644 --- a/src/queries/catalog/linter-query/linter-query-format.ts +++ b/src/queries/catalog/linter-query/linter-query-format.ts @@ -1,5 +1,5 @@ import type { BaseQueryFormat, BaseQueryResult } from '../../base-query-format'; -import type { QueryResults, SupportedQuery } from '../../query'; +import type { ParsedQueryLine, QueryResults, SupportedQuery } from '../../query'; import Joi from 'joi'; import { executeLinterQuery } from './linter-query-executor'; import type { @@ -15,8 +15,7 @@ import { isLintingResultsError, LintingPrettyPrintContext, LintingResultCertaint import { bold } from '../../../util/text/ansi'; import { printAsMs } from '../../../util/text/time'; import { codeInline } from '../../../documentation/doc-util/doc-code'; -import type { FlowrAnalysisProvider } from '../../../project/flowr-analyzer'; -import { requestFromInput } from '../../../r-bridge/retriever'; +import type { FlowrConfigOptions } from '../../../config'; export interface LinterQuery extends BaseQueryFormat { readonly type: 'linter'; @@ -45,7 +44,7 @@ function rulesFromInput(rulesPart: string[]): (LintingRuleNames | ConfiguredLint }).filter(r => !(r === undefined)); } -function linterQueryLineParser(line: readonly string[], analyzer: FlowrAnalysisProvider): [LinterQuery] { +function linterQueryLineParser(line: readonly string[], _config: FlowrConfigOptions): ParsedQueryLine { let rules: (LintingRuleNames | ConfiguredLintingRule)[] | undefined = undefined; let input: string | undefined = undefined; if(line.length > 0 && line[0].startsWith('rules:')) { @@ -55,12 +54,7 @@ function linterQueryLineParser(line: readonly string[], analyzer: FlowrAnalysisP } else if(line.length > 0) { input = line[0]; } - - if(input) { - analyzer.reset(); - analyzer.context().addRequest(requestFromInput(input)); - } - return [{ type: 'linter', rules: rules }]; + return { query: [{ type: 'linter', rules: rules }], input } ; } export const LinterQueryDefinition = { diff --git a/src/queries/query.ts b/src/queries/query.ts index eab1c523a35..48eebfc39cb 100644 --- a/src/queries/query.ts +++ b/src/queries/query.ts @@ -88,12 +88,17 @@ type SupportedQueriesType = { [QueryType in Query['type']]: SupportedQuery } +export interface ParsedQueryLine { + query: Query | Query[] | undefined; + input?: string; +} + export interface SupportedQuery { executor: QueryExecutor, Promise> /** optional completion in, e.g., the repl */ completer?: (splitLine: readonly string[], config: FlowrConfigOptions) => string[] /** optional query construction from an, e.g., repl line */ - fromLine?: (splitLine: readonly string[], analyzer: FlowrAnalysisProvider) => Query | Query[] | undefined + fromLine?: (splitLine: readonly string[], config: FlowrConfigOptions) => ParsedQueryLine asciiSummarizer: (formatter: OutputFormatter, analyzer: FlowrAnalysisProvider, queryResults: BaseQueryResult, resultStrings: string[], query: readonly Query[]) => AsyncOrSync schema: Joi.ObjectSchema /** From 3de1af3c3e3c6fdf0cdf7654732e28d678dc0bd2 Mon Sep 17 00:00:00 2001 From: MaxAtoms <7847075+MaxAtoms@users.noreply.github.com> Date: Sat, 25 Oct 2025 18:36:01 +0200 Subject: [PATCH 06/20] refactor(repl): review comments --- src/cli/repl/commands/repl-lineage.ts | 2 +- src/cli/repl/commands/repl-main.ts | 5 +++-- src/cli/repl/commands/repl-parse.ts | 2 +- src/cli/repl/commands/repl-query.ts | 2 +- src/cli/repl/core.ts | 4 ++-- src/queries/catalog/linter-query/linter-query-format.ts | 7 ++++--- src/queries/query.ts | 7 ++++++- 7 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/cli/repl/commands/repl-lineage.ts b/src/cli/repl/commands/repl-lineage.ts index e26eec41e6c..4fc2e683c3f 100644 --- a/src/cli/repl/commands/repl-lineage.ts +++ b/src/cli/repl/commands/repl-lineage.ts @@ -65,7 +65,7 @@ export const lineageCommand: ReplCodeCommand = { const [criterion, rest] = splitAt(args, args.indexOf(' ')); const code = rest.trim(); return { - input: code.startsWith('"') ? JSON.parse(code) as string : code, + rCode: code.startsWith('"') ? JSON.parse(code) as string : code, remaining: [criterion] }; }, diff --git a/src/cli/repl/commands/repl-main.ts b/src/cli/repl/commands/repl-main.ts index fb093962463..ee4d5e1d27e 100644 --- a/src/cli/repl/commands/repl-main.ts +++ b/src/cli/repl/commands/repl-main.ts @@ -84,7 +84,8 @@ export interface ReplCodeCommand extends ReplBaseCommand { */ fn: (info: ReplCodeCommandInformation) => Promise | void /** - * Argument parser function which handles the input given after the repl command + * Argument parser function which handles the input given after the repl command. + * If no R code is returned, the input R code of a previous REPL command will be re-used for processing the current REPL command. */ - argsParser: (remainingLine: string) => { input?: string | undefined, remaining: string[]} + argsParser: (remainingLine: string) => { rCode?: string | undefined, remaining: string[]} } diff --git a/src/cli/repl/commands/repl-parse.ts b/src/cli/repl/commands/repl-parse.ts index ed5b0c79712..cb0b9294494 100644 --- a/src/cli/repl/commands/repl-parse.ts +++ b/src/cli/repl/commands/repl-parse.ts @@ -161,7 +161,7 @@ export const parseCommand: ReplCodeCommand = { argsParser: (line: string) => { return { // Threat the whole input line as R code - input: removeRQuotes(line.trim()), + rCode: removeRQuotes(line.trim()), remaining: [] }; }, diff --git a/src/cli/repl/commands/repl-query.ts b/src/cli/repl/commands/repl-query.ts index cfe74a443c2..60d57ce54e9 100644 --- a/src/cli/repl/commands/repl-query.ts +++ b/src/cli/repl/commands/repl-query.ts @@ -42,7 +42,7 @@ async function processQueryArgs(output: ReplOutput, analyzer: FlowrAnalysisProvi const parseResult = queryObj.fromLine(remainingArgs, analyzer.flowrConfig); const q = parseResult.query; parsedQuery = q ? (Array.isArray(q) ? q : [q]) : []; - input = parseResult.input; + input = parseResult.rCode; } else { parsedQuery = [{ type: query.slice(1) as SupportedQueryTypes } as Query]; input = remainingArgs.join(' ').trim(); diff --git a/src/cli/repl/core.ts b/src/cli/repl/core.ts index c94ce66a339..a5215e8e915 100644 --- a/src/cli/repl/core.ts +++ b/src/cli/repl/core.ts @@ -114,9 +114,9 @@ async function replProcessStatement(output: ReplOutput, statement: string, analy const remainingLine = statement.slice(command.length + 2).trim(); if(processor.isCodeCommand) { const args = processor.argsParser(remainingLine); - if(args.input) { + if(args.rCode) { analyzer.reset(); - analyzer.context().addRequest(requestFromInput(args.input)); + analyzer.context().addRequest(requestFromInput(args.rCode)); } await processor.fn({ output, analyzer, remainingArgs: args.remaining }); } else { diff --git a/src/queries/catalog/linter-query/linter-query-format.ts b/src/queries/catalog/linter-query/linter-query-format.ts index a2b0bf21215..2948b5cbee3 100644 --- a/src/queries/catalog/linter-query/linter-query-format.ts +++ b/src/queries/catalog/linter-query/linter-query-format.ts @@ -16,6 +16,7 @@ import { bold } from '../../../util/text/ansi'; import { printAsMs } from '../../../util/text/time'; import { codeInline } from '../../../documentation/doc-util/doc-code'; import type { FlowrConfigOptions } from '../../../config'; +import { isNotUndefined } from '../../../util/assert'; export interface LinterQuery extends BaseQueryFormat { readonly type: 'linter'; @@ -33,7 +34,7 @@ export interface LinterQueryResult extends BaseQueryResult { readonly results: { [L in LintingRuleNames]?: LintingResults} } -function rulesFromInput(rulesPart: string[]): (LintingRuleNames | ConfiguredLintingRule)[] { +function rulesFromInput(rulesPart: readonly string[]): (LintingRuleNames | ConfiguredLintingRule)[] { return rulesPart.map(rule => { const ruleName = rule.trim(); if(!(ruleName in LintingRules)) { @@ -41,7 +42,7 @@ function rulesFromInput(rulesPart: string[]): (LintingRuleNames | ConfiguredLint return; } return ruleName as LintingRuleNames; - }).filter(r => !(r === undefined)); + }).filter(r => isNotUndefined(r)); } function linterQueryLineParser(line: readonly string[], _config: FlowrConfigOptions): ParsedQueryLine { @@ -54,7 +55,7 @@ function linterQueryLineParser(line: readonly string[], _config: FlowrConfigOpti } else if(line.length > 0) { input = line[0]; } - return { query: [{ type: 'linter', rules: rules }], input } ; + return { query: [{ type: 'linter', rules: rules }], rCode: input } ; } export const LinterQueryDefinition = { diff --git a/src/queries/query.ts b/src/queries/query.ts index 48eebfc39cb..1e370f0e547 100644 --- a/src/queries/query.ts +++ b/src/queries/query.ts @@ -88,9 +88,14 @@ type SupportedQueriesType = { [QueryType in Query['type']]: SupportedQuery } +/** + * The result of parsing a query line from, e.g., the repl. + */ export interface ParsedQueryLine { + /** The parsed query or queries from the line. */ query: Query | Query[] | undefined; - input?: string; + /** Optional R code associated with the query. */ + rCode?: string; } export interface SupportedQuery { From df03321ddabaa8d592ceec9138199741c86531d3 Mon Sep 17 00:00:00 2001 From: MaxAtoms <7847075+MaxAtoms@users.noreply.github.com> Date: Sat, 25 Oct 2025 18:53:45 +0200 Subject: [PATCH 07/20] feat-fix(analyzer): analyzer should control request changes --- src/cli/repl/commands/repl-query.ts | 2 +- src/cli/repl/core.ts | 2 +- src/project/flowr-analyzer-builder.ts | 15 +++++++++------ src/project/flowr-analyzer.ts | 21 +++++++++++++++++++-- 4 files changed, 30 insertions(+), 10 deletions(-) diff --git a/src/cli/repl/commands/repl-query.ts b/src/cli/repl/commands/repl-query.ts index 60d57ce54e9..aba9a183b1d 100644 --- a/src/cli/repl/commands/repl-query.ts +++ b/src/cli/repl/commands/repl-query.ts @@ -68,7 +68,7 @@ async function processQueryArgs(output: ReplOutput, analyzer: FlowrAnalysisProvi if(input) { analyzer.reset(); - analyzer.context().addRequest(requestFromInput(input)); + analyzer.addRequest(requestFromInput(input)); } return { diff --git a/src/cli/repl/core.ts b/src/cli/repl/core.ts index a5215e8e915..679f9e413fb 100644 --- a/src/cli/repl/core.ts +++ b/src/cli/repl/core.ts @@ -116,7 +116,7 @@ async function replProcessStatement(output: ReplOutput, statement: string, analy const args = processor.argsParser(remainingLine); if(args.rCode) { analyzer.reset(); - analyzer.context().addRequest(requestFromInput(args.rCode)); + analyzer.addRequest(requestFromInput(args.rCode)); } await processor.fn({ output, analyzer, remainingArgs: args.remaining }); } else { diff --git a/src/project/flowr-analyzer-builder.ts b/src/project/flowr-analyzer-builder.ts index bc9827f2cc4..0a54040adb7 100644 --- a/src/project/flowr-analyzer-builder.ts +++ b/src/project/flowr-analyzer-builder.ts @@ -2,7 +2,7 @@ import type { EngineConfig, FlowrConfigOptions } from '../config'; import { amendConfig, cloneConfig, defaultConfigOptions } from '../config'; import type { DeepWritable } from 'ts-essentials'; import type { RParseRequest } from '../r-bridge/retriever'; -import { fileProtocol , isParseRequest , requestFromInput } from '../r-bridge/retriever'; +import { fileProtocol, isParseRequest, requestFromInput } from '../r-bridge/retriever'; import { FlowrAnalyzer } from './flowr-analyzer'; import { retrieveEngineInstances } from '../engines'; import type { KnownParser } from '../r-bridge/parser'; @@ -196,10 +196,6 @@ export class FlowrAnalyzerBuilder { guard(this.request !== undefined, 'Currently we require at least one request to build an analyzer, please provide one using the constructor or the addRequest method'); const context = new FlowrAnalyzerContext(this.plugins); - context.addRequests(this.request); - // we do it here to save time later if the analyzer is to be duplicated - context.resolvePreAnalysis(); - const cache = FlowrAnalyzerCache.create({ parser: this.parser, config: this.flowrConfig, @@ -207,11 +203,18 @@ export class FlowrAnalyzerBuilder { ...(this.input ?? {}) }); - return new FlowrAnalyzer( + const analyzer = new FlowrAnalyzer( this.flowrConfig, this.parser, context, cache ); + + analyzer.addRequests(this.request); + + // we do it here to save time later if the analyzer is to be duplicated + context.resolvePreAnalysis(); + + return analyzer; } } \ No newline at end of file diff --git a/src/project/flowr-analyzer.ts b/src/project/flowr-analyzer.ts index cdf7fb563eb..a017461462e 100644 --- a/src/project/flowr-analyzer.ts +++ b/src/project/flowr-analyzer.ts @@ -17,6 +17,7 @@ import { CfgKind } from './cfg-kind'; import type { OutputCollectorConfiguration } from '../r-bridge/shell'; import { RShell } from '../r-bridge/shell'; import { guard } from '../util/assert'; +import type { RAnalysisRequest } from './context/flowr-analyzer-files-context'; /** * Exposes the central analyses and information provided by the {@link FlowrAnalyzer} to the linter, search, and query APIs. @@ -33,12 +34,20 @@ export interface FlowrAnalysisProvider { * @param addonConfig - Additional configuration for the output collector. */ sendCommandWithOutput(command: string, addonConfig?: Partial): Promise; + /** + * Add multiple analysis requests to the analyzer instance + */ + addRequests(requests: readonly RAnalysisRequest[]): void + /** + * Add a single analysis request to the analyzer instance + */ + addRequest(request: RAnalysisRequest): void /** * Returns project context information. * If you are a user that wants to inspect the context, prefer {@link inspectContext} instead. * Please be aware that modifications to the context may break analyzer assumptions. */ - context(): FlowrAnalyzerContext + context(): FlowrAnalyzerContext /** * Returns a read-only version of the project context information. * This is the preferred method for users that want to inspect the context. @@ -82,7 +91,7 @@ export interface FlowrAnalysisProvider { */ runFull(force?: boolean): Promise; /** - * Reset all caches used by the analyzer and effectively force all analyses to be redone. + * Reset the analyzer state, including the context and the cache. */ reset(): void; /** This is the config used for the analyzer */ @@ -146,6 +155,14 @@ export class FlowrAnalyzer implements this.cache.reset(); } + public addRequests(requests: readonly RAnalysisRequest[]): void { + this.ctx.addRequests(requests); + } + + public addRequest(request: RAnalysisRequest): void { + this.ctx.addRequest(request); + } + public async parse(force?: boolean): Promise['parse']>> { return this.cache.parse(force); } From ce02fab3d04b62bb4b675776e828eaf96fb38561 Mon Sep 17 00:00:00 2001 From: MaxAtoms <7847075+MaxAtoms@users.noreply.github.com> Date: Mon, 27 Oct 2025 07:52:48 +0100 Subject: [PATCH 08/20] feat(repl): use output to print error messages --- src/cli/repl/commands/repl-query.ts | 2 +- .../config-query/config-query-format.ts | 5 +-- .../linter-query/linter-query-format.ts | 31 ++++++++++++------- src/queries/query.ts | 3 +- 4 files changed, 25 insertions(+), 16 deletions(-) diff --git a/src/cli/repl/commands/repl-query.ts b/src/cli/repl/commands/repl-query.ts index aba9a183b1d..568f3c7d483 100644 --- a/src/cli/repl/commands/repl-query.ts +++ b/src/cli/repl/commands/repl-query.ts @@ -39,7 +39,7 @@ async function processQueryArgs(output: ReplOutput, analyzer: FlowrAnalysisProvi const queryName = query.slice(1); const queryObj = SupportedQueries[queryName as keyof typeof SupportedQueries] as SupportedQuery; if(queryObj?.fromLine) { - const parseResult = queryObj.fromLine(remainingArgs, analyzer.flowrConfig); + const parseResult = queryObj.fromLine(output, remainingArgs, analyzer.flowrConfig); const q = parseResult.query; parsedQuery = q ? (Array.isArray(q) ? q : [q]) : []; input = parseResult.rCode; diff --git a/src/queries/catalog/config-query/config-query-format.ts b/src/queries/catalog/config-query/config-query-format.ts index 3d81be42a0a..99009c36b31 100644 --- a/src/queries/catalog/config-query/config-query-format.ts +++ b/src/queries/catalog/config-query/config-query-format.ts @@ -7,6 +7,7 @@ import type { FlowrConfigOptions } from '../../../config'; import { jsonReplacer } from '../../../util/json'; import type { DeepPartial } from 'ts-essentials'; import type { ParsedQueryLine, SupportedQuery } from '../../query'; +import type { ReplOutput } from '../../../cli/repl/commands/repl-main'; export interface ConfigQuery extends BaseQueryFormat { readonly type: 'config'; @@ -46,13 +47,13 @@ function configReplCompleter(partialLine: readonly string[], config: FlowrConfig return []; } -function configQueryLineParser(line: readonly string[], _config: FlowrConfigOptions): ParsedQueryLine { +function configQueryLineParser(output: ReplOutput, line: readonly string[], _config: FlowrConfigOptions): ParsedQueryLine { if(line.length > 0 && line[0].startsWith('+')) { const [pathPart, ...valueParts] = line[0].slice(1).split('='); // build the update object const path = pathPart.split('.').filter(p => p.length > 0); if(path.length === 0 || valueParts.length !== 1) { - console.error('Invalid config update syntax, must be of the form +path.to.field=value'); + output.stdout(`Invalid config update syntax, must be of the form ${bold('+path.to.field=value', output.formatter)}`); } else { const update: DeepPartial = {}; const value = valueParts[0]; diff --git a/src/queries/catalog/linter-query/linter-query-format.ts b/src/queries/catalog/linter-query/linter-query-format.ts index 2948b5cbee3..9ae40f55eb1 100644 --- a/src/queries/catalog/linter-query/linter-query-format.ts +++ b/src/queries/catalog/linter-query/linter-query-format.ts @@ -16,7 +16,7 @@ import { bold } from '../../../util/text/ansi'; import { printAsMs } from '../../../util/text/time'; import { codeInline } from '../../../documentation/doc-util/doc-code'; import type { FlowrConfigOptions } from '../../../config'; -import { isNotUndefined } from '../../../util/assert'; +import type { ReplOutput } from '../../../cli/repl/commands/repl-main'; export interface LinterQuery extends BaseQueryFormat { readonly type: 'linter'; @@ -34,23 +34,30 @@ export interface LinterQueryResult extends BaseQueryResult { readonly results: { [L in LintingRuleNames]?: LintingResults} } -function rulesFromInput(rulesPart: readonly string[]): (LintingRuleNames | ConfiguredLintingRule)[] { - return rulesPart.map(rule => { - const ruleName = rule.trim(); - if(!(ruleName in LintingRules)) { - console.error(`Unknown linting rule '${ruleName}'`); - return; - } - return ruleName as LintingRuleNames; - }).filter(r => isNotUndefined(r)); +function rulesFromInput(output: ReplOutput, rulesPart: readonly string[]): {valid: (LintingRuleNames | ConfiguredLintingRule)[], invalid: string[]} { + return rulesPart + .map(r => r.trim()) + .reduce((acc, ruleName) => { + if(ruleName in LintingRules) { + acc.valid.push(ruleName as LintingRuleNames); + } else { + acc.invalid.push(ruleName); + } + return acc; + }, { valid: [] as (LintingRuleNames | ConfiguredLintingRule)[], invalid: [] as string[] }); } -function linterQueryLineParser(line: readonly string[], _config: FlowrConfigOptions): ParsedQueryLine { +function linterQueryLineParser(output: ReplOutput, line: readonly string[], _config: FlowrConfigOptions): ParsedQueryLine { let rules: (LintingRuleNames | ConfiguredLintingRule)[] | undefined = undefined; let input: string | undefined = undefined; if(line.length > 0 && line[0].startsWith('rules:')) { const rulesPart = line[0].slice('rules:'.length).split(','); - rules = rulesFromInput(rulesPart); + const parseResult = rulesFromInput(output, rulesPart); + if(parseResult.invalid.length > 0) { + output.stdout(`Invalid linting rule name(s): ${parseResult.invalid.map(r => bold(r, output.formatter)).join(', ')}` + +`\nValid rule names are: ${Object.keys(LintingRules).map(r => bold(r, output.formatter)).join(', ')}`); + } + rules = parseResult.valid; input = line[1]; } else if(line.length > 0) { input = line[0]; diff --git a/src/queries/query.ts b/src/queries/query.ts index 1e370f0e547..bf0c619295b 100644 --- a/src/queries/query.ts +++ b/src/queries/query.ts @@ -52,6 +52,7 @@ import { } from './catalog/inspect-higher-order-query/inspect-higher-order-query-format'; import type { FlowrAnalysisProvider } from '../project/flowr-analyzer'; import { log } from '../util/log'; +import type { ReplOutput } from '../cli/repl/commands/repl-main'; /** * These are all queries that can be executed from within flowR @@ -103,7 +104,7 @@ export interface SupportedQuery string[] /** optional query construction from an, e.g., repl line */ - fromLine?: (splitLine: readonly string[], config: FlowrConfigOptions) => ParsedQueryLine + fromLine?: (output: ReplOutput, splitLine: readonly string[], config: FlowrConfigOptions) => ParsedQueryLine asciiSummarizer: (formatter: OutputFormatter, analyzer: FlowrAnalysisProvider, queryResults: BaseQueryResult, resultStrings: string[], query: readonly Query[]) => AsyncOrSync schema: Joi.ObjectSchema /** From d3162adf77b7eb9427d89c2f060ec260d8618aa0 Mon Sep 17 00:00:00 2001 From: MaxAtoms <7847075+MaxAtoms@users.noreply.github.com> Date: Mon, 27 Oct 2025 07:54:50 +0100 Subject: [PATCH 09/20] refactor(repl): introduce variable --- src/queries/catalog/linter-query/linter-query-format.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/queries/catalog/linter-query/linter-query-format.ts b/src/queries/catalog/linter-query/linter-query-format.ts index 9ae40f55eb1..354634538f2 100644 --- a/src/queries/catalog/linter-query/linter-query-format.ts +++ b/src/queries/catalog/linter-query/linter-query-format.ts @@ -48,10 +48,11 @@ function rulesFromInput(output: ReplOutput, rulesPart: readonly string[]): {vali } function linterQueryLineParser(output: ReplOutput, line: readonly string[], _config: FlowrConfigOptions): ParsedQueryLine { + const rulesPrefix = 'rules:'; let rules: (LintingRuleNames | ConfiguredLintingRule)[] | undefined = undefined; let input: string | undefined = undefined; - if(line.length > 0 && line[0].startsWith('rules:')) { - const rulesPart = line[0].slice('rules:'.length).split(','); + if(line.length > 0 && line[0].startsWith(rulesPrefix)) { + const rulesPart = line[0].slice(rulesPrefix.length).split(','); const parseResult = rulesFromInput(output, rulesPart); if(parseResult.invalid.length > 0) { output.stdout(`Invalid linting rule name(s): ${parseResult.invalid.map(r => bold(r, output.formatter)).join(', ')}` From 775da56a6099976dfd3a453acc7c237e522defc5 Mon Sep 17 00:00:00 2001 From: MaxAtoms <7847075+MaxAtoms@users.noreply.github.com> Date: Mon, 27 Oct 2025 08:16:27 +0100 Subject: [PATCH 10/20] refactor(analyzer): introduce ModifiableFlowrAnalysisProvider --- src/cli/repl/commands/repl-main.ts | 4 +- src/cli/repl/commands/repl-query.ts | 4 +- src/project/flowr-analyzer.ts | 42 +++++++++++-------- .../dependencies-query-executor.ts | 2 +- .../dependencies-query-format.ts | 2 +- 5 files changed, 30 insertions(+), 24 deletions(-) diff --git a/src/cli/repl/commands/repl-main.ts b/src/cli/repl/commands/repl-main.ts index ee4d5e1d27e..7cc033a26cd 100644 --- a/src/cli/repl/commands/repl-main.ts +++ b/src/cli/repl/commands/repl-main.ts @@ -1,6 +1,6 @@ import type { OutputFormatter } from '../../../util/text/ansi'; import { formatter } from '../../../util/text/ansi'; -import type { FlowrAnalysisProvider } from '../../../project/flowr-analyzer'; +import type { FlowrAnalysisProvider, ModifiableFlowrAnalysisProvider } from '../../../project/flowr-analyzer'; /** * Defines the main interface for output of the repl. @@ -44,7 +44,7 @@ export interface ReplCommandInformation { */ export interface ReplCodeCommandInformation { output: ReplOutput, - analyzer: FlowrAnalysisProvider + analyzer: ModifiableFlowrAnalysisProvider remainingArgs: string[] } diff --git a/src/cli/repl/commands/repl-query.ts b/src/cli/repl/commands/repl-query.ts index 568f3c7d483..00fbad6e25c 100644 --- a/src/cli/repl/commands/repl-query.ts +++ b/src/cli/repl/commands/repl-query.ts @@ -7,7 +7,7 @@ import type { Query, QueryResults, SupportedQuery, SupportedQueryTypes } from '. import { AnyQuerySchema, executeQueries, QueriesSchema, SupportedQueries } from '../../../queries/query'; import { jsonReplacer } from '../../../util/json'; import { asciiSummaryOfQueryResult } from '../../../queries/query-print'; -import type { FlowrAnalysisProvider } from '../../../project/flowr-analyzer'; +import type { FlowrAnalysisProvider, ModifiableFlowrAnalysisProvider } from '../../../project/flowr-analyzer'; function printHelp(output: ReplOutput) { @@ -21,7 +21,7 @@ function printHelp(output: ReplOutput) { output.stdout(`With this, ${italic(':query @config', output.formatter)} prints the result of the config query.`); } -async function processQueryArgs(output: ReplOutput, analyzer: FlowrAnalysisProvider, remainingArgs: string[]): Promise { +async function processQueryArgs(output: ReplOutput, analyzer: ModifiableFlowrAnalysisProvider, remainingArgs: string[]): Promise { const query = remainingArgs.shift(); if(!query) { diff --git a/src/project/flowr-analyzer.ts b/src/project/flowr-analyzer.ts index a017461462e..9a7b9586387 100644 --- a/src/project/flowr-analyzer.ts +++ b/src/project/flowr-analyzer.ts @@ -19,6 +19,30 @@ import { RShell } from '../r-bridge/shell'; import { guard } from '../util/assert'; import type { RAnalysisRequest } from './context/flowr-analyzer-files-context'; +/** + * Extends the {@link FlowrAnalysisProvider} with methods that allow modifying the analyzer state. + */ +export interface ModifiableFlowrAnalysisProvider extends FlowrAnalysisProvider { + /** + * Returns project context information. + * If you are a user that wants to inspect the context, prefer {@link inspectContext} instead. + * Please be aware that modifications to the context may break analyzer assumptions. + */ + context(): FlowrAnalyzerContext + /** + * Add multiple analysis requests to the analyzer instance + */ + addRequests(requests: readonly RAnalysisRequest[]): void + /** + * Add a single analysis request to the analyzer instance + */ + addRequest(request: RAnalysisRequest): void + /** + * Reset the analyzer state, including the context and the cache. + */ + reset(): void; +} + /** * Exposes the central analyses and information provided by the {@link FlowrAnalyzer} to the linter, search, and query APIs. * This allows us to exchange the underlying implementation of the analyzer without affecting the APIs. @@ -34,20 +58,6 @@ export interface FlowrAnalysisProvider { * @param addonConfig - Additional configuration for the output collector. */ sendCommandWithOutput(command: string, addonConfig?: Partial): Promise; - /** - * Add multiple analysis requests to the analyzer instance - */ - addRequests(requests: readonly RAnalysisRequest[]): void - /** - * Add a single analysis request to the analyzer instance - */ - addRequest(request: RAnalysisRequest): void - /** - * Returns project context information. - * If you are a user that wants to inspect the context, prefer {@link inspectContext} instead. - * Please be aware that modifications to the context may break analyzer assumptions. - */ - context(): FlowrAnalyzerContext /** * Returns a read-only version of the project context information. * This is the preferred method for users that want to inspect the context. @@ -90,10 +100,6 @@ export interface FlowrAnalysisProvider { * This executes all steps of the core analysis (parse, normalize, dataflow). */ runFull(force?: boolean): Promise; - /** - * Reset the analyzer state, including the context and the cache. - */ - reset(): void; /** This is the config used for the analyzer */ flowrConfig: FlowrConfigOptions; } diff --git a/src/queries/catalog/dependencies-query/dependencies-query-executor.ts b/src/queries/catalog/dependencies-query/dependencies-query-executor.ts index c88e52bbb1b..b93a5da25a4 100644 --- a/src/queries/catalog/dependencies-query/dependencies-query-executor.ts +++ b/src/queries/catalog/dependencies-query/dependencies-query-executor.ts @@ -134,7 +134,7 @@ function getResults(queries: readonly DependenciesQuery[], { dataflow, config, n const results: DependencyInfo[] = []; for(const [arg, values] of foundValues.entries()) { for(const value of values) { - const dep = value ? data?.analyzer.context().deps.getDependency(value) ?? undefined : undefined; + const dep = value ? data?.analyzer.inspectContext().deps.getDependency(value) ?? undefined : undefined; const result = compactRecord({ nodeId: id, functionName: vertex.name, diff --git a/src/queries/catalog/dependencies-query/dependencies-query-format.ts b/src/queries/catalog/dependencies-query/dependencies-query-format.ts index fd217559818..48fa674ebf6 100644 --- a/src/queries/catalog/dependencies-query/dependencies-query-format.ts +++ b/src/queries/catalog/dependencies-query/dependencies-query-format.ts @@ -36,7 +36,7 @@ export const DefaultDependencyCategories = { if(!ignoreDefault) { visitAst((await data.analyzer.normalize()).ast, n => { if(n.type === RType.Symbol && n.namespace) { - const dep = data.analyzer.context().deps.getDependency(n.namespace); + const dep = data.analyzer.inspectContext().deps.getDependency(n.namespace); /* we should improve the identification of ':::' */ result.push({ nodeId: n.info.id, From bbe0d9ff2df3055a9067e95ade152bfa6a453bdf Mon Sep 17 00:00:00 2001 From: MaxAtoms <7847075+MaxAtoms@users.noreply.github.com> Date: Mon, 27 Oct 2025 09:07:45 +0100 Subject: [PATCH 11/20] feat-fix(repl): set correct field --- src/cli/repl/commands/repl-main.ts | 10 +++++++++- src/cli/repl/commands/repl-query.ts | 1 + src/cli/repl/core.ts | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/cli/repl/commands/repl-main.ts b/src/cli/repl/commands/repl-main.ts index 7cc033a26cd..a8ce54c6590 100644 --- a/src/cli/repl/commands/repl-main.ts +++ b/src/cli/repl/commands/repl-main.ts @@ -72,6 +72,14 @@ export interface ReplCommand extends ReplBaseCommand { fn: (info: ReplCommandInformation) => Promise | void } +/** + * Result of parsing a REPL code command line. + * `rCode` may be undefined, in which case the R code of a previous REPL command will be re-used. + */ +interface ParsedReplLine { + rCode: string | undefined; + remaining: string[]; +} /** * Repl command that uses the {@link FlowrAnalyzer} @@ -87,5 +95,5 @@ export interface ReplCodeCommand extends ReplBaseCommand { * Argument parser function which handles the input given after the repl command. * If no R code is returned, the input R code of a previous REPL command will be re-used for processing the current REPL command. */ - argsParser: (remainingLine: string) => { rCode?: string | undefined, remaining: string[]} + argsParser: (remainingLine: string) => ParsedReplLine } diff --git a/src/cli/repl/commands/repl-query.ts b/src/cli/repl/commands/repl-query.ts index 00fbad6e25c..72c1ffd421a 100644 --- a/src/cli/repl/commands/repl-query.ts +++ b/src/cli/repl/commands/repl-query.ts @@ -89,6 +89,7 @@ async function processQueryArgs(output: ReplOutput, analyzer: ModifiableFlowrAna function parseArgs(line: string) { const args = splitAtEscapeSensitive(line); return { + rCode: undefined, remaining: args }; } diff --git a/src/cli/repl/core.ts b/src/cli/repl/core.ts index 679f9e413fb..bddeb56c1c2 100644 --- a/src/cli/repl/core.ts +++ b/src/cli/repl/core.ts @@ -99,7 +99,7 @@ export function makeDefaultReplReadline(config: FlowrConfigOptions): readline.Re export function handleString(code: string) { return { - input: code.length == 0 ? undefined : code.startsWith('"') ? JSON.parse(code) as string : code, + rCode: code.length == 0 ? undefined : code.startsWith('"') ? JSON.parse(code) as string : code, remaining: [] }; } From ad3b6df531fb1f3b2ada23452ee690a2638c43ba Mon Sep 17 00:00:00 2001 From: MaxAtoms <7847075+MaxAtoms@users.noreply.github.com> Date: Mon, 27 Oct 2025 09:25:58 +0100 Subject: [PATCH 12/20] refactor(analyzer): rename interfaces --- src/cli/repl/commands/repl-cfg.ts | 4 +-- src/cli/repl/commands/repl-execute.ts | 4 +-- src/cli/repl/commands/repl-main.ts | 6 ++--- src/cli/repl/commands/repl-query.ts | 4 +-- src/dataflow/graph/dataflowgraph-builder.ts | 22 ++++++++-------- src/linter/linter-executor.ts | 4 +-- src/project/flowr-analyzer.ts | 8 +++--- src/queries/base-query-format.ts | 4 +-- .../call-context-query-format.ts | 4 +-- src/queries/query-print.ts | 4 +-- src/queries/query.ts | 4 +-- src/search/flowr-search-executor.ts | 4 +-- src/search/flowr-search.ts | 4 +-- .../search-executor/search-enrichers.ts | 4 +-- .../search-executor/search-generators.ts | 14 +++++----- src/search/search-executor/search-mappers.ts | 8 +++--- .../search-executor/search-transformer.ts | 26 +++++++++---------- src/util/version.ts | 6 ++--- test/functionality/_helper/shell.ts | 4 +-- 19 files changed, 69 insertions(+), 69 deletions(-) diff --git a/src/cli/repl/commands/repl-cfg.ts b/src/cli/repl/commands/repl-cfg.ts index 0262bb0b130..d09d27e25e0 100644 --- a/src/cli/repl/commands/repl-cfg.ts +++ b/src/cli/repl/commands/repl-cfg.ts @@ -6,14 +6,14 @@ import type { ControlFlowInformation } from '../../../control-flow/control-flow- import type { NormalizedAst } from '../../../r-bridge/lang-4.x/ast/model/processing/decorate'; import type { CfgSimplificationPassName } from '../../../control-flow/cfg-simplification'; import { DefaultCfgSimplificationOrder } from '../../../control-flow/cfg-simplification'; -import type { FlowrAnalysisProvider } from '../../../project/flowr-analyzer'; +import type { ReadonlyFlowrAnalysisProvider } from '../../../project/flowr-analyzer'; import { handleString } from '../core'; function formatInfo(out: ReplOutput, type: string): string { return out.formatter.format(`Copied ${type} to clipboard.`, { color: Colors.White, effect: ColorEffect.Foreground, style: FontStyles.Italic }); } -async function produceAndPrintCfg(analyzer: FlowrAnalysisProvider, output: ReplOutput, simplifications: readonly CfgSimplificationPassName[], cfgConverter: (cfg: ControlFlowInformation, ast: NormalizedAst) => string) { +async function produceAndPrintCfg(analyzer: ReadonlyFlowrAnalysisProvider, output: ReplOutput, simplifications: readonly CfgSimplificationPassName[], cfgConverter: (cfg: ControlFlowInformation, ast: NormalizedAst) => string) { const cfg = await analyzer.controlflow([...DefaultCfgSimplificationOrder, ...simplifications]); const normalizedAst = await analyzer.normalize(); const mermaid = cfgConverter(cfg, normalizedAst); diff --git a/src/cli/repl/commands/repl-execute.ts b/src/cli/repl/commands/repl-execute.ts index 1a381ba3026..49d8a9d24f9 100644 --- a/src/cli/repl/commands/repl-execute.ts +++ b/src/cli/repl/commands/repl-execute.ts @@ -1,6 +1,6 @@ import type { ReplCommand, ReplCommandInformation, ReplOutput } from './repl-main'; import { ColorEffect, Colors, FontStyles, italic } from '../../../util/text/ansi'; -import type { FlowrAnalysisProvider } from '../../../project/flowr-analyzer'; +import type { ReadonlyFlowrAnalysisProvider } from '../../../project/flowr-analyzer'; export async function tryExecuteRShellCommand({ output, analyzer, allowRSessionAccess, remainingLine }: ReplCommandInformation) { const parserInfo = await analyzer.parserInformation(); @@ -14,7 +14,7 @@ If you want to do so, please restart flowR with the ${output.formatter.format('- } } -async function executeRShellCommand(output: ReplOutput, analyzer: FlowrAnalysisProvider, statement: string) { +async function executeRShellCommand(output: ReplOutput, analyzer: ReadonlyFlowrAnalysisProvider, statement: string) { try { const result = await analyzer.sendCommandWithOutput(statement, { from: 'both', diff --git a/src/cli/repl/commands/repl-main.ts b/src/cli/repl/commands/repl-main.ts index a8ce54c6590..be7a53702bb 100644 --- a/src/cli/repl/commands/repl-main.ts +++ b/src/cli/repl/commands/repl-main.ts @@ -1,6 +1,6 @@ import type { OutputFormatter } from '../../../util/text/ansi'; import { formatter } from '../../../util/text/ansi'; -import type { FlowrAnalysisProvider, ModifiableFlowrAnalysisProvider } from '../../../project/flowr-analyzer'; +import type { FlowrAnalysisProvider, ReadonlyFlowrAnalysisProvider } from '../../../project/flowr-analyzer'; /** * Defines the main interface for output of the repl. @@ -33,7 +33,7 @@ export const standardReplOutput: ReplOutput = { export interface ReplCommandInformation { output: ReplOutput, allowRSessionAccess: boolean, - analyzer: FlowrAnalysisProvider, + analyzer: ReadonlyFlowrAnalysisProvider, remainingLine: string, } @@ -44,7 +44,7 @@ export interface ReplCommandInformation { */ export interface ReplCodeCommandInformation { output: ReplOutput, - analyzer: ModifiableFlowrAnalysisProvider + analyzer: FlowrAnalysisProvider remainingArgs: string[] } diff --git a/src/cli/repl/commands/repl-query.ts b/src/cli/repl/commands/repl-query.ts index 72c1ffd421a..a496d4ffa13 100644 --- a/src/cli/repl/commands/repl-query.ts +++ b/src/cli/repl/commands/repl-query.ts @@ -7,7 +7,7 @@ import type { Query, QueryResults, SupportedQuery, SupportedQueryTypes } from '. import { AnyQuerySchema, executeQueries, QueriesSchema, SupportedQueries } from '../../../queries/query'; import { jsonReplacer } from '../../../util/json'; import { asciiSummaryOfQueryResult } from '../../../queries/query-print'; -import type { FlowrAnalysisProvider, ModifiableFlowrAnalysisProvider } from '../../../project/flowr-analyzer'; +import type { FlowrAnalysisProvider, ReadonlyFlowrAnalysisProvider } from '../../../project/flowr-analyzer'; function printHelp(output: ReplOutput) { @@ -21,7 +21,7 @@ function printHelp(output: ReplOutput) { output.stdout(`With this, ${italic(':query @config', output.formatter)} prints the result of the config query.`); } -async function processQueryArgs(output: ReplOutput, analyzer: ModifiableFlowrAnalysisProvider, remainingArgs: string[]): Promise { +async function processQueryArgs(output: ReplOutput, analyzer: FlowrAnalysisProvider, remainingArgs: string[]): Promise { const query = remainingArgs.shift(); if(!query) { diff --git a/src/dataflow/graph/dataflowgraph-builder.ts b/src/dataflow/graph/dataflowgraph-builder.ts index 4e87d17e8c4..52b49203b84 100644 --- a/src/dataflow/graph/dataflowgraph-builder.ts +++ b/src/dataflow/graph/dataflowgraph-builder.ts @@ -17,7 +17,7 @@ import { DefaultBuiltinConfig, getDefaultProcessor } from '../environments/defau import type { FlowrSearchLike } from '../../search/flowr-search-builder'; import { runSearch } from '../../search/flowr-search-executor'; import { guard } from '../../util/assert'; -import type { FlowrAnalysisProvider } from '../../project/flowr-analyzer'; +import type { ReadonlyFlowrAnalysisProvider } from '../../project/flowr-analyzer'; export function emptyGraph(idMap?: AstIdMap) { return new DataflowGraphBuilder(idMap); @@ -204,7 +204,7 @@ export class DataflowGraphBuilder extends DataflowGraph { return this.addEdge(normalizeIdToNumberIfPossible(from), normalizeIdToNumberIfPossible(to as NodeId), type); } - private async queryHelper(from: FromQueryParam, to: ToQueryParam, data: FlowrAnalysisProvider, type: EdgeType) { + private async queryHelper(from: FromQueryParam, to: ToQueryParam, data: ReadonlyFlowrAnalysisProvider, type: EdgeType) { let fromId: NodeId; if('nodeId' in from) { fromId = from.nodeId; @@ -243,7 +243,7 @@ export class DataflowGraphBuilder extends DataflowGraph { * @param to - Either a node id or a query to find the node id. * @param input - The input to search in i.e. the dataflow graph. */ - public readsQuery(from: FromQueryParam, to: ToQueryParam, input: FlowrAnalysisProvider) { + public readsQuery(from: FromQueryParam, to: ToQueryParam, input: ReadonlyFlowrAnalysisProvider) { return this.queryHelper(from, to, input, EdgeType.Reads); } @@ -262,7 +262,7 @@ export class DataflowGraphBuilder extends DataflowGraph { * * @see {@link DataflowGraphBuilder#readsQuery|readsQuery} for parameters. */ - public definedByQuery(from: FromQueryParam, to: ToQueryParam, data: FlowrAnalysisProvider) { + public definedByQuery(from: FromQueryParam, to: ToQueryParam, data: ReadonlyFlowrAnalysisProvider) { return this.queryHelper(from, to, data, EdgeType.DefinedBy); } @@ -280,7 +280,7 @@ export class DataflowGraphBuilder extends DataflowGraph { * * @see {@link DataflowGraphBuilder#readsQuery|readsQuery} for parameters. */ - public callsQuery(from: FromQueryParam, to: ToQueryParam, data: FlowrAnalysisProvider) { + public callsQuery(from: FromQueryParam, to: ToQueryParam, data: ReadonlyFlowrAnalysisProvider) { return this.queryHelper(from, to, data, EdgeType.Calls); } @@ -298,7 +298,7 @@ export class DataflowGraphBuilder extends DataflowGraph { * * @see {@link DataflowGraphBuilder#readsQuery|readsQuery} for parameters. */ - public returnsQuery(from: FromQueryParam, to: ToQueryParam, data: FlowrAnalysisProvider) { + public returnsQuery(from: FromQueryParam, to: ToQueryParam, data: ReadonlyFlowrAnalysisProvider) { return this.queryHelper(from, to, data, EdgeType.Returns); } @@ -316,7 +316,7 @@ export class DataflowGraphBuilder extends DataflowGraph { * * @see {@link DataflowGraphBuilder#readsQuery|readsQuery} for parameters. */ - public definesOnCallQuery(from: FromQueryParam, to: ToQueryParam, data: FlowrAnalysisProvider) { + public definesOnCallQuery(from: FromQueryParam, to: ToQueryParam, data: ReadonlyFlowrAnalysisProvider) { return this.queryHelper(from, to, data, EdgeType.DefinesOnCall); } @@ -334,7 +334,7 @@ export class DataflowGraphBuilder extends DataflowGraph { * * @see {@link DataflowGraphBuilder#readsQuery|readsQuery} for parameters. */ - public definedByOnCallQuery(from: FromQueryParam, to: ToQueryParam, data: FlowrAnalysisProvider) { + public definedByOnCallQuery(from: FromQueryParam, to: ToQueryParam, data: ReadonlyFlowrAnalysisProvider) { return this.queryHelper(from, to, data, EdgeType.DefinedByOnCall); } @@ -352,7 +352,7 @@ export class DataflowGraphBuilder extends DataflowGraph { * * @see {@link DataflowGraphBuilder#readsQuery|readsQuery} for parameters. */ - public argumentQuery(from: FromQueryParam, to: ToQueryParam, data: FlowrAnalysisProvider) { + public argumentQuery(from: FromQueryParam, to: ToQueryParam, data: ReadonlyFlowrAnalysisProvider) { return this.queryHelper(from, to, data, EdgeType.Argument); } @@ -370,7 +370,7 @@ export class DataflowGraphBuilder extends DataflowGraph { * * @see {@link DataflowGraphBuilder#readsQuery|readsQuery} for parameters. */ - public nseQuery(from: FromQueryParam, to: ToQueryParam, data: FlowrAnalysisProvider) { + public nseQuery(from: FromQueryParam, to: ToQueryParam, data: ReadonlyFlowrAnalysisProvider) { return this.queryHelper(from, to, data, EdgeType.NonStandardEvaluation); } @@ -388,7 +388,7 @@ export class DataflowGraphBuilder extends DataflowGraph { * * @see {@link DataflowGraphBuilder#readsQuery|readsQuery} for parameters. */ - public sideEffectOnCallQuery(from: FromQueryParam, to: ToQueryParam, data: FlowrAnalysisProvider) { + public sideEffectOnCallQuery(from: FromQueryParam, to: ToQueryParam, data: ReadonlyFlowrAnalysisProvider) { return this.queryHelper(from, to, data, EdgeType.SideEffectOnCall); } diff --git a/src/linter/linter-executor.ts b/src/linter/linter-executor.ts index 3b90133853f..a7647b1b158 100644 --- a/src/linter/linter-executor.ts +++ b/src/linter/linter-executor.ts @@ -4,9 +4,9 @@ import type { LintingResults, LintingRule } from './linter-format'; import { runSearch } from '../search/flowr-search-executor'; import type { DeepPartial } from 'ts-essentials'; import { deepMergeObject } from '../util/objects'; -import type { FlowrAnalysisProvider } from '../project/flowr-analyzer'; +import type { ReadonlyFlowrAnalysisProvider } from '../project/flowr-analyzer'; -export async function executeLintingRule(ruleName: Name, input: FlowrAnalysisProvider, lintingRuleConfig?: DeepPartial>): Promise> { +export async function executeLintingRule(ruleName: Name, input: ReadonlyFlowrAnalysisProvider, lintingRuleConfig?: DeepPartial>): Promise> { try { const rule = LintingRules[ruleName] as unknown as LintingRule, LintingRuleMetadata, LintingRuleConfig>; const fullConfig = deepMergeObject>(rule.info.defaultConfig, lintingRuleConfig); diff --git a/src/project/flowr-analyzer.ts b/src/project/flowr-analyzer.ts index 9a7b9586387..e55a0b880ca 100644 --- a/src/project/flowr-analyzer.ts +++ b/src/project/flowr-analyzer.ts @@ -20,9 +20,9 @@ import { guard } from '../util/assert'; import type { RAnalysisRequest } from './context/flowr-analyzer-files-context'; /** - * Extends the {@link FlowrAnalysisProvider} with methods that allow modifying the analyzer state. + * Extends the {@link ReadonlyFlowrAnalysisProvider} with methods that allow modifying the analyzer state. */ -export interface ModifiableFlowrAnalysisProvider extends FlowrAnalysisProvider { +export interface FlowrAnalysisProvider extends ReadonlyFlowrAnalysisProvider { /** * Returns project context information. * If you are a user that wants to inspect the context, prefer {@link inspectContext} instead. @@ -47,7 +47,7 @@ export interface ModifiableFlowrAnalysisProvider extends FlowrAnalysisProvider { * Exposes the central analyses and information provided by the {@link FlowrAnalyzer} to the linter, search, and query APIs. * This allows us to exchange the underlying implementation of the analyzer without affecting the APIs. */ -export interface FlowrAnalysisProvider { +export interface ReadonlyFlowrAnalysisProvider { /** * Get the name of the parser used by the analyzer. */ @@ -113,7 +113,7 @@ export interface FlowrAnalysisProvider { * * To inspect the context of the analyzer, use {@link FlowrAnalyzer#inspectContext} (if you are a plugin and need to modify it, use {@link FlowrAnalyzer#context} instead). */ -export class FlowrAnalyzer implements FlowrAnalysisProvider { +export class FlowrAnalyzer implements ReadonlyFlowrAnalysisProvider { public readonly flowrConfig: FlowrConfigOptions; /** The parser and engine backend */ private readonly parser: Parser; diff --git a/src/queries/base-query-format.ts b/src/queries/base-query-format.ts index 1b1e10a1713..968ce1e24a4 100644 --- a/src/queries/base-query-format.ts +++ b/src/queries/base-query-format.ts @@ -1,4 +1,4 @@ -import type { FlowrAnalysisProvider } from '../project/flowr-analyzer'; +import type { ReadonlyFlowrAnalysisProvider } from '../project/flowr-analyzer'; export interface BaseQueryFormat { /** used to select the query type :) */ @@ -14,5 +14,5 @@ export interface BaseQueryResult { } export interface BasicQueryData { - readonly analyzer: FlowrAnalysisProvider; + readonly analyzer: ReadonlyFlowrAnalysisProvider; } diff --git a/src/queries/catalog/call-context-query/call-context-query-format.ts b/src/queries/catalog/call-context-query/call-context-query-format.ts index 0fa287b93df..2699e1b8fe4 100644 --- a/src/queries/catalog/call-context-query/call-context-query-format.ts +++ b/src/queries/catalog/call-context-query/call-context-query-format.ts @@ -12,7 +12,7 @@ import type { DataflowGraph } from '../../../dataflow/graph/graph'; import type { DataflowGraphVertexInfo } from '../../../dataflow/graph/vertex'; import type { CascadeAction } from './cascade-action'; import type { NoInfo } from '../../../r-bridge/lang-4.x/ast/model/model'; -import type { FlowrAnalysisProvider } from '../../../project/flowr-analyzer'; +import type { ReadonlyFlowrAnalysisProvider } from '../../../project/flowr-analyzer'; export interface FileFilter { /** @@ -125,7 +125,7 @@ const CallContextQueryLinkTo = Joi.object({ export const CallContextQueryDefinition = { executor: executeCallContextQueries, - asciiSummarizer: async(formatter: OutputFormatter, analyzer: FlowrAnalysisProvider, queryResults: BaseQueryResult, result: string[]) => { + asciiSummarizer: async(formatter: OutputFormatter, analyzer: ReadonlyFlowrAnalysisProvider, queryResults: BaseQueryResult, result: string[]) => { const out = queryResults as CallContextQueryResult; result.push(`Query: ${bold('call-context', formatter)} (${printAsMs(out['.meta'].timing, 0)})`); result.push(asciiCallContext(formatter, out, (await analyzer.normalize()).idMap)); diff --git a/src/queries/query-print.ts b/src/queries/query-print.ts index 2bbbb03ce12..de995333e1e 100644 --- a/src/queries/query-print.ts +++ b/src/queries/query-print.ts @@ -9,7 +9,7 @@ import type { BaseQueryMeta, BaseQueryResult } from './base-query-format'; import { printAsMs } from '../util/text/time'; import { isBuiltIn } from '../dataflow/environments/built-in'; import type { AstIdMap, ParentInformation } from '../r-bridge/lang-4.x/ast/model/processing/decorate'; -import type { FlowrAnalysisProvider } from '../project/flowr-analyzer'; +import type { ReadonlyFlowrAnalysisProvider } from '../project/flowr-analyzer'; function nodeString(nodeId: NodeId | { id: NodeId, info?: object}, formatter: OutputFormatter, idMap: AstIdMap): string { const isObj = typeof nodeId === 'object' && nodeId !== null && 'id' in nodeId; @@ -78,7 +78,7 @@ export function summarizeIdsIfTooLong(formatter: OutputFormatter, ids: readonly export async function asciiSummaryOfQueryResult( formatter: OutputFormatter, totalInMs: number, results: QueryResults, - analyzer: FlowrAnalysisProvider, queries: Queries + analyzer: ReadonlyFlowrAnalysisProvider, queries: Queries ): Promise { const result: string[] = []; diff --git a/src/queries/query.ts b/src/queries/query.ts index bf0c619295b..4f732bb7a0b 100644 --- a/src/queries/query.ts +++ b/src/queries/query.ts @@ -50,7 +50,7 @@ import type { InspectHigherOrderQuery } from './catalog/inspect-higher-order-que import { InspectHigherOrderQueryDefinition } from './catalog/inspect-higher-order-query/inspect-higher-order-query-format'; -import type { FlowrAnalysisProvider } from '../project/flowr-analyzer'; +import type { ReadonlyFlowrAnalysisProvider } from '../project/flowr-analyzer'; import { log } from '../util/log'; import type { ReplOutput } from '../cli/repl/commands/repl-main'; @@ -105,7 +105,7 @@ export interface SupportedQuery string[] /** optional query construction from an, e.g., repl line */ fromLine?: (output: ReplOutput, splitLine: readonly string[], config: FlowrConfigOptions) => ParsedQueryLine - asciiSummarizer: (formatter: OutputFormatter, analyzer: FlowrAnalysisProvider, queryResults: BaseQueryResult, resultStrings: string[], query: readonly Query[]) => AsyncOrSync + asciiSummarizer: (formatter: OutputFormatter, analyzer: ReadonlyFlowrAnalysisProvider, queryResults: BaseQueryResult, resultStrings: string[], query: readonly Query[]) => AsyncOrSync schema: Joi.ObjectSchema /** * Flattens the involved query nodes to be added to a flowR search when the {@link fromQuery} function is used based on the given result after this query is executed. diff --git a/src/search/flowr-search-executor.ts b/src/search/flowr-search-executor.ts index 1691d853521..7fb4fba1e25 100644 --- a/src/search/flowr-search-executor.ts +++ b/src/search/flowr-search-executor.ts @@ -4,7 +4,7 @@ import type { FlowrSearchElements } from './flowr-search'; import { getGenerator } from './search-executor/search-generators'; import { getTransformer } from './search-executor/search-transformer'; import type { ParentInformation } from '../r-bridge/lang-4.x/ast/model/processing/decorate'; -import type { FlowrAnalysisProvider } from '../project/flowr-analyzer'; +import type { ReadonlyFlowrAnalysisProvider } from '../project/flowr-analyzer'; export type GetSearchElements = S extends FlowrSearch ? Elements : never; @@ -13,7 +13,7 @@ export type GetSearchElements = S extends FlowrSearch( search: S, - input: FlowrAnalysisProvider + input: ReadonlyFlowrAnalysisProvider ): Promise>> { const s = getFlowrSearch(search); diff --git a/src/search/flowr-search.ts b/src/search/flowr-search.ts index affc7df63d0..19d08830b7f 100644 --- a/src/search/flowr-search.ts +++ b/src/search/flowr-search.ts @@ -10,7 +10,7 @@ import type { EnrichmentSearchContent } from './search-executor/search-enrichers'; import { Enrichments } from './search-executor/search-enrichers'; -import type { FlowrAnalysisProvider } from '../project/flowr-analyzer'; +import type { ReadonlyFlowrAnalysisProvider } from '../project/flowr-analyzer'; /** * Yes, for now we do technically not need a wrapper around the RNode, but this allows us to attach caches etc. @@ -101,7 +101,7 @@ export class FlowrSearchElements(data: FlowrAnalysisProvider, enrichment: E, args?: EnrichmentSearchArguments): Promise { + public async enrich(data: ReadonlyFlowrAnalysisProvider, enrichment: E, args?: EnrichmentSearchArguments): Promise { const enrichmentData = Enrichments[enrichment] as unknown as EnrichmentData, EnrichmentElementArguments, EnrichmentSearchContent, EnrichmentSearchArguments>; if(enrichmentData.enrichSearch !== undefined) { this.enrichments = { diff --git a/src/search/search-executor/search-enrichers.ts b/src/search/search-executor/search-enrichers.ts index 2f87f8dd756..02d5a6289bb 100644 --- a/src/search/search-executor/search-enrichers.ts +++ b/src/search/search-executor/search-enrichers.ts @@ -22,7 +22,7 @@ import type { Query, QueryResult } from '../../queries/query'; import type { CfgSimplificationPassName } from '../../control-flow/cfg-simplification'; import { cfgFindAllReachable, DefaultCfgSimplificationOrder } from '../../control-flow/cfg-simplification'; import type { AsyncOrSync, AsyncOrSyncType } from 'ts-essentials'; -import type { FlowrAnalysisProvider } from '../../project/flowr-analyzer'; +import type { ReadonlyFlowrAnalysisProvider } from '../../project/flowr-analyzer'; import type { DataflowInformation } from '../../dataflow/info'; import { promoteCallName } from '../../queries/catalog/call-context-query/call-context-query-executor'; import { CfgKind } from '../../project/cfg-kind'; @@ -33,7 +33,7 @@ export interface EnrichmentData, search: FlowrSearchElements, data: {dataflow: DataflowInformation, normalize: NormalizedAst, cfg: ControlFlowInformation}, args: ElementArguments | undefined, previousValue: ElementContent | undefined) => AsyncOrSync - readonly enrichSearch?: (search: FlowrSearchElements, data: FlowrAnalysisProvider, args: SearchArguments | undefined, previousValue: SearchContent | undefined) => AsyncOrSync + readonly enrichSearch?: (search: FlowrSearchElements, data: ReadonlyFlowrAnalysisProvider, args: SearchArguments | undefined, previousValue: SearchContent | undefined) => AsyncOrSync /** * The mapping function used by the {@link Mapper.Enrichment} mapper. */ diff --git a/src/search/search-executor/search-generators.ts b/src/search/search-executor/search-generators.ts index 29fd89706ad..1e41dbe5b8b 100644 --- a/src/search/search-executor/search-generators.ts +++ b/src/search/search-executor/search-generators.ts @@ -10,7 +10,7 @@ import { executeQueries, SupportedQueries } from '../../queries/query'; import type { BaseQueryResult } from '../../queries/base-query-format'; import type { RNode } from '../../r-bridge/lang-4.x/ast/model/model'; import { enrichElement, Enrichment } from './search-enrichers'; -import type { FlowrAnalysisProvider } from '../../project/flowr-analyzer'; +import type { ReadonlyFlowrAnalysisProvider } from '../../project/flowr-analyzer'; /** * This is a union of all possible generator node types @@ -38,19 +38,19 @@ export const generators = { 'from-query': generateFromQuery } as const; -async function generateAll(data: FlowrAnalysisProvider): Promise> { +async function generateAll(data: ReadonlyFlowrAnalysisProvider): Promise> { return new FlowrSearchElements((await getAllNodes(data)) .map(node => ({ node }))); } -async function getAllNodes(data: FlowrAnalysisProvider): Promise { +async function getAllNodes(data: ReadonlyFlowrAnalysisProvider): Promise { const normalize = await data.normalize(); return [...new Map([...normalize.idMap.values()].map(n => [n.info.id, n])) .values()]; } -async function generateGet(input: FlowrAnalysisProvider, { filter: { line, column, id, name, nameIsRegex } }: { filter: FlowrSearchGetFilter }): Promise> { +async function generateGet(input: ReadonlyFlowrAnalysisProvider, { filter: { line, column, id, name, nameIsRegex } }: { filter: FlowrSearchGetFilter }): Promise> { const normalize = await input.normalize(); let potentials = (id ? [normalize.idMap.get(id)].filter(isNotUndefined) : @@ -83,11 +83,11 @@ async function generateGet(input: FlowrAnalysisProvider, { filter: { line, colum return new FlowrSearchElements(potentials.map(node => ({ node }))); } -function generateFrom(_input: FlowrAnalysisProvider, args: { from: FlowrSearchElement | FlowrSearchElement[] }): FlowrSearchElements { +function generateFrom(_input: ReadonlyFlowrAnalysisProvider, args: { from: FlowrSearchElement | FlowrSearchElement[] }): FlowrSearchElements { return new FlowrSearchElements(Array.isArray(args.from) ? args.from : [args.from]); } -async function generateFromQuery(input: FlowrAnalysisProvider, args: { +async function generateFromQuery(input: ReadonlyFlowrAnalysisProvider, args: { from: readonly Query[] }): Promise[]>> { const result = await executeQueries({ analyzer: input }, args.from); @@ -121,7 +121,7 @@ async function generateFromQuery(input: FlowrAnalysisProvider, args: { }))) as unknown as FlowrSearchElements[]>; } -async function generateCriterion(input: FlowrAnalysisProvider, args: { criterion: SlicingCriteria }): Promise> { +async function generateCriterion(input: ReadonlyFlowrAnalysisProvider, args: { criterion: SlicingCriteria }): Promise> { const idMap = (await input.normalize()).idMap; return new FlowrSearchElements( args.criterion.map(c => ({ node: idMap.get(slicingCriterionToId(c, idMap)) as RNodeWithParent })) diff --git a/src/search/search-executor/search-mappers.ts b/src/search/search-executor/search-mappers.ts index 1a5638312c7..75618140e6f 100644 --- a/src/search/search-executor/search-mappers.ts +++ b/src/search/search-executor/search-mappers.ts @@ -4,20 +4,20 @@ import type { Enrichment, EnrichmentData, EnrichmentElementContent } from './sea import { enrichmentContent, Enrichments } from './search-enrichers'; import type { MergeableRecord } from '../../util/objects'; -import type { FlowrAnalysisProvider } from '../../project/flowr-analyzer'; +import type { ReadonlyFlowrAnalysisProvider } from '../../project/flowr-analyzer'; export enum Mapper { Enrichment = 'enrichment' } export interface MapperData { - mapper: (e: FlowrSearchElement, data: FlowrAnalysisProvider, args: Arguments) => FlowrSearchElement[] + mapper: (e: FlowrSearchElement, data: ReadonlyFlowrAnalysisProvider, args: Arguments) => FlowrSearchElement[] } export type MapperArguments = typeof Mappers[M] extends MapperData ? Arguments : never; const Mappers = { [Mapper.Enrichment]: { - mapper: (e: FlowrSearchElement, _data: FlowrAnalysisProvider, enrichment: Enrichment) => { + mapper: (e: FlowrSearchElement, _data: ReadonlyFlowrAnalysisProvider, enrichment: Enrichment) => { const enrichmentData = Enrichments[enrichment] as unknown as EnrichmentData>; const content = enrichmentContent(e, enrichment); return content !== undefined ? enrichmentData.mapper?.(content) ?? [] : []; @@ -26,6 +26,6 @@ const Mappers = { } as const; export function map, MapperType extends Mapper>( - e: Element, data: FlowrAnalysisProvider, mapper: MapperType, args: MapperArguments): Element[] { + e: Element, data: ReadonlyFlowrAnalysisProvider, mapper: MapperType, args: MapperArguments): Element[] { return (Mappers[mapper] as MapperData>).mapper(e, data, args) as Element[]; } diff --git a/src/search/search-executor/search-transformer.ts b/src/search/search-executor/search-transformer.ts index c3386084397..ad0863d8df8 100644 --- a/src/search/search-executor/search-transformer.ts +++ b/src/search/search-executor/search-transformer.ts @@ -12,7 +12,7 @@ import { enrichElement } from './search-enrichers'; import type { Mapper, MapperArguments } from './search-mappers'; import { map } from './search-mappers'; import type { ElementOf } from 'ts-essentials'; -import type { FlowrAnalysisProvider } from '../../project/flowr-analyzer'; +import type { ReadonlyFlowrAnalysisProvider } from '../../project/flowr-analyzer'; /** @@ -101,23 +101,23 @@ type CascadeEmpty[], NewE Elements extends [] ? FlowrSearchElements : FlowrSearchElements; function getFirst[], FSE extends FlowrSearchElements>( - data: FlowrAnalysisProvider, elements: FSE + data: ReadonlyFlowrAnalysisProvider, elements: FSE ): CascadeEmpty { return elements.mutate(e => [getFirstByLocation(e)] as Elements) as unknown as CascadeEmpty; } function getLast[], FSE extends FlowrSearchElements>( - data: FlowrAnalysisProvider, elements: FSE): CascadeEmpty]> { + data: ReadonlyFlowrAnalysisProvider, elements: FSE): CascadeEmpty]> { return elements.mutate(e => [getLastByLocation(e)] as Elements) as unknown as CascadeEmpty]>; } function getIndex[], FSE extends FlowrSearchElements>( - data: FlowrAnalysisProvider, elements: FSE, { index }: { index: number }): CascadeEmpty { + data: ReadonlyFlowrAnalysisProvider, elements: FSE, { index }: { index: number }): CascadeEmpty { return elements.mutate(e => [sortFully(e)[index]] as Elements) as unknown as CascadeEmpty; } function getSelect[], FSE extends FlowrSearchElements>( - data: FlowrAnalysisProvider, elements: FSE, { select }: { select: number[] }): CascadeEmpty { + data: ReadonlyFlowrAnalysisProvider, elements: FSE, { select }: { select: number[] }): CascadeEmpty { return elements.mutate(e => { sortFully(e); return select.map(i => e[i]).filter(isNotUndefined) as Elements; @@ -125,7 +125,7 @@ function getSelect[], FSE } function getTail[], FSE extends FlowrSearchElements>( - data: FlowrAnalysisProvider, elements: FSE): CascadeEmpty> { + data: ReadonlyFlowrAnalysisProvider, elements: FSE): CascadeEmpty> { return elements.mutate(e => { const first = getFirstByLocation(e); return e.filter(el => el !== first) as Elements; @@ -133,17 +133,17 @@ function getTail[], FSE e } function getTake[], FSE extends FlowrSearchElements>( - data: FlowrAnalysisProvider, elements: FSE, { count }: { count: number }): CascadeEmpty> { + data: ReadonlyFlowrAnalysisProvider, elements: FSE, { count }: { count: number }): CascadeEmpty> { return elements.mutate(e => sortFully(e).slice(0, count) as Elements) as unknown as CascadeEmpty>; } function getSkip[], FSE extends FlowrSearchElements>( - data: FlowrAnalysisProvider, elements: FSE, { count }: { count: number }): CascadeEmpty> { + data: ReadonlyFlowrAnalysisProvider, elements: FSE, { count }: { count: number }): CascadeEmpty> { return elements.mutate(e => sortFully(e).slice(count) as Elements) as unknown as CascadeEmpty>; } async function getFilter[], FSE extends FlowrSearchElements>( - data: FlowrAnalysisProvider, elements: FSE, { filter }: { + data: ReadonlyFlowrAnalysisProvider, elements: FSE, { filter }: { filter: FlowrFilterExpression }): Promise> { const dataflow = await data.dataflow(); @@ -153,7 +153,7 @@ async function getFilter[ } async function getWith[], FSE extends FlowrSearchElements>( - input: FlowrAnalysisProvider, elements: FSE, { info, args }: { + input: ReadonlyFlowrAnalysisProvider, elements: FSE, { info, args }: { info: Enrichment, args?: EnrichmentElementArguments }): Promise[]>> { @@ -171,7 +171,7 @@ async function getWith[], } function getMap[], FSE extends FlowrSearchElements>( - data: FlowrAnalysisProvider, elements: FSE, { mapper, args }: { mapper: Mapper, args: MapperArguments }): FlowrSearchElements { + data: ReadonlyFlowrAnalysisProvider, elements: FSE, { mapper, args }: { mapper: Mapper, args: MapperArguments }): FlowrSearchElements { return elements.mutate( elements => elements.flatMap(e => map(e, data, mapper, args)) as Elements ) as unknown as FlowrSearchElements; @@ -179,7 +179,7 @@ function getMap[], FSE ex async function getMerge[], FSE extends FlowrSearchElements>( /* search has to be unknown because it is a recursive type */ - data: FlowrAnalysisProvider, elements: FSE, other: { + data: ReadonlyFlowrAnalysisProvider, elements: FSE, other: { search: unknown[], generator: FlowrSearchGeneratorNode }): Promise[]>> { @@ -188,7 +188,7 @@ async function getMerge[] } function getUnique[], FSE extends FlowrSearchElements>( - data: FlowrAnalysisProvider, elements: FSE): CascadeEmpty { + data: ReadonlyFlowrAnalysisProvider, elements: FSE): CascadeEmpty { return elements.mutate(e => e.reduce((acc, cur) => { if(!acc.some(el => el.node.id === cur.node.id)) { diff --git a/src/util/version.ts b/src/util/version.ts index 04f3cfbf3ac..f4b694aada0 100644 --- a/src/util/version.ts +++ b/src/util/version.ts @@ -2,7 +2,7 @@ import { SemVer } from 'semver'; import type { KnownParser } from '../r-bridge/parser'; import { guard } from './assert'; import type { ReplOutput } from '../cli/repl/commands/repl-main'; -import type { FlowrAnalysisProvider } from '../project/flowr-analyzer'; +import type { ReadonlyFlowrAnalysisProvider } from '../project/flowr-analyzer'; // this is automatically replaced with the current version by release-it const version = '2.6.1'; @@ -26,7 +26,7 @@ export interface VersionInformation { const versionRegex = /^\d+\.\d+\.\d+/m; -export async function retrieveVersionInformation(input: KnownParser | FlowrAnalysisProvider): Promise { +export async function retrieveVersionInformation(input: KnownParser | ReadonlyFlowrAnalysisProvider): Promise { const flowr = flowrVersion().toString(); let r: string; @@ -46,7 +46,7 @@ export async function retrieveVersionInformation(input: KnownParser | FlowrAnaly return { flowr: flowr as Version, r: r as Version, engine: name }; } -export async function printVersionInformation(output: ReplOutput, input: KnownParser | FlowrAnalysisProvider) { +export async function printVersionInformation(output: ReplOutput, input: KnownParser | ReadonlyFlowrAnalysisProvider) { const { flowr, r, engine } = await retrieveVersionInformation(input); output.stdout(`Engine: ${engine}`); output.stdout(` flowR: ${flowr}`); diff --git a/test/functionality/_helper/shell.ts b/test/functionality/_helper/shell.ts index bcf0e5aa833..1508b977b8e 100644 --- a/test/functionality/_helper/shell.ts +++ b/test/functionality/_helper/shell.ts @@ -53,7 +53,7 @@ import { assertCfgSatisfiesProperties } from '../../../src/control-flow/cfg-prop import type { FlowrConfigOptions } from '../../../src/config'; import { cloneConfig, defaultConfigOptions } from '../../../src/config'; import { FlowrAnalyzerBuilder } from '../../../src/project/flowr-analyzer-builder'; -import type { FlowrAnalysisProvider } from '../../../src/project/flowr-analyzer'; +import type { ReadonlyFlowrAnalysisProvider } from '../../../src/project/flowr-analyzer'; import type { KnownParser } from '../../../src/r-bridge/parser'; import { SliceDirection } from '../../../src/core/steps/all/static-slicing/00-slice'; @@ -360,7 +360,7 @@ export function assertDataflow( name: string | TestLabel, shell: RShell, input: string | RParseRequests, - expected: DataflowGraph | ((input: FlowrAnalysisProvider) => Promise), + expected: DataflowGraph | ((input: ReadonlyFlowrAnalysisProvider) => Promise), userConfig?: Partial, startIndexForDeterministicIds = 0, config = cloneConfig(defaultConfigOptions) From de1b6e13912681a898d70ec1f7af333ea9a4b9c4 Mon Sep 17 00:00:00 2001 From: MaxAtoms <7847075+MaxAtoms@users.noreply.github.com> Date: Mon, 27 Oct 2025 12:29:39 +0100 Subject: [PATCH 13/20] feat(repl): linter completions --- src/cli/repl/core.ts | 2 +- .../config-query/config-query-format.ts | 2 +- .../linter-query/linter-query-format.ts | 43 +++++++++++++++++-- src/queries/query.ts | 2 +- 4 files changed, 43 insertions(+), 6 deletions(-) diff --git a/src/cli/repl/core.ts b/src/cli/repl/core.ts index bddeb56c1c2..11ea59c1ae9 100644 --- a/src/cli/repl/core.ts +++ b/src/cli/repl/core.ts @@ -78,7 +78,7 @@ function replQueryCompleter(splitLine: readonly string[], startingNewArg: boolea const q = nonEmpty[0].slice(1); const queryElement = SupportedQueries[q as keyof typeof SupportedQueries] as SupportedQuery; if(queryElement?.completer) { - candidates = candidates.concat(queryElement.completer(nonEmpty.slice(1), config)); + candidates = candidates.concat(queryElement.completer(nonEmpty.slice(1), startingNewArg, config)); } } diff --git a/src/queries/catalog/config-query/config-query-format.ts b/src/queries/catalog/config-query/config-query-format.ts index 99009c36b31..3875a2aeaba 100644 --- a/src/queries/catalog/config-query/config-query-format.ts +++ b/src/queries/catalog/config-query/config-query-format.ts @@ -18,7 +18,7 @@ export interface ConfigQueryResult extends BaseQueryResult { readonly config: FlowrConfigOptions; } -function configReplCompleter(partialLine: readonly string[], config: FlowrConfigOptions): string[] { +function configReplCompleter(partialLine: readonly string[], _startingNewArg: boolean, config: FlowrConfigOptions): string[] { if(partialLine.length === 0) { // update specific fields return ['+']; diff --git a/src/queries/catalog/linter-query/linter-query-format.ts b/src/queries/catalog/linter-query/linter-query-format.ts index 354634538f2..05d5a27de6e 100644 --- a/src/queries/catalog/linter-query/linter-query-format.ts +++ b/src/queries/catalog/linter-query/linter-query-format.ts @@ -47,8 +47,9 @@ function rulesFromInput(output: ReplOutput, rulesPart: readonly string[]): {vali }, { valid: [] as (LintingRuleNames | ConfiguredLintingRule)[], invalid: [] as string[] }); } +const rulesPrefix = 'rules:'; + function linterQueryLineParser(output: ReplOutput, line: readonly string[], _config: FlowrConfigOptions): ParsedQueryLine { - const rulesPrefix = 'rules:'; let rules: (LintingRuleNames | ConfiguredLintingRule)[] | undefined = undefined; let input: string | undefined = undefined; if(line.length > 0 && line[0].startsWith(rulesPrefix)) { @@ -66,6 +67,41 @@ function linterQueryLineParser(output: ReplOutput, line: readonly string[], _con return { query: [{ type: 'linter', rules: rules }], rCode: input } ; } +function linterQueryCompleter(line: readonly string[], startingNewArg: boolean, _config: FlowrConfigOptions): string[] { + const completions: string[] = []; + const rulesPrefixNotPresent = line.length == 0 || (line.length == 1 && line[0].length < rulesPrefix.length); + const rulesNotFinished = line.length == 1 && line[0].startsWith(rulesPrefix) && !startingNewArg; + + if(rulesPrefixNotPresent) { + // Return complete rules prefix + completions.push(`${rulesPrefix}`); + } else if(rulesNotFinished) { + const allRules = Object.keys(LintingRules); + const existingRules = line[0].slice(rulesPrefix.length).split(',').map(r => r.trim()); + const lastRule = existingRules[existingRules.length - 1]; + const unusedRules = allRules.filter(r => !existingRules.includes(r)); + + if(!allRules.includes(lastRule)) { + // Complete the last rule or add all possible rules that are not used yet + for(const ruleName of unusedRules) { + if(ruleName.startsWith(lastRule)) { + const ending = unusedRules.length > 1 ? ',' : ' '; + completions.push(`${rulesPrefix}${existingRules.slice(0, -1).concat([ruleName]).join(',')}${ending}`); + } + } + } else if(unusedRules.length > 0) { + // Add a comma, if the current last rule is complete + completions.push(`${rulesPrefix}${existingRules.join(',')},`); + } else { + // All rules are used, complete with a space + completions.push(`${rulesPrefix}${existingRules.join(',')} `); + } + } + + // TODO Add file protocol if rules are finished + return completions; +} + export const LinterQueryDefinition = { executor: executeLinterQuery, asciiSummarizer: (formatter, _analyzer, queryResults, result) => { @@ -76,8 +112,9 @@ export const LinterQueryDefinition = { } return true; }, - fromLine: linterQueryLineParser, - schema: Joi.object({ + completer: linterQueryCompleter, + fromLine: linterQueryLineParser, + schema: Joi.object({ type: Joi.string().valid('linter').required().description('The type of the query.'), rules: Joi.array().items( Joi.string().valid(...Object.keys(LintingRules)), diff --git a/src/queries/query.ts b/src/queries/query.ts index 4f732bb7a0b..36eeb4d0d1f 100644 --- a/src/queries/query.ts +++ b/src/queries/query.ts @@ -102,7 +102,7 @@ export interface ParsedQueryLine { export interface SupportedQuery { executor: QueryExecutor, Promise> /** optional completion in, e.g., the repl */ - completer?: (splitLine: readonly string[], config: FlowrConfigOptions) => string[] + completer?: (splitLine: readonly string[], startingNewArg: boolean, config: FlowrConfigOptions) => string[] /** optional query construction from an, e.g., repl line */ fromLine?: (output: ReplOutput, splitLine: readonly string[], config: FlowrConfigOptions) => ParsedQueryLine asciiSummarizer: (formatter: OutputFormatter, analyzer: ReadonlyFlowrAnalysisProvider, queryResults: BaseQueryResult, resultStrings: string[], query: readonly Query[]) => AsyncOrSync From 59adc3845418ee13528c5637cad51a8ee3319877 Mon Sep 17 00:00:00 2001 From: MaxAtoms <7847075+MaxAtoms@users.noreply.github.com> Date: Mon, 27 Oct 2025 12:47:57 +0100 Subject: [PATCH 14/20] refactor(repl): omit empty array creation --- src/cli/repl/core.ts | 7 +++---- .../linter-query/linter-query-format.ts | 19 +++++++++---------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/cli/repl/core.ts b/src/cli/repl/core.ts index 11ea59c1ae9..a71fc21ebc1 100644 --- a/src/cli/repl/core.ts +++ b/src/cli/repl/core.ts @@ -71,18 +71,17 @@ export function replCompleter(line: string, config: FlowrConfigOptions): [string function replQueryCompleter(splitLine: readonly string[], startingNewArg: boolean, config: FlowrConfigOptions): string[] { const nonEmpty = splitLine.slice(1).map(s => s.trim()).filter(s => s.length > 0); const queryShorts = Object.keys(SupportedQueries).map(q => `@${q}`).concat(['help']); - let candidates: string[] = []; if(nonEmpty.length == 0 || (nonEmpty.length == 1 && queryShorts.some(q => q.startsWith(nonEmpty[0]) && nonEmpty[0] !== q && !startingNewArg))) { - candidates = candidates.concat(queryShorts.map(q => `${q} `)); + return queryShorts.map(q => `${q} `); } else { const q = nonEmpty[0].slice(1); const queryElement = SupportedQueries[q as keyof typeof SupportedQueries] as SupportedQuery; if(queryElement?.completer) { - candidates = candidates.concat(queryElement.completer(nonEmpty.slice(1), startingNewArg, config)); + return queryElement.completer(nonEmpty.slice(1), startingNewArg, config); } } - return candidates; + return []; } export function makeDefaultReplReadline(config: FlowrConfigOptions): readline.ReadLineOptions { diff --git a/src/queries/catalog/linter-query/linter-query-format.ts b/src/queries/catalog/linter-query/linter-query-format.ts index 05d5a27de6e..174acd3ba6c 100644 --- a/src/queries/catalog/linter-query/linter-query-format.ts +++ b/src/queries/catalog/linter-query/linter-query-format.ts @@ -17,6 +17,7 @@ import { printAsMs } from '../../../util/text/time'; import { codeInline } from '../../../documentation/doc-util/doc-code'; import type { FlowrConfigOptions } from '../../../config'; import type { ReplOutput } from '../../../cli/repl/commands/repl-main'; +import { isNotUndefined } from '../../../util/assert'; export interface LinterQuery extends BaseQueryFormat { readonly type: 'linter'; @@ -68,13 +69,12 @@ function linterQueryLineParser(output: ReplOutput, line: readonly string[], _con } function linterQueryCompleter(line: readonly string[], startingNewArg: boolean, _config: FlowrConfigOptions): string[] { - const completions: string[] = []; const rulesPrefixNotPresent = line.length == 0 || (line.length == 1 && line[0].length < rulesPrefix.length); const rulesNotFinished = line.length == 1 && line[0].startsWith(rulesPrefix) && !startingNewArg; if(rulesPrefixNotPresent) { // Return complete rules prefix - completions.push(`${rulesPrefix}`); + return [`${rulesPrefix}`]; } else if(rulesNotFinished) { const allRules = Object.keys(LintingRules); const existingRules = line[0].slice(rulesPrefix.length).split(',').map(r => r.trim()); @@ -83,23 +83,22 @@ function linterQueryCompleter(line: readonly string[], startingNewArg: boolean, if(!allRules.includes(lastRule)) { // Complete the last rule or add all possible rules that are not used yet - for(const ruleName of unusedRules) { - if(ruleName.startsWith(lastRule)) { + return unusedRules.map(r => { + if(r.startsWith(lastRule)) { const ending = unusedRules.length > 1 ? ',' : ' '; - completions.push(`${rulesPrefix}${existingRules.slice(0, -1).concat([ruleName]).join(',')}${ending}`); + return `${rulesPrefix}${existingRules.slice(0, -1).concat([r]).join(',')}${ending}`; } - } + }).filter(r => isNotUndefined(r)); } else if(unusedRules.length > 0) { // Add a comma, if the current last rule is complete - completions.push(`${rulesPrefix}${existingRules.join(',')},`); + return [`${rulesPrefix}${existingRules.join(',')},`]; } else { // All rules are used, complete with a space - completions.push(`${rulesPrefix}${existingRules.join(',')} `); + return [`${rulesPrefix}${existingRules.join(',')} `]; } } - // TODO Add file protocol if rules are finished - return completions; + return []; } export const LinterQueryDefinition = { From 9ce847651324138ea251c15110b8119de646d1cf Mon Sep 17 00:00:00 2001 From: MaxAtoms <7847075+MaxAtoms@users.noreply.github.com> Date: Mon, 27 Oct 2025 14:09:23 +0100 Subject: [PATCH 15/20] feat(repl): complete current linter rule only --- src/cli/repl/core.ts | 27 +++++++++++--- .../config-query/config-query-format.ts | 15 ++++---- .../linter-query/linter-query-format.ts | 37 +++++++++---------- src/queries/query.ts | 3 +- 4 files changed, 50 insertions(+), 32 deletions(-) diff --git a/src/cli/repl/core.ts b/src/cli/repl/core.ts index a71fc21ebc1..26588e9a115 100644 --- a/src/cli/repl/core.ts +++ b/src/cli/repl/core.ts @@ -32,6 +32,19 @@ function replCompleterKeywords() { } const defaultHistoryFile = path.join(os.tmpdir(), '.flowrhistory'); +/** + * Completion suggestions for a specific REPL command + */ +export interface CommandCompletions { + /** The possible completions for the current argument */ + readonly completions: string[]; + /** + * The current argument fragment being completed, if any. + * This is relevant if an argument is composed of multiple parts (e.g. comma-separated lists). + */ + readonly argumentPart?: string; +} + /** * Used by the repl to provide automatic completions for a given (partial) input line */ @@ -45,6 +58,7 @@ export function replCompleter(line: string, config: FlowrConfigOptions): [string const commandNameColon = replCompleterKeywords().find(k => splitLine[0] === k); if(commandNameColon) { const completions: string[] = []; + let currentArg = startingNewArg ? '' : splitLine[splitLine.length - 1]; const commandName = commandNameColon.slice(1); const cmd = getCommand(commandName); @@ -53,13 +67,16 @@ export function replCompleter(line: string, config: FlowrConfigOptions): [string const options = scripts[commandName as keyof typeof scripts].options; completions.push(...getValidOptionsForCompletion(options, splitLine).map(o => `${o} `)); } else if(commandName.startsWith('query')) { - completions.push(...replQueryCompleter(splitLine, startingNewArg, config)); + const { completions: queryCompletions, argumentPart: splitArg } = replQueryCompleter(splitLine, startingNewArg, config); + if(splitArg !== undefined) { + currentArg = splitArg; + } + completions.push(...queryCompletions); } else { // autocomplete command arguments (specifically, autocomplete the file:// protocol) completions.push(fileProtocol); } - const currentArg = startingNewArg ? '' : splitLine[splitLine.length - 1]; return [completions.filter(a => a.startsWith(currentArg)), currentArg]; } } @@ -68,11 +85,11 @@ export function replCompleter(line: string, config: FlowrConfigOptions): [string return [replCompleterKeywords().filter(k => k.startsWith(line)).map(k => `${k} `), line]; } -function replQueryCompleter(splitLine: readonly string[], startingNewArg: boolean, config: FlowrConfigOptions): string[] { +function replQueryCompleter(splitLine: readonly string[], startingNewArg: boolean, config: FlowrConfigOptions): CommandCompletions { const nonEmpty = splitLine.slice(1).map(s => s.trim()).filter(s => s.length > 0); const queryShorts = Object.keys(SupportedQueries).map(q => `@${q}`).concat(['help']); if(nonEmpty.length == 0 || (nonEmpty.length == 1 && queryShorts.some(q => q.startsWith(nonEmpty[0]) && nonEmpty[0] !== q && !startingNewArg))) { - return queryShorts.map(q => `${q} `); + return { completions: queryShorts.map(q => `${q} `) }; } else { const q = nonEmpty[0].slice(1); const queryElement = SupportedQueries[q as keyof typeof SupportedQueries] as SupportedQuery; @@ -81,7 +98,7 @@ function replQueryCompleter(splitLine: readonly string[], startingNewArg: boolea } } - return []; + return { completions: [] }; } export function makeDefaultReplReadline(config: FlowrConfigOptions): readline.ReadLineOptions { diff --git a/src/queries/catalog/config-query/config-query-format.ts b/src/queries/catalog/config-query/config-query-format.ts index 3875a2aeaba..142655b4ec6 100644 --- a/src/queries/catalog/config-query/config-query-format.ts +++ b/src/queries/catalog/config-query/config-query-format.ts @@ -8,6 +8,7 @@ import { jsonReplacer } from '../../../util/json'; import type { DeepPartial } from 'ts-essentials'; import type { ParsedQueryLine, SupportedQuery } from '../../query'; import type { ReplOutput } from '../../../cli/repl/commands/repl-main'; +import type { CommandCompletions } from '../../../cli/repl/core'; export interface ConfigQuery extends BaseQueryFormat { readonly type: 'config'; @@ -18,16 +19,16 @@ export interface ConfigQueryResult extends BaseQueryResult { readonly config: FlowrConfigOptions; } -function configReplCompleter(partialLine: readonly string[], _startingNewArg: boolean, config: FlowrConfigOptions): string[] { +function configReplCompleter(partialLine: readonly string[], _startingNewArg: boolean, config: FlowrConfigOptions): CommandCompletions { if(partialLine.length === 0) { // update specific fields - return ['+']; + return { completions: ['+'] }; } else if(partialLine.length === 1 && partialLine[0].startsWith('+')) { const path = partialLine[0].slice(1).split('.').filter(p => p.length > 0); const fullPath = path.slice(); const lastPath = partialLine[0].endsWith('.') ? '' : path.pop() ?? ''; if(lastPath.endsWith('=')) { - return []; + return { completions: [] }; } const subConfig = path.reduce((obj, key) => ( obj && (obj as Record)[key] !== undefined && typeof (obj as Record)[key] === 'object') ? (obj as Record)[key] as object : obj, config); @@ -36,15 +37,15 @@ function configReplCompleter(partialLine: readonly string[], _startingNewArg: bo .filter(k => k.startsWith(lastPath) && k !== lastPath) .map(k => `${partialLine[0].slice(0,1)}${[...path, k].join('.')}`); if(have.length > 0) { - return have; + return { completions: have }; } else if(lastPath.length > 0) { - return [`${partialLine[0].slice(0,1)}${fullPath.join('.')}.`]; + return { completions: [`${partialLine[0].slice(0,1)}${fullPath.join('.')}.`] }; } } - return [`${partialLine[0].slice(0,1)}${fullPath.join('.')}=`]; + return { completions: [`${partialLine[0].slice(0,1)}${fullPath.join('.')}=`] }; } - return []; + return { completions: [] }; } function configQueryLineParser(output: ReplOutput, line: readonly string[], _config: FlowrConfigOptions): ParsedQueryLine { diff --git a/src/queries/catalog/linter-query/linter-query-format.ts b/src/queries/catalog/linter-query/linter-query-format.ts index 174acd3ba6c..344e8b81701 100644 --- a/src/queries/catalog/linter-query/linter-query-format.ts +++ b/src/queries/catalog/linter-query/linter-query-format.ts @@ -17,7 +17,8 @@ import { printAsMs } from '../../../util/text/time'; import { codeInline } from '../../../documentation/doc-util/doc-code'; import type { FlowrConfigOptions } from '../../../config'; import type { ReplOutput } from '../../../cli/repl/commands/repl-main'; -import { isNotUndefined } from '../../../util/assert'; +import type { CommandCompletions } from '../../../cli/repl/core'; +import { fileProtocol } from '../../../r-bridge/retriever'; export interface LinterQuery extends BaseQueryFormat { readonly type: 'linter'; @@ -68,37 +69,35 @@ function linterQueryLineParser(output: ReplOutput, line: readonly string[], _con return { query: [{ type: 'linter', rules: rules }], rCode: input } ; } -function linterQueryCompleter(line: readonly string[], startingNewArg: boolean, _config: FlowrConfigOptions): string[] { +function linterQueryCompleter(line: readonly string[], startingNewArg: boolean, _config: FlowrConfigOptions): CommandCompletions { const rulesPrefixNotPresent = line.length == 0 || (line.length == 1 && line[0].length < rulesPrefix.length); const rulesNotFinished = line.length == 1 && line[0].startsWith(rulesPrefix) && !startingNewArg; + const endOfRules = line.length == 1 && startingNewArg; if(rulesPrefixNotPresent) { - // Return complete rules prefix - return [`${rulesPrefix}`]; + return { completions: [`${rulesPrefix}`] }; + } else if(endOfRules) { + return { completions: [fileProtocol], argumentPart: '' }; } else if(rulesNotFinished) { + const rulesWithoutPrefix = line[0].slice(rulesPrefix.length); + const usedRules = rulesWithoutPrefix.split(',').map(r => r.trim()); const allRules = Object.keys(LintingRules); - const existingRules = line[0].slice(rulesPrefix.length).split(',').map(r => r.trim()); - const lastRule = existingRules[existingRules.length - 1]; - const unusedRules = allRules.filter(r => !existingRules.includes(r)); + const unusedRules = allRules.filter(r => !usedRules.includes(r)); + const lastRule = usedRules[usedRules.length - 1]; + const lastRuleIsUnfinished = !allRules.includes(lastRule); - if(!allRules.includes(lastRule)) { - // Complete the last rule or add all possible rules that are not used yet - return unusedRules.map(r => { - if(r.startsWith(lastRule)) { - const ending = unusedRules.length > 1 ? ',' : ' '; - return `${rulesPrefix}${existingRules.slice(0, -1).concat([r]).join(',')}${ending}`; - } - }).filter(r => isNotUndefined(r)); + if(lastRuleIsUnfinished) { + // Return all rules that have not been added yet + return { completions: unusedRules, argumentPart: lastRule }; } else if(unusedRules.length > 0) { // Add a comma, if the current last rule is complete - return [`${rulesPrefix}${existingRules.join(',')},`]; + return { completions: [','], argumentPart: '' }; } else { // All rules are used, complete with a space - return [`${rulesPrefix}${existingRules.join(',')} `]; + return { completions: [' '], argumentPart: '' }; } } - // TODO Add file protocol if rules are finished - return []; + return { completions: [] }; } export const LinterQueryDefinition = { diff --git a/src/queries/query.ts b/src/queries/query.ts index 36eeb4d0d1f..02e36b3d4ec 100644 --- a/src/queries/query.ts +++ b/src/queries/query.ts @@ -53,6 +53,7 @@ import { import type { ReadonlyFlowrAnalysisProvider } from '../project/flowr-analyzer'; import { log } from '../util/log'; import type { ReplOutput } from '../cli/repl/commands/repl-main'; +import type { CommandCompletions } from '../cli/repl/core'; /** * These are all queries that can be executed from within flowR @@ -102,7 +103,7 @@ export interface ParsedQueryLine { export interface SupportedQuery { executor: QueryExecutor, Promise> /** optional completion in, e.g., the repl */ - completer?: (splitLine: readonly string[], startingNewArg: boolean, config: FlowrConfigOptions) => string[] + completer?: (splitLine: readonly string[], startingNewArg: boolean, config: FlowrConfigOptions) => CommandCompletions; /** optional query construction from an, e.g., repl line */ fromLine?: (output: ReplOutput, splitLine: readonly string[], config: FlowrConfigOptions) => ParsedQueryLine asciiSummarizer: (formatter: OutputFormatter, analyzer: ReadonlyFlowrAnalysisProvider, queryResults: BaseQueryResult, resultStrings: string[], query: readonly Query[]) => AsyncOrSync From 78a0c3abe4fa3e772e89eaba926cfa4418b8b6bb Mon Sep 17 00:00:00 2001 From: MaxAtoms <7847075+MaxAtoms@users.noreply.github.com> Date: Mon, 27 Oct 2025 15:14:19 +0100 Subject: [PATCH 16/20] feat-fix(repl): properly complete file arg --- src/queries/catalog/linter-query/linter-query-format.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/queries/catalog/linter-query/linter-query-format.ts b/src/queries/catalog/linter-query/linter-query-format.ts index 344e8b81701..78a14d00200 100644 --- a/src/queries/catalog/linter-query/linter-query-format.ts +++ b/src/queries/catalog/linter-query/linter-query-format.ts @@ -72,12 +72,12 @@ function linterQueryLineParser(output: ReplOutput, line: readonly string[], _con function linterQueryCompleter(line: readonly string[], startingNewArg: boolean, _config: FlowrConfigOptions): CommandCompletions { const rulesPrefixNotPresent = line.length == 0 || (line.length == 1 && line[0].length < rulesPrefix.length); const rulesNotFinished = line.length == 1 && line[0].startsWith(rulesPrefix) && !startingNewArg; - const endOfRules = line.length == 1 && startingNewArg; + const endOfRules = line.length == 1 && startingNewArg || line.length == 2; if(rulesPrefixNotPresent) { return { completions: [`${rulesPrefix}`] }; } else if(endOfRules) { - return { completions: [fileProtocol], argumentPart: '' }; + return { completions: [fileProtocol] }; } else if(rulesNotFinished) { const rulesWithoutPrefix = line[0].slice(rulesPrefix.length); const usedRules = rulesWithoutPrefix.split(',').map(r => r.trim()); From b1ed564b49b2eade3d3b3fa064638c6ba9f7217b Mon Sep 17 00:00:00 2001 From: MaxAtoms <7847075+MaxAtoms@users.noreply.github.com> Date: Tue, 28 Oct 2025 10:53:55 +0100 Subject: [PATCH 17/20] test(repl): add linter query completion tests --- test/functionality/_helper/repl.ts | 17 ++++++++++++++++ .../cli/repl/linter-query-repl.test.ts | 20 +++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 test/functionality/_helper/repl.ts create mode 100644 test/functionality/cli/repl/linter-query-repl.test.ts diff --git a/test/functionality/_helper/repl.ts b/test/functionality/_helper/repl.ts new file mode 100644 index 00000000000..754df09c699 --- /dev/null +++ b/test/functionality/_helper/repl.ts @@ -0,0 +1,17 @@ +import type { FlowrConfigOptions } from '../../../src/config'; +import { defaultConfigOptions } from '../../../src/config'; +import type { CommandCompletions } from '../../../src/cli/repl/core'; +import { expect, test } from 'vitest'; + +export function assertReplCompletions( + completer: (splitLine: readonly string[], startingNewArg: boolean, config: FlowrConfigOptions) => CommandCompletions, + label: string, + startingNewArg: boolean, + splitLine: readonly string[], + expectedCompletions: readonly string[] +) { + test(label, () => { + const result = completer(splitLine, startingNewArg, defaultConfigOptions); + expect(result.completions).toEqual(expectedCompletions); + }); +} diff --git a/test/functionality/cli/repl/linter-query-repl.test.ts b/test/functionality/cli/repl/linter-query-repl.test.ts new file mode 100644 index 00000000000..e0f91d59f82 --- /dev/null +++ b/test/functionality/cli/repl/linter-query-repl.test.ts @@ -0,0 +1,20 @@ +import { describe } from 'vitest'; +import { assertReplCompletions } from '../../_helper/repl'; +import { SupportedQueries } from '../../../../src/queries/query'; +import { fileProtocol } from '../../../../src/r-bridge/retriever'; +import { LintingRules } from '../../../../src/linter/linter-rules'; + +describe('Linter Query REPL Completions', () => { + const c = SupportedQueries['linter'].completer; + const r = Object.keys(LintingRules); + assertReplCompletions(c, 'empty arguments', true, [''], ['rules:']); + assertReplCompletions(c, 'partial prefix', false, ['r'], ['rules:']); + assertReplCompletions(c, 'no rules', false, ['rules:'], r); + assertReplCompletions(c, 'partial rule', false, ['rules:d'], r); + assertReplCompletions(c, 'partial unique rule', false, ['rules:dead'], r); + assertReplCompletions(c, 'multiple rules, one partial', false, ['rules:dead-code,file-path-val'], r.filter(l => !(l === 'dead-code'))); + assertReplCompletions(c, 'multiple rules, no new one', false, ['rules:dead-code,file-path-validity'], [',']); + assertReplCompletions(c, 'multiple rules, starting new one', false, ['rules:dead-code,file-path-validity,'], r.filter(l => !['dead-code','file-path-validity'].includes(l))); + assertReplCompletions(c, 'all rules used', false, [`rules:${r.join(',')}`], [' ']); + assertReplCompletions(c, 'rules finished', true, ['rules:dead'], [fileProtocol]); +}); From 9b43af267ee1ff6cf9816dbf85f4a795a98a5981 Mon Sep 17 00:00:00 2001 From: MaxAtoms <7847075+MaxAtoms@users.noreply.github.com> Date: Tue, 28 Oct 2025 13:10:36 +0100 Subject: [PATCH 18/20] refactor(repl): review comments --- src/cli/repl/core.ts | 2 +- .../cli/repl/linter-query-repl.test.ts | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/cli/repl/core.ts b/src/cli/repl/core.ts index 3e2da56be34..e7c73029dfc 100644 --- a/src/cli/repl/core.ts +++ b/src/cli/repl/core.ts @@ -66,7 +66,7 @@ export function replCompleter(line: string, config: FlowrConfigOptions): [string if(cmd?.script === true){ // autocomplete script arguments const options = scripts[commandName as keyof typeof scripts].options; - completions.push(...getValidOptionsForCompletion(options, splitLine).map(o => `${o} `)); + completions.concat(...getValidOptionsForCompletion(options, splitLine).map(o => `${o} `)); } else if(commandName.startsWith('query')) { const { completions: queryCompletions, argumentPart: splitArg } = replQueryCompleter(splitLine, startingNewArg, config); if(splitArg !== undefined) { diff --git a/test/functionality/cli/repl/linter-query-repl.test.ts b/test/functionality/cli/repl/linter-query-repl.test.ts index e0f91d59f82..5ee86718922 100644 --- a/test/functionality/cli/repl/linter-query-repl.test.ts +++ b/test/functionality/cli/repl/linter-query-repl.test.ts @@ -6,15 +6,15 @@ import { LintingRules } from '../../../../src/linter/linter-rules'; describe('Linter Query REPL Completions', () => { const c = SupportedQueries['linter'].completer; - const r = Object.keys(LintingRules); + const all = Object.keys(LintingRules); assertReplCompletions(c, 'empty arguments', true, [''], ['rules:']); assertReplCompletions(c, 'partial prefix', false, ['r'], ['rules:']); - assertReplCompletions(c, 'no rules', false, ['rules:'], r); - assertReplCompletions(c, 'partial rule', false, ['rules:d'], r); - assertReplCompletions(c, 'partial unique rule', false, ['rules:dead'], r); - assertReplCompletions(c, 'multiple rules, one partial', false, ['rules:dead-code,file-path-val'], r.filter(l => !(l === 'dead-code'))); + assertReplCompletions(c, 'no rules', false, ['rules:'], all); + assertReplCompletions(c, 'partial rule', false, ['rules:d'], all); + assertReplCompletions(c, 'partial unique rule', false, ['rules:dead'], all); + assertReplCompletions(c, 'multiple rules, one partial', false, ['rules:dead-code,file-path-val'], all.filter(l => !(l === 'dead-code'))); assertReplCompletions(c, 'multiple rules, no new one', false, ['rules:dead-code,file-path-validity'], [',']); - assertReplCompletions(c, 'multiple rules, starting new one', false, ['rules:dead-code,file-path-validity,'], r.filter(l => !['dead-code','file-path-validity'].includes(l))); - assertReplCompletions(c, 'all rules used', false, [`rules:${r.join(',')}`], [' ']); - assertReplCompletions(c, 'rules finished', true, ['rules:dead'], [fileProtocol]); + assertReplCompletions(c, 'multiple rules, starting new one', false, ['rules:dead-code,file-path-validity,'], all.filter(l => !['dead-code','file-path-validity'].includes(l))); + assertReplCompletions(c, 'all rules used', false, [`rules:${all.join(',')}`], [' ']); + assertReplCompletions(c, 'rules finished', true, ['rules:dead'], [fileProtocol]); }); From 4b7093c44166e0e74350a4072dfc66fe692a5591 Mon Sep 17 00:00:00 2001 From: MaxAtoms <7847075+MaxAtoms@users.noreply.github.com> Date: Tue, 28 Oct 2025 13:34:39 +0100 Subject: [PATCH 19/20] fixup! refactor(repl): review comments --- src/cli/repl/core.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cli/repl/core.ts b/src/cli/repl/core.ts index e7c73029dfc..e868c2f8de3 100644 --- a/src/cli/repl/core.ts +++ b/src/cli/repl/core.ts @@ -58,7 +58,7 @@ export function replCompleter(line: string, config: FlowrConfigOptions): [string if(splitLine.length > 1 || startingNewArg){ const commandNameColon = replCompleterKeywords().find(k => splitLine[0] === k); if(commandNameColon) { - const completions: string[] = []; + let completions: string[] = []; let currentArg = startingNewArg ? '' : splitLine[splitLine.length - 1]; const commandName = commandNameColon.slice(1); @@ -66,13 +66,13 @@ export function replCompleter(line: string, config: FlowrConfigOptions): [string if(cmd?.script === true){ // autocomplete script arguments const options = scripts[commandName as keyof typeof scripts].options; - completions.concat(...getValidOptionsForCompletion(options, splitLine).map(o => `${o} `)); + completions = completions.concat(getValidOptionsForCompletion(options, splitLine).map(o => `${o} `)); } else if(commandName.startsWith('query')) { const { completions: queryCompletions, argumentPart: splitArg } = replQueryCompleter(splitLine, startingNewArg, config); if(splitArg !== undefined) { currentArg = splitArg; } - completions.push(...queryCompletions); + completions = completions.concat(queryCompletions); } else { // autocomplete command arguments (specifically, autocomplete the file:// protocol) completions.push(fileProtocol); From dcbc0f26608723a568387b007b04c97651257eea Mon Sep 17 00:00:00 2001 From: MaxAtoms <7847075+MaxAtoms@users.noreply.github.com> Date: Tue, 28 Oct 2025 20:53:57 +0100 Subject: [PATCH 20/20] test(repl): add config query completion tests --- test/functionality/_helper/repl.ts | 17 ++-- .../cli/repl/config-query-repl.test.ts | 67 ++++++++++++++++ .../cli/repl/linter-query-repl.test.ts | 80 ++++++++++++++++--- 3 files changed, 145 insertions(+), 19 deletions(-) create mode 100644 test/functionality/cli/repl/config-query-repl.test.ts diff --git a/test/functionality/_helper/repl.ts b/test/functionality/_helper/repl.ts index 754df09c699..f0554537853 100644 --- a/test/functionality/_helper/repl.ts +++ b/test/functionality/_helper/repl.ts @@ -3,15 +3,18 @@ import { defaultConfigOptions } from '../../../src/config'; import type { CommandCompletions } from '../../../src/cli/repl/core'; import { expect, test } from 'vitest'; -export function assertReplCompletions( - completer: (splitLine: readonly string[], startingNewArg: boolean, config: FlowrConfigOptions) => CommandCompletions, - label: string, - startingNewArg: boolean, - splitLine: readonly string[], +export interface ReplCompletionTestCase { + completer: (splitLine: readonly string[], startingNewArg: boolean, config: FlowrConfigOptions) => CommandCompletions, + label: string, + startingNewArg: boolean, + config?: object, + splitLine: readonly string[], expectedCompletions: readonly string[] -) { +} + +export function assertReplCompletions({ completer, label, startingNewArg, splitLine, config = defaultConfigOptions, expectedCompletions }: ReplCompletionTestCase) { test(label, () => { - const result = completer(splitLine, startingNewArg, defaultConfigOptions); + const result = completer(splitLine, startingNewArg, config as FlowrConfigOptions); expect(result.completions).toEqual(expectedCompletions); }); } diff --git a/test/functionality/cli/repl/config-query-repl.test.ts b/test/functionality/cli/repl/config-query-repl.test.ts new file mode 100644 index 00000000000..f15924e2c65 --- /dev/null +++ b/test/functionality/cli/repl/config-query-repl.test.ts @@ -0,0 +1,67 @@ +import { describe } from 'vitest'; +import { SupportedQueries } from '../../../../src/queries/query'; +import { assertReplCompletions } from '../../_helper/repl'; + +describe('Config Query REPL Completions', () => { + const completer = SupportedQueries['config'].completer; + assertReplCompletions({ completer, + label: 'empty arguments', + startingNewArg: true, + splitLine: [], + expectedCompletions: ['+'] + }); + assertReplCompletions({ completer, + label: 'all root nodes', + startingNewArg: false, + config: { aTopNode: 'test', bTopNode: 'test' }, + splitLine: ['+'], + expectedCompletions: ['+aTopNode', '+bTopNode'], + }); + assertReplCompletions({ completer, + label: 'provides completion for partial root node', + startingNewArg: false, + config: { aTopNode: 'test', bTopNode: 'test' }, + splitLine: ['+a'], + expectedCompletions: ['+aTopNode'], + }); + assertReplCompletions({ completer, + label: 'adds dot after full root node', + startingNewArg: false, + config: { aTopNode: 'test', bTopNode: { aSecondNode: 'test', bSecondNode: 'test', } }, + splitLine: ['+bTopNode'], + expectedCompletions: ['+bTopNode.'] + }); + assertReplCompletions({ completer, + label: 'all second level nodes', + startingNewArg: false, + config: { aTopNode: 'test', bTopNode: { aSecondNode: 'test', bSecondNode: 'test', } }, + splitLine: ['+bTopNode.'], + expectedCompletions: ['+bTopNode.aSecondNode', '+bTopNode.bSecondNode'], + }); + assertReplCompletions({ completer, + label: 'provides completion for partial second level node', + startingNewArg: false, + config: { aTopNode: 'test', bTopNode: { aSecondNode: 'test', bSecondNode: 'test', } }, + splitLine: ['+bTopNode.b'], + expectedCompletions: ['+bTopNode.bSecondNode'], + }); + assertReplCompletions({ completer, + label: 'adds equals sign after full path', + startingNewArg: false, + config: { aTopNode: 'test', bTopNode: { aSecondNode: 'test', bSecondNode: 'test', } }, + splitLine: ['+bTopNode.bSecondNode'], + expectedCompletions: ['+bTopNode.bSecondNode='], + }); + assertReplCompletions({ completer, + label: 'no completions after equals sign', + startingNewArg: false, + splitLine: ['+someConfigThing='], + expectedCompletions: [], + }); + assertReplCompletions({ completer, + label: 'no completions after config update string', + startingNewArg: true, + splitLine: ['+someConfigThing', 'abc'], + expectedCompletions: [], + }); +}); diff --git a/test/functionality/cli/repl/linter-query-repl.test.ts b/test/functionality/cli/repl/linter-query-repl.test.ts index 5ee86718922..14c7917c625 100644 --- a/test/functionality/cli/repl/linter-query-repl.test.ts +++ b/test/functionality/cli/repl/linter-query-repl.test.ts @@ -5,16 +5,72 @@ import { fileProtocol } from '../../../../src/r-bridge/retriever'; import { LintingRules } from '../../../../src/linter/linter-rules'; describe('Linter Query REPL Completions', () => { - const c = SupportedQueries['linter'].completer; - const all = Object.keys(LintingRules); - assertReplCompletions(c, 'empty arguments', true, [''], ['rules:']); - assertReplCompletions(c, 'partial prefix', false, ['r'], ['rules:']); - assertReplCompletions(c, 'no rules', false, ['rules:'], all); - assertReplCompletions(c, 'partial rule', false, ['rules:d'], all); - assertReplCompletions(c, 'partial unique rule', false, ['rules:dead'], all); - assertReplCompletions(c, 'multiple rules, one partial', false, ['rules:dead-code,file-path-val'], all.filter(l => !(l === 'dead-code'))); - assertReplCompletions(c, 'multiple rules, no new one', false, ['rules:dead-code,file-path-validity'], [',']); - assertReplCompletions(c, 'multiple rules, starting new one', false, ['rules:dead-code,file-path-validity,'], all.filter(l => !['dead-code','file-path-validity'].includes(l))); - assertReplCompletions(c, 'all rules used', false, [`rules:${all.join(',')}`], [' ']); - assertReplCompletions(c, 'rules finished', true, ['rules:dead'], [fileProtocol]); + const completer = SupportedQueries['linter'].completer; + const allRules = Object.keys(LintingRules); + assertReplCompletions({ completer, + label: 'empty arguments', + startingNewArg: true, + splitLine: [''], + expectedCompletions: ['rules:'] + }); + assertReplCompletions({ completer, + label: 'partial prefix', + startingNewArg: false, + splitLine: ['r'], + expectedCompletions: ['rules:'] + }); + assertReplCompletions({ completer, + label: 'no rules', + startingNewArg: false, + splitLine: ['rules:'], + expectedCompletions: allRules + }); + assertReplCompletions({ completer, + label: 'partial rule', + startingNewArg: false, + splitLine: ['rules:d'], + expectedCompletions: allRules + }); + assertReplCompletions({ completer, + label: 'partial unique rule', + startingNewArg: false, + splitLine: ['rules:dead'], + expectedCompletions: allRules + }); + assertReplCompletions({ completer, + label: 'multiple rules, one partial', + startingNewArg: false, + splitLine: ['rules:dead-code,file-path-val'], + expectedCompletions: allRules.filter(l => !(l === 'dead-code')) + }); + assertReplCompletions({ completer, + label: 'multiple rules, no new one', + startingNewArg: false, + splitLine: ['rules:dead-code,file-path-validity'], + expectedCompletions: [','] + }); + assertReplCompletions({ completer, + label: 'multiple rules, starting new one', + startingNewArg: false, + splitLine: ['rules:dead-code,file-path-validity,'], + expectedCompletions: allRules.filter(l => !['dead-code','file-path-validity'].includes(l)) + }); + assertReplCompletions({ completer, + label: 'all rules used', + startingNewArg: false, + splitLine: [`rules:${allRules.join(',')}`], + expectedCompletions: [' '] + }); + assertReplCompletions({ completer, + label: 'rules finished', + startingNewArg: true, + splitLine: ['rules:dead'], + expectedCompletions: [fileProtocol] + }); + assertReplCompletions({ completer, + label: 'rules finished', + startingNewArg: true, + splitLine: ['rules:dead'], + expectedCompletions: [fileProtocol] + }); });