Skip to content

Commit 7114d0d

Browse files
MaxAtomsstimjannikEagleoutIce
authored
Project Builder API (#1780)
* feat(builder): add first experiment * feat(builder): add engine and query support * feat(builder): fixes from review * feat(plugin): add FlowrAnalyzerPlugin and DESCRIPTION-file plugin * test(plugin): library versions from DESCRIPTION test * feat(plugin): add (un)registering plugins in FlowrAnalyzerBuilder * feat(builder): simplifications * feat(builder): use EngineConfig type * feat(builder): use varargs/rest parameter syntax * refactor(plugin): use abstract classes instead of interfaces only * lint-fix(plugin): fixed linter issues * lint-fix(plugin): fixed linter issues * feat(plugin): add new DESCRIPTION parser * feat(plugin): add loading order from DESCRIPTION retriever * test-fix(plugin): adjust description file test * lint(plugin): adjust analyzer and pluginConfig to be ignored * lint-fix(plugin): fix mixed spaces and tabs * lint-fix(plugin): unify use of `:` for type annotations * refactor(plugin/files): move dcf-parsing to file * feat(plugin): derive package version from multiple constraints * wip(plugin): add libraries to dependency query * lint-fix(plugin): add missing semicolon * feat(plugin): add description file infos to dependency query * feat-fix(plugin): namespace returns undefined for constraints if no pkg * test(dependencies-query): add tests for packages with versions * lint-fix(dependencies-query): fix braces * feat(builder): add cfg and force param * feat(builder): add reset function * feat(analyzer): use analyzer in repl * lint-fix: type-annotation spacing * feat-fix: re-introduce timing info to repl * feat: use analyzer in repl query * feat: expose argument parser per command * feat: pass analyzer to repl-query * feat: add parse pipeline to analyzer * feat: introduce simpleCfg to analyzer * feat: use analyzer for server * feat: use analyzer for search, query, and linter * feat: use analyzer to get cfg in queries * feat-fix: prevent excessive R-Shell creation * feat-fix: set correct slicing params noMagicComments query parameter will be used to set autoSelectIf * feat-fix: clean-up * feat-fix(analyzer): cfg caching * doc(analyzer): clean-up * feat(config): allow config amendment without return * doc(repl): improve comments * feat-fix(query): resolve promise Stupid beginner mistake :( * feat(analyzer): use analyzer for queries * feat-fix: use more specific type * feat-fix(libraries): include libraries in new dependencies query * feat(analyzer): add documentation comments * feat-fix(analyzer): rename ArrayMap Reflects actual generality of the approach * feat-fix(repl): fix config updates * feat-fix(analyzer): proposal to generalize analyzer over parser * feat-fix(libraries): versions for ':::' and '::' reduced to necessary * test-fix(libraries): replace old values in expected results * feat-fix(server): use then syntax * feat-fix(dataframe): map nodes Previously, the code expected an implicit link between the search result elements and the normalized AST. * refactor: recover buildability * refactor: output tunes to existing tests * refactor: minor patches to asyncness * refactor: design of separate cache api * feat: flo's opinionated analyzer caching updates * refactor: simplification iteration * refactor: defer early force in query print * refactor: query improvements for projects * feat-fix: data naming * lint-fix: handle linter awaits * refactor: remove only debug fixture * Add analyzer to Wiki (#1940) * docs(analyzer): add analyzer to wiki * docs(analyzer): use analyzer for ast and dataflow * docs(analyzer): add query api example * docs(analyzer): fix line breaks * feat-fix(analyzer): kill open engines --------- Co-authored-by: Florian Sihler <[email protected]> --------- Co-authored-by: stimjannik <[email protected]> Co-authored-by: Florian Sihler <[email protected]>
1 parent 9434149 commit 7114d0d

File tree

113 files changed

+2060
-895
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

113 files changed

+2060
-895
lines changed

src/cli/flowr.ts

Lines changed: 3 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,23 @@ import { NetServer, WebSocketServerWrapper } from './repl/server/net';
1111
import { flowrVersion } from '../util/version';
1212
import commandLineUsage from 'command-line-usage';
1313
import { log, LogLevel } from '../util/log';
14-
import { bold, ColorEffect, Colors, FontStyles, formatter, italic, setFormatter, voidFormatter } from '../util/text/ansi';
14+
import { FontStyles, formatter, italic, setFormatter, voidFormatter } from '../util/text/ansi';
1515
import commandLineArgs from 'command-line-args';
1616
import type { EngineConfig, FlowrConfigOptions, KnownEngines } from '../config';
17-
import { amendConfig, getConfig, getEngineConfig, parseConfig } from '../config';
17+
import { amendConfig, getConfig, parseConfig } from '../config';
1818
import { guard } from '../util/assert';
1919
import type { ScriptInformation } from './common/scripts-info';
2020
import { scripts } from './common/scripts-info';
21-
import { RShell, RShellReviveOptions } from '../r-bridge/shell';
2221
import { waitOnScript } from './repl/execute';
2322
import { standardReplOutput } from './repl/commands/repl-main';
2423
import { repl, replProcessAnswer } from './repl/core';
2524
import { printVersionInformation } from './repl/commands/repl-version';
2625
import { printVersionRepl } from './repl/print-version';
2726
import { defaultConfigFile, flowrMainOptionDefinitions, getScriptsText } from './flowr-main-options';
28-
import { TreeSitterExecutor } from '../r-bridge/lang-4.x/tree-sitter/tree-sitter-executor';
2927
import type { KnownParser } from '../r-bridge/parser';
3028
import fs from 'fs';
3129
import path from 'path';
30+
import { retrieveEngineInstances } from '../engines';
3231

3332
export const toolName = 'flowr';
3433

@@ -137,33 +136,6 @@ function createConfig(): FlowrConfigOptions {
137136
return config;
138137
}
139138

140-
141-
async function retrieveEngineInstances(config: FlowrConfigOptions): Promise<{ engines: KnownEngines, default: keyof KnownEngines }> {
142-
const engines: KnownEngines = {};
143-
if(getEngineConfig(config, 'r-shell')) {
144-
// we keep an active shell session to allow other parse investigations :)
145-
engines['r-shell'] = new RShell(getEngineConfig(config, 'r-shell'), {
146-
revive: RShellReviveOptions.Always,
147-
onRevive: (code, signal) => {
148-
const signalText = signal == null ? '' : ` and signal ${signal}`;
149-
console.log(formatter.format(`R process exited with code ${code}${signalText}. Restarting...`, { color: Colors.Magenta, effect: ColorEffect.Foreground }));
150-
console.log(italic(`If you want to exit, press either Ctrl+C twice, or enter ${bold(':quit')}`));
151-
}
152-
});
153-
}
154-
if(getEngineConfig(config, 'tree-sitter')) {
155-
await TreeSitterExecutor.initTreeSitter(getEngineConfig(config, 'tree-sitter'));
156-
engines['tree-sitter'] = new TreeSitterExecutor();
157-
}
158-
let defaultEngine = config.defaultEngine;
159-
if(!defaultEngine || !engines[defaultEngine]) {
160-
// if a default engine isn't specified, we just take the first one we have
161-
defaultEngine = Object.keys(engines)[0] as keyof KnownEngines;
162-
}
163-
log.info(`Using engines ${Object.keys(engines).join(', ')} with default ${defaultEngine}`);
164-
return { engines, default: defaultEngine };
165-
}
166-
167139
function hookSignalHandlers(engines: { engines: KnownEngines; default: keyof KnownEngines }) {
168140
const end = () => {
169141
if(options.execute === undefined) {

src/cli/repl/commands/repl-cfg.ts

Lines changed: 28 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,22 @@
1-
import type { ReplCommand, ReplOutput } from './repl-main';
2-
import { extractCfg } from '../../../control-flow/extract-cfg';
3-
import { createDataflowPipeline } from '../../../core/steps/pipeline/default-pipelines';
4-
import { fileProtocol, requestFromInput } from '../../../r-bridge/retriever';
1+
import type { ReplCodeCommand, ReplOutput } from './repl-main';
2+
import { fileProtocol } from '../../../r-bridge/retriever';
53
import { cfgToMermaid, cfgToMermaidUrl } from '../../../util/mermaid/cfg';
6-
import type { KnownParser } from '../../../r-bridge/parser';
74
import { ColorEffect, Colors, FontStyles } from '../../../util/text/ansi';
85
import type { ControlFlowInformation } from '../../../control-flow/control-flow-graph';
96
import type { NormalizedAst } from '../../../r-bridge/lang-4.x/ast/model/processing/decorate';
107
import type { CfgSimplificationPassName } from '../../../control-flow/cfg-simplification';
118
import { DefaultCfgSimplificationOrder } from '../../../control-flow/cfg-simplification';
12-
import type { FlowrConfigOptions } from '../../../config';
13-
14-
async function controlflow(parser: KnownParser, remainingLine: string, config: FlowrConfigOptions) {
15-
return await createDataflowPipeline(parser, {
16-
request: requestFromInput(remainingLine.trim())
17-
}, config).allRemainingSteps();
18-
}
19-
20-
function handleString(code: string): string {
21-
return code.startsWith('"') ? JSON.parse(code) as string : code;
22-
}
9+
import type { FlowrAnalysisProvider } from '../../../project/flowr-analyzer';
10+
import { handleString } from '../core';
2311

2412
function formatInfo(out: ReplOutput, type: string): string {
2513
return out.formatter.format(`Copied ${type} to clipboard.`, { color: Colors.White, effect: ColorEffect.Foreground, style: FontStyles.Italic });
2614
}
2715

28-
async function produceAndPrintCfg(shell: KnownParser, remainingLine: string, output: ReplOutput, simplifications: readonly CfgSimplificationPassName[], cfgConverter: (cfg: ControlFlowInformation, ast: NormalizedAst) => string, config: FlowrConfigOptions) {
29-
const result = await controlflow(shell, handleString(remainingLine), config);
30-
31-
const cfg = extractCfg(result.normalize, config, result.dataflow.graph, [...DefaultCfgSimplificationOrder, ...simplifications]);
32-
const mermaid = cfgConverter(cfg, result.normalize);
16+
async function produceAndPrintCfg(analyzer: FlowrAnalysisProvider, output: ReplOutput, simplifications: readonly CfgSimplificationPassName[], cfgConverter: (cfg: ControlFlowInformation, ast: NormalizedAst) => string) {
17+
const cfg = await analyzer.controlflow([...DefaultCfgSimplificationOrder, ...simplifications]);
18+
const normalizedAst = await analyzer.normalize();
19+
const mermaid = cfgConverter(cfg, normalizedAst);
3320
output.stdout(mermaid);
3421
try {
3522
const clipboard = await import('clipboardy');
@@ -39,45 +26,53 @@ async function produceAndPrintCfg(shell: KnownParser, remainingLine: string, out
3926
}
4027
}
4128

42-
export const controlflowCommand: ReplCommand = {
29+
export const controlflowCommand: ReplCodeCommand = {
4330
description: `Get mermaid code for the control-flow graph of R code, start with '${fileProtocol}' to indicate a file`,
31+
usesAnalyzer: true,
4432
usageExample: ':controlflow',
4533
aliases: [ 'cfg', 'cf' ],
4634
script: false,
47-
fn: async({ output, parser, remainingLine, config }) => {
48-
await produceAndPrintCfg(parser, remainingLine, output, [], cfgToMermaid, config);
35+
argsParser: (args: string) => handleString(args),
36+
fn: async({ output, analyzer }) => {
37+
await produceAndPrintCfg(analyzer, output, [], cfgToMermaid);
4938
}
5039
};
5140

5241

53-
export const controlflowStarCommand: ReplCommand = {
42+
export const controlflowStarCommand: ReplCodeCommand = {
5443
description: 'Returns the URL to mermaid.live',
44+
usesAnalyzer: true,
5545
usageExample: ':controlflow*',
5646
aliases: [ 'cfg*', 'cf*' ],
5747
script: false,
58-
fn: async({ output, parser, remainingLine, config }) => {
59-
await produceAndPrintCfg(parser, remainingLine, output, [], cfgToMermaidUrl, config);
48+
argsParser: (args: string) => handleString(args),
49+
fn: async({ output, analyzer }) => {
50+
await produceAndPrintCfg(analyzer, output, [], cfgToMermaidUrl);
6051
}
6152
};
6253

6354

64-
export const controlflowBbCommand: ReplCommand = {
55+
export const controlflowBbCommand: ReplCodeCommand = {
6556
description: `Get mermaid code for the control-flow graph with basic blocks, start with '${fileProtocol}' to indicate a file`,
57+
usesAnalyzer: true,
6658
usageExample: ':controlflowbb',
6759
aliases: [ 'cfgb', 'cfb' ],
6860
script: false,
69-
fn: async({ output, parser, remainingLine, config }) => {
70-
await produceAndPrintCfg(parser, remainingLine, output, ['to-basic-blocks'], cfgToMermaid, config);
61+
argsParser: (args: string) => handleString(args),
62+
fn: async({ output, analyzer }) => {
63+
await produceAndPrintCfg(analyzer, output, ['to-basic-blocks'], cfgToMermaid);
7164
}
7265
};
7366

7467

75-
export const controlflowBbStarCommand: ReplCommand = {
68+
export const controlflowBbStarCommand: ReplCodeCommand = {
7669
description: 'Returns the URL to mermaid.live',
70+
usesAnalyzer: true,
7771
usageExample: ':controlflowbb*',
7872
aliases: [ 'cfgb*', 'cfb*' ],
7973
script: false,
80-
fn: async({ output, parser, remainingLine, config }) => {
81-
await produceAndPrintCfg(parser, remainingLine, output, ['to-basic-blocks' ], cfgToMermaidUrl, config);
74+
argsParser: (args: string) => handleString(args),
75+
fn: async({ output, analyzer }) => {
76+
await produceAndPrintCfg(analyzer, output, ['to-basic-blocks' ], cfgToMermaidUrl);
8277
}
8378
};

src/cli/repl/commands/repl-commands.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { quitCommand } from './repl-quit';
22
import { stdioCaptureProcessor, waitOnScript } from '../execute';
3-
import type { ReplCommand } from './repl-main';
3+
import type { ReplBaseCommand, ReplCodeCommand, ReplCommand } from './repl-main';
44
import { rawPrompt } from '../prompt';
55
import { versionCommand } from './repl-version';
66
import { parseCommand } from './repl-parse';
@@ -21,7 +21,7 @@ import { scripts } from '../../common/scripts-info';
2121
import { lineageCommand } from './repl-lineage';
2222
import { queryCommand, queryStarCommand } from './repl-query';
2323

24-
function printHelpForScript(script: [string, ReplCommand], f: OutputFormatter, starredVersion?: ReplCommand): string {
24+
function printHelpForScript(script: [string, ReplBaseCommand], f: OutputFormatter, starredVersion?: ReplBaseCommand): string {
2525
let base = ` ${bold(padCmd(':' + script[0] + (starredVersion ? '[*]' : '')), f)}${script[1].description}`;
2626
if(starredVersion) {
2727
base += ` (star: ${starredVersion.description})`;
@@ -49,6 +49,7 @@ function printCommandHelp(formatter: OutputFormatter) {
4949

5050
export const helpCommand: ReplCommand = {
5151
description: 'Show help information',
52+
usesAnalyzer: false,
5253
script: false,
5354
usageExample: ':help',
5455
aliases: [ 'h', '?' ],
@@ -79,7 +80,7 @@ You can combine commands by separating them with a semicolon ${bold(';',output.f
7980
/**
8081
* All commands that should be available in the REPL.
8182
*/
82-
const _commands: Record<string, ReplCommand> = {
83+
const _commands: Record<string, ReplCommand | ReplCodeCommand> = {
8384
'help': helpCommand,
8485
'quit': quitCommand,
8586
'version': versionCommand,
@@ -122,6 +123,7 @@ export function getReplCommands() {
122123
aliases: [],
123124
script: true,
124125
usageExample: `:${script} --help`,
126+
usesAnalyzer: false,
125127
fn: async({ output, remainingLine }) => {
126128
// check if the target *module* exists in the current directory, else try two dirs up, otherwise, fail with a message
127129
let path = `${__dirname}/${target}`;
@@ -177,7 +179,7 @@ function initCommandMapping() {
177179
* Get the command for a given command name or alias.
178180
* @param command - The name of the command (without the leading `:`)
179181
*/
180-
export function getCommand(command: string): ReplCommand | undefined {
182+
export function getCommand(command: string): ReplCodeCommand | ReplCommand | undefined {
181183
if(commandMapping === undefined) {
182184
initCommandMapping();
183185
}
Lines changed: 35 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,93 +1,88 @@
1-
import type { ReplCommand, ReplOutput } from './repl-main';
2-
import { createDataflowPipeline } from '../../../core/steps/pipeline/default-pipelines';
3-
import { fileProtocol, requestFromInput } from '../../../r-bridge/retriever';
1+
import type { ReplCodeCommand, ReplOutput } from './repl-main';
2+
import { fileProtocol } from '../../../r-bridge/retriever';
43
import { graphToMermaid, graphToMermaidUrl } from '../../../util/mermaid/dfg';
5-
import type { KnownParser } from '../../../r-bridge/parser';
64
import { ColorEffect, Colors, FontStyles } from '../../../util/text/ansi';
7-
import type { FlowrConfigOptions } from '../../../config';
5+
import type { PipelinePerStepMetaInformation } from '../../../core/steps/pipeline/pipeline';
6+
import { handleString } from '../core';
87

9-
/**
10-
* Obtain the dataflow graph using a known parser (such as the {@link RShell} or {@link TreeSitterExecutor}).
11-
*/
12-
async function replGetDataflow(config: FlowrConfigOptions, parser: KnownParser, code: string) {
13-
return await createDataflowPipeline(parser, {
14-
request: requestFromInput(code.trim())
15-
}, config).allRemainingSteps();
8+
function formatInfo(out: ReplOutput, type: string, meta: PipelinePerStepMetaInformation ): string {
9+
return out.formatter.format(`Copied ${type} to clipboard (dataflow: ${meta['.meta'].timing + 'ms'}).`,
10+
{ color: Colors.White, effect: ColorEffect.Foreground, style: FontStyles.Italic });
1611
}
1712

18-
function handleString(code: string): string {
19-
return code.startsWith('"') ? JSON.parse(code) as string : code;
20-
}
21-
22-
function formatInfo(out: ReplOutput, type: string, timing: number): string {
23-
return out.formatter.format(`Copied ${type} to clipboard (dataflow: ${timing}ms).`, { color: Colors.White, effect: ColorEffect.Foreground, style: FontStyles.Italic });
24-
}
25-
26-
export const dataflowCommand: ReplCommand = {
13+
export const dataflowCommand: ReplCodeCommand = {
2714
description: `Get mermaid code for the dataflow graph, start with '${fileProtocol}' to indicate a file`,
15+
usesAnalyzer: true,
2816
usageExample: ':dataflow',
2917
aliases: [ 'd', 'df' ],
3018
script: false,
31-
fn: async({ output, parser, remainingLine, config }) => {
32-
const result = await replGetDataflow(config, parser, handleString(remainingLine));
33-
const mermaid = graphToMermaid({ graph: result.dataflow.graph, includeEnvironments: false }).string;
19+
argsParser: (args: string) => handleString(args),
20+
fn: async({ output, analyzer }) => {
21+
const result = await analyzer.dataflow();
22+
const mermaid = graphToMermaid({ graph: result.graph, includeEnvironments: false }).string;
3423
output.stdout(mermaid);
3524
try {
3625
const clipboard = await import('clipboardy');
3726
clipboard.default.writeSync(mermaid);
38-
output.stdout(formatInfo(output, 'mermaid code', result.dataflow['.meta'].timing));
27+
output.stdout(formatInfo(output, 'mermaid code', result));
3928
} catch{ /* do nothing this is a service thing */ }
4029
}
4130
};
4231

43-
export const dataflowStarCommand: ReplCommand = {
32+
export const dataflowStarCommand: ReplCodeCommand = {
4433
description: 'Returns the URL to mermaid.live',
34+
usesAnalyzer: true,
4535
usageExample: ':dataflow*',
4636
aliases: [ 'd*', 'df*' ],
4737
script: false,
48-
fn: async({ output, parser, remainingLine, config }) => {
49-
const result = await replGetDataflow(config, parser, handleString(remainingLine));
50-
const mermaid = graphToMermaidUrl(result.dataflow.graph, false);
38+
argsParser: (args: string) => handleString(args),
39+
fn: async({ output, analyzer }) => {
40+
const result = await analyzer.dataflow();
41+
const mermaid = graphToMermaidUrl(result.graph, false);
5142
output.stdout(mermaid);
5243
try {
5344
const clipboard = await import('clipboardy');
5445
clipboard.default.writeSync(mermaid);
55-
output.stdout(formatInfo(output, 'mermaid url', result.dataflow['.meta'].timing));
46+
output.stdout(formatInfo(output, 'mermaid url', result));
5647
} catch{ /* do nothing this is a service thing */ }
5748
}
5849
};
5950

6051

61-
export const dataflowSimplifiedCommand: ReplCommand = {
52+
export const dataflowSimplifiedCommand: ReplCodeCommand = {
6253
description: `Get mermaid code for the simplified dataflow graph, start with '${fileProtocol}' to indicate a file`,
54+
usesAnalyzer: true,
6355
usageExample: ':dataflowsimple',
6456
aliases: [ 'ds', 'dfs' ],
6557
script: false,
66-
fn: async({ output, parser, remainingLine, config }) => {
67-
const result = await replGetDataflow(config, parser, handleString(remainingLine));
68-
const mermaid = graphToMermaid({ graph: result.dataflow.graph, includeEnvironments: false, simplified: true }).string;
58+
argsParser: (args: string) => handleString(args),
59+
fn: async({ output, analyzer }) => {
60+
const result = await analyzer.dataflow();
61+
const mermaid = graphToMermaid({ graph: result.graph, includeEnvironments: false, simplified: true }).string;
6962
output.stdout(mermaid);
7063
try {
7164
const clipboard = await import('clipboardy');
7265
clipboard.default.writeSync(mermaid);
73-
output.stdout(formatInfo(output, 'mermaid code', result.dataflow['.meta'].timing));
66+
output.stdout(formatInfo(output, 'mermaid code', result));
7467
} catch{ /* do nothing this is a service thing */ }
7568
}
7669
};
7770

78-
export const dataflowSimpleStarCommand: ReplCommand = {
71+
export const dataflowSimpleStarCommand: ReplCodeCommand = {
7972
description: 'Returns the URL to mermaid.live',
73+
usesAnalyzer: true,
8074
usageExample: ':dataflowsimple*',
8175
aliases: [ 'ds*', 'dfs*' ],
8276
script: false,
83-
fn: async({ output, parser, remainingLine, config }) => {
84-
const result = await replGetDataflow(config, parser, handleString(remainingLine));
85-
const mermaid = graphToMermaidUrl(result.dataflow.graph, false, undefined, true);
77+
argsParser: (args: string) => handleString(args),
78+
fn: async({ output, analyzer }) => {
79+
const result = await analyzer.dataflow();
80+
const mermaid = graphToMermaidUrl(result.graph, false, undefined, true);
8681
output.stdout(mermaid);
8782
try {
8883
const clipboard = await import('clipboardy');
8984
clipboard.default.writeSync(mermaid);
90-
output.stdout(formatInfo(output, 'mermaid url', result.dataflow['.meta'].timing));
85+
output.stdout(formatInfo(output, 'mermaid url', result));
9186
} catch{ /* do nothing this is a service thing */ }
9287
}
9388
};

src/cli/repl/commands/repl-execute.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ async function executeRShellCommand(output: ReplOutput, shell: RShell, statement
2727

2828
export const executeCommand: ReplCommand = {
2929
description: 'Execute the given code as R code (essentially similar to using now command). This requires the `--r-session-access` flag to be set and requires the r-shell engine.',
30+
usesAnalyzer: false,
3031
usageExample: ':execute',
3132
aliases: [ 'e', 'r' ],
3233
script: false,

src/cli/repl/commands/repl-lineage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export function getLineage(criterion: SingleSlicingCriterion, graph: DataflowGra
6767

6868
export const lineageCommand: ReplCommand = {
6969
description: 'Get the lineage of an R object',
70+
usesAnalyzer: false,
7071
usageExample: ':lineage',
7172
aliases: ['lin'],
7273
script: false,

0 commit comments

Comments
 (0)