diff --git a/src/cli/repl/commands/parse.ts b/src/cli/repl/commands/parse.ts index a851cac445..ec8e57c2cc 100644 --- a/src/cli/repl/commands/parse.ts +++ b/src/cli/repl/commands/parse.ts @@ -135,7 +135,7 @@ export const parseCommand: ReplCommand = { }).allRemainingSteps() const config = deepMergeObject(DEFAULT_XML_PARSER_CONFIG, { tokenMap: await shell.tokenMap() }) - const object = await xlm2jsonObject(config, result.parse) + const object = xlm2jsonObject(config, result.parse) output.stdout(depthListToTextTree(toDepthMap(object, config), config, output.formatter)) } diff --git a/src/core/print/parse-printer.ts b/src/core/print/parse-printer.ts index fcc1698505..95e341d210 100644 --- a/src/core/print/parse-printer.ts +++ b/src/core/print/parse-printer.ts @@ -24,8 +24,8 @@ function filterObject(obj: XmlBasedJson, keys: Set): XmlBasedJson[] | Xm } -export async function parseToQuads(code: string, config: QuadSerializationConfiguration, parseConfig: XmlParserConfig): Promise { - const obj = await xlm2jsonObject(parseConfig, code) +export function parseToQuads(code: string, config: QuadSerializationConfiguration, parseConfig: XmlParserConfig): string{ + const obj = xlm2jsonObject(parseConfig, code) // recursively filter so that if the object contains one of the keys 'a', 'b' or 'c', all other keys are ignored return serialize2quads( filterObject(obj, new Set([parseConfig.attributeName, parseConfig.childrenName, parseConfig.contentName])) as XmlBasedJson, diff --git a/src/core/slicer.ts b/src/core/slicer.ts index e652d34667..e132dd357e 100644 --- a/src/core/slicer.ts +++ b/src/core/slicer.ts @@ -204,11 +204,11 @@ export class SteppingSlicer, 'dataflow': { description: 'Construct the dataflow graph', - processor: produceDataFlowGraph, + processor: (r, a) => produceDataFlowGraph(r, a), required: 'once-per-file', printer: { [StepOutputFormat.Internal]: internalPrinter, diff --git a/src/dataflow/environments/environment.ts b/src/dataflow/environments/environment.ts index ee37ffb3f2..2d9b644885 100644 --- a/src/dataflow/environments/environment.ts +++ b/src/dataflow/environments/environment.ts @@ -147,6 +147,14 @@ export const DefaultEnvironmentMemory = new Map = { [RType.ExpressionList]: processExpressionList, } -export function produceDataFlowGraph(ast: NormalizedAst, initialScope: DataflowScopeName = LocalScope): DataflowInformation { - return processDataflowFor(ast.ast, { completeAst: ast, activeScope: initialScope, environments: initializeCleanEnvironments(), processors: processors as DataflowProcessors }) +export function produceDataFlowGraph(request: RParseRequest, ast: NormalizedAst, initialScope: DataflowScopeName = LocalScope): DataflowInformation { + return processDataflowFor(ast.ast, { + completeAst: ast, + activeScope: initialScope, + environments: initializeCleanEnvironments(), + processors: processors as DataflowProcessors, + currentRequest: request, + referenceChain: [requestFingerprint(request)] + }) } export function processBinaryOp(node: RBinaryOp, data: DataflowProcessorInformation) { diff --git a/src/dataflow/internal/process/functions/function-call.ts b/src/dataflow/internal/process/functions/function-call.ts index 9a12cae5fe..e3a9305180 100644 --- a/src/dataflow/internal/process/functions/function-call.ts +++ b/src/dataflow/internal/process/functions/function-call.ts @@ -1,14 +1,15 @@ import type { DataflowInformation } from '../../info' -import type { DataflowProcessorInformation} from '../../../processor' +import type {DataflowProcessorInformation} from '../../../processor' import { processDataflowFor } from '../../../processor' -import { define, overwriteEnvironments, resolveByName } from '../../../environments' -import type { ParentInformation, RFunctionCall} from '../../../../r-bridge' -import { RType } from '../../../../r-bridge' +import {define, overwriteEnvironments, resolveByName} from '../../../environments' +import type {ParentInformation, RFunctionCall} from '../../../../r-bridge' +import { RType} from '../../../../r-bridge' import { guard } from '../../../../util/assert' -import type { FunctionArgument } from '../../../index' +import type {FunctionArgument} from '../../../index' import { DataflowGraph, dataflowLogger, EdgeType } from '../../../index' import { linkArgumentsOnCall } from '../../linker' import { LocalScope } from '../../../environments/scopes' +import {isSourceCall, processSourceCall} from './source' export const UnnamedFunctionCallPrefix = 'unnamed-function-call-' @@ -40,7 +41,6 @@ export function processFunctionCall(functionCall: RFunctionCall(functionCall: RFunctionCall(functionCall: RFunctionCall(functionCall: RFunctionCall, data: DataflowProcessorInformation, information: DataflowInformation): DataflowInformation { + const sourceFile = functionCall.arguments[0] as RArgument | undefined + if(sourceFile?.value?.type == RType.String) { + const path = removeTokenMapQuotationMarks(sourceFile.lexeme) + const request = sourceProvider.createRequest(path) + + // check if the sourced file has already been dataflow analyzed, and if so, skip it + if(data.referenceChain.includes(requestFingerprint(request))) { + dataflowLogger.info(`Found loop in dataflow analysis for ${JSON.stringify(request)}: ${JSON.stringify(data.referenceChain)}, skipping further dataflow analysis`) + return information + } + + return sourceRequest(request, data, information, sourcedDeterministicCountingIdGenerator(path, functionCall.location)) + } else { + dataflowLogger.info(`Non-constant argument ${JSON.stringify(sourceFile)} for source is currently not supported, skipping`) + return information + } +} + +export function sourceRequest(request: RParseRequest, data: DataflowProcessorInformation, information: DataflowInformation, getId: IdGenerator): DataflowInformation { + const executor = new RShellExecutor() + + // parse, normalize and dataflow the sourced file + let normalized: NormalizedAst + let dataflow: DataflowInformation + try { + const parsed = executeSingleSubStep('parse', request, executor) as string + normalized = executeSingleSubStep('normalize', parsed, executor.getTokenMap(), undefined, getId) as NormalizedAst + dataflow = processDataflowFor(normalized.ast, { + ...data, + currentRequest: request, + environments: information.environments, + referenceChain: [...data.referenceChain, requestFingerprint(request)] + }) + } catch(e) { + dataflowLogger.warn(`Failed to analyze sourced file ${JSON.stringify(request)}, skipping: ${(e as Error).message}`) + return information + } + + // update our graph with the sourced file's information + const newInformation = {...information} + newInformation.environments = overwriteEnvironments(information.environments, dataflow.environments) + newInformation.graph.mergeWith(dataflow.graph) + // this can be improved, see issue #628 + for(const [k, v] of normalized.idMap) { + data.completeAst.idMap.set(k, v) + } + return newInformation +} diff --git a/src/dataflow/processor.ts b/src/dataflow/processor.ts index 9e94a06588..8e9497129b 100644 --- a/src/dataflow/processor.ts +++ b/src/dataflow/processor.ts @@ -4,7 +4,7 @@ import type { NormalizedAst, ParentInformation, RNode, - RNodeWithParent + RNodeWithParent, RParseRequest } from '../r-bridge' import type { DataflowInformation } from './internal/info' import type { DataflowScopeName, REnvironmentInformation } from './environments' @@ -13,20 +13,29 @@ export interface DataflowProcessorInformation { /** * Initial and frozen ast-information */ - readonly completeAst: NormalizedAst + readonly completeAst: NormalizedAst /** * Correctly contains pushed local scopes introduced by `function` scopes. * Will by default *not* contain any symbol-bindings introduces along the way, they have to be decorated when moving up the tree. */ - readonly environments: REnvironmentInformation + readonly environments: REnvironmentInformation /** * Name of the currently active scope, (hopefully) always {@link LocalScope | Local} */ - readonly activeScope: DataflowScopeName + readonly activeScope: DataflowScopeName /** * Other processors to be called by the given functions */ - readonly processors: DataflowProcessors + readonly processors: DataflowProcessors + /** + * The {@link RParseRequest} that is currently being parsed + */ + readonly currentRequest: RParseRequest + /** + * The chain of {@link RParseRequest} fingerprints ({@link requestFingerprint}) that lead to the {@link currentRequest}. + * The most recent (last) entry is expected to always be the {@link currentRequest}. + */ + readonly referenceChain: string[] } export type DataflowProcessor> = (node: NodeType, data: DataflowProcessorInformation) => DataflowInformation @@ -55,6 +64,3 @@ export type DataflowProcessors = { export function processDataflowFor(current: RNodeWithParent, data: DataflowProcessorInformation): DataflowInformation { return data.processors[current.type](current as never, data) } - - - diff --git a/src/r-bridge/lang-4.x/ast/model/processing/decorate.ts b/src/r-bridge/lang-4.x/ast/model/processing/decorate.ts index 9e0a9cb4c0..284ea7ee42 100644 --- a/src/r-bridge/lang-4.x/ast/model/processing/decorate.ts +++ b/src/r-bridge/lang-4.x/ast/model/processing/decorate.ts @@ -46,6 +46,11 @@ export function deterministicCountingIdGenerator(start = 0): () => NodeId { return () => `${id++}` } +export function sourcedDeterministicCountingIdGenerator(path: string, location: SourceRange, start = 0): () => NodeId { + let id = start + return () => `${path}-${loc2Id(location)}-${id++}` +} + function loc2Id(loc: SourceRange) { return `${loc.start.line}:${loc.start.column}-${loc.end.line}:${loc.end.column}` } diff --git a/src/r-bridge/lang-4.x/ast/parser/xml/internal/xml-to-json.ts b/src/r-bridge/lang-4.x/ast/parser/xml/internal/xml-to-json.ts index 9f3ce31c38..6a0a910770 100644 --- a/src/r-bridge/lang-4.x/ast/parser/xml/internal/xml-to-json.ts +++ b/src/r-bridge/lang-4.x/ast/parser/xml/internal/xml-to-json.ts @@ -8,8 +8,11 @@ import type { XmlBasedJson } from '../input-format' * @param config - The configuration to use (i.e., what names should be used for the attributes, children, ...) * @param xmlString - The xml input to parse */ -export function xlm2jsonObject(config: XmlParserConfig, xmlString: string): Promise { - return xml2js.parseStringPromise(xmlString, { +export function xlm2jsonObject(config: XmlParserConfig, xmlString: string): XmlBasedJson { + let result: XmlBasedJson = {} + xml2js.parseString(xmlString, { + // we want this to be strictly synchronous so that the result can be returned immediately below! + async: false, attrkey: config.attributeName, charkey: config.contentName, childkey: config.childrenName, @@ -22,5 +25,6 @@ export function xlm2jsonObject(config: XmlParserConfig, xmlString: string): Prom includeWhiteChars: true, normalize: false, strict: true - }) as Promise + }, (_, r)=> result = r as XmlBasedJson) + return result } diff --git a/src/r-bridge/lang-4.x/ast/parser/xml/parser.ts b/src/r-bridge/lang-4.x/ast/parser/xml/parser.ts index 1f068b650a..9ae30443dd 100644 --- a/src/r-bridge/lang-4.x/ast/parser/xml/parser.ts +++ b/src/r-bridge/lang-4.x/ast/parser/xml/parser.ts @@ -30,12 +30,12 @@ export const parseLog = log.getSubLogger({ name: 'ast-parser' }) * * @returns The normalized and decorated AST (i.e., as a doubly linked tree) */ -export async function normalize(xmlString: string, tokenMap: TokenMap, hooks?: DeepPartial, getId: IdGenerator = deterministicCountingIdGenerator(0)): Promise { +export function normalize(xmlString: string, tokenMap: TokenMap, hooks?: DeepPartial, getId: IdGenerator = deterministicCountingIdGenerator(0)): NormalizedAst { const config = { ...DEFAULT_XML_PARSER_CONFIG, tokenMap } const hooksWithDefaults = deepMergeObject(DEFAULT_PARSER_HOOKS, hooks) as XmlParserHooks const data: ParserData = { config, hooks: hooksWithDefaults, currentRange: undefined, currentLexeme: undefined } - const object = await xlm2jsonObject(config, xmlString) + const object = xlm2jsonObject(config, xmlString) return decorateAst(parseRootObjToAst(data, object), getId) } diff --git a/src/r-bridge/retriever.ts b/src/r-bridge/retriever.ts index 6ae8a939bf..b65b92e7d1 100644 --- a/src/r-bridge/retriever.ts +++ b/src/r-bridge/retriever.ts @@ -2,8 +2,9 @@ import { type RShell } from './shell' import type { XmlParserHooks, NormalizedAst } from './lang-4.x' import { ts2r, normalize } from './lang-4.x' import { startAndEndsWith } from '../util/strings' -import type { DeepPartial, DeepReadonly } from 'ts-essentials' +import type {AsyncOrSync, DeepPartial, DeepReadonly} from 'ts-essentials' import { guard } from '../util/assert' +import {RShellExecutor} from './shell-executor' export interface RParseRequestFromFile { request: 'file'; @@ -25,6 +26,13 @@ interface RParseRequestBase { ensurePackageInstalled: boolean } +/** + * A provider for an {@link RParseRequest} that can be used, for example, to override source file parsing behavior in tests + */ +export interface RParseRequestProvider { + createRequest(path: string): RParseRequest +} + /** * A request that can be passed along to {@link retrieveXmlFromRCode}. */ @@ -45,6 +53,34 @@ export function requestFromInput(input: `file://${string}` | string): RParseRequ } } +export function requestProviderFromFile(): RParseRequestProvider { + return { + createRequest(path: string): RParseRequest { + return { + request: 'file', + content: path, + ensurePackageInstalled: false} + } + } +} + +export function requestProviderFromText(text: {[path: string]: string}): RParseRequestProvider{ + return { + createRequest(path: string): RParseRequest { + return { + request: 'text', + content: text[path], + ensurePackageInstalled: false + } + } + } +} + +export function requestFingerprint(request: RParseRequest): string { + // eventually we should do this properly, like using a hashing function etc. + return JSON.stringify(request) +} + const ErrorMarker = 'err' /** @@ -54,22 +90,37 @@ const ErrorMarker = 'err' * Throws if the file could not be parsed. * If successful, allows to further query the last result with {@link retrieveNumberOfRTokensOfLastParse}. */ -export async function retrieveXmlFromRCode(request: RParseRequest, shell: RShell): Promise { - if(request.ensurePackageInstalled) { - await shell.ensurePackageInstalled('xmlparsedata', true) - } - +export function retrieveXmlFromRCode(request: RParseRequest, shell: (RShell | RShellExecutor)): AsyncOrSync { const suffix = request.request === 'file' ? ', encoding="utf-8"' : '' - - shell.sendCommands(`flowr_output <- flowr_parsed <- "${ErrorMarker}"`, + const setupCommands = [ + `flowr_output <- flowr_parsed <- "${ErrorMarker}"`, // now, try to retrieve the ast `try(flowr_parsed<-parse(${request.request}=${JSON.stringify(request.content)},keep.source=TRUE${suffix}),silent=FALSE)`, - 'try(flowr_output<-xmlparsedata::xml_parse_data(flowr_parsed,includeText=TRUE,pretty=FALSE),silent=FALSE)' - ) - const xml = await shell.sendCommandWithOutput(`cat(flowr_output,${ts2r(shell.options.eol)})`) - const output = xml.join(shell.options.eol) - guard(output !== ErrorMarker, () => `unable to parse R code (see the log for more information) for request ${JSON.stringify(request)}}`) - return output + 'try(flowr_output<-xmlparsedata::xml_parse_data(flowr_parsed,includeText=TRUE,pretty=FALSE),silent=FALSE)', + ] + const outputCommand = `cat(flowr_output,${ts2r(shell.options.eol)})` + + if(shell instanceof RShellExecutor){ + if(request.ensurePackageInstalled) + shell.ensurePackageInstalled('xmlparsedata',true) + + shell.addPrerequisites(setupCommands) + return guardOutput(shell.run(outputCommand)) + } else { + const run = async() => { + if(request.ensurePackageInstalled) + await shell.ensurePackageInstalled('xmlparsedata', true) + + shell.sendCommands(...setupCommands) + return guardOutput((await shell.sendCommandWithOutput(outputCommand)).join(shell.options.eol)) + } + return run() + } + + function guardOutput(output: string): string { + guard(output !== ErrorMarker, () => `unable to parse R code (see the log for more information) for request ${JSON.stringify(request)}}`) + return output + } } /** @@ -78,7 +129,7 @@ export async function retrieveXmlFromRCode(request: RParseRequest, shell: RShell */ export async function retrieveNormalizedAstFromRCode(request: RParseRequest, shell: RShell, hooks?: DeepPartial): Promise { const xml = await retrieveXmlFromRCode(request, shell) - return await normalize(xml, await shell.tokenMap(), hooks) + return normalize(xml, await shell.tokenMap(), hooks) } /** diff --git a/test/functionality/dataflow/processing-of-elements/functions/source-tests.ts b/test/functionality/dataflow/processing-of-elements/functions/source-tests.ts new file mode 100644 index 0000000000..40c8b390f4 --- /dev/null +++ b/test/functionality/dataflow/processing-of-elements/functions/source-tests.ts @@ -0,0 +1,253 @@ +import {assertDataflow, withShell} from '../../../_helper/shell' +import {setSourceProvider} from '../../../../../src/dataflow/internal/process/functions/source' +import {BuiltIn, DataflowGraph, EdgeType, initializeCleanEnvironments, requestProviderFromFile, requestProviderFromText, sourcedDeterministicCountingIdGenerator} from '../../../../../src' +import {LocalScope} from '../../../../../src/dataflow/environments/scopes' +import {UnnamedArgumentPrefix} from '../../../../../src/dataflow/internal/process/functions/argument' +import {define} from '../../../../../src/dataflow/environments' + +describe('source', withShell(shell => { + // reset the source provider back to the default value after our tests + after(() => setSourceProvider(requestProviderFromFile())) + + const sources = { + simple: 'N <- 9', + recursive1: 'x <- 1\nsource("recursive2")', + recursive2: 'cat(x)\nsource("recursive1")' + } + setSourceProvider(requestProviderFromText(sources)) + + const envWithSimpleN = define( + {nodeId: 'simple-1:1-1:6-0', scope: 'local', name: 'N', used: 'always', kind: 'variable', definedAt: 'simple-1:1-1:6-2' }, + LocalScope, + initializeCleanEnvironments() + ) + assertDataflow('simple source', shell, 'source("simple")\ncat(N)', new DataflowGraph() + .addVertex({ tag: 'variable-definition', id: 'simple-1:1-1:6-0', name: 'N', scope: LocalScope }) + .addVertex({ + tag: 'function-call', + name: 'source', + id: '3', + environment: initializeCleanEnvironments(), + args: [{ + nodeId: '2', name: `${UnnamedArgumentPrefix}2`, scope: LocalScope, used: 'always' } + ] + }) + .addVertex({ + tag: 'function-call', + name: 'cat', + id: '7', + environment: envWithSimpleN, + args: [{ + nodeId: '6', name: `${UnnamedArgumentPrefix}6`, scope: LocalScope, used: 'always' + }] + }) + .addVertex({tag: 'use', id: '5', name: 'N', environment: envWithSimpleN}) + .addVertex({tag: 'use', id: '2', name: `${UnnamedArgumentPrefix}2`}) + .addVertex({tag: 'use', id: '6', name: `${UnnamedArgumentPrefix}6`, environment: envWithSimpleN}) + .addEdge('3', '2', EdgeType.Argument, 'always') + .addEdge('3', BuiltIn, EdgeType.Reads, 'always') + .addEdge('5', 'simple-1:1-1:6-0', EdgeType.Reads, 'always') + .addEdge('6', '5', EdgeType.Reads, 'always') + .addEdge('7', '6', EdgeType.Argument, 'always') + .addEdge('7', BuiltIn, EdgeType.Reads, 'always') + ) + + assertDataflow('multiple source', shell, 'source("simple")\nN <- 0\nsource("simple")\ncat(N)', new DataflowGraph() + .addVertex({ + tag: 'function-call', + name: 'source', + id: '3', + environment: initializeCleanEnvironments(), + args: [{ + nodeId: '2', name: `${UnnamedArgumentPrefix}2`, scope: LocalScope, used: 'always' } + ], + when: 'always' + }) + .addVertex({ + tag: 'function-call', + name: 'source', + id: '10', + environment: define({nodeId: '4', scope: 'local', name: 'N', used: 'always', kind: 'variable', definedAt: '6' }, LocalScope, initializeCleanEnvironments()), + args: [{ + nodeId: '9', name: `${UnnamedArgumentPrefix}9`, scope: LocalScope, used: 'always' } + ], + when: 'always' + }) + .addVertex({ + tag: 'function-call', + name: 'cat', + id: '14', + environment: define({nodeId: 'simple-3:1-3:6-0', scope: 'local', name: 'N', used: 'always', kind: 'variable', definedAt: 'simple-3:1-3:6-2' }, LocalScope, initializeCleanEnvironments()), + args: [{ + nodeId: '13', name: `${UnnamedArgumentPrefix}13`, scope: LocalScope, used: 'always' } + ], + when: 'always' + }) + .addVertex({ + tag: 'variable-definition', + id: 'simple-3:1-3:6-0', + name: 'N', + scope: LocalScope, + environment: define({nodeId: '4', scope: 'local', name: 'N', used: 'always', kind: 'variable', definedAt: '6' }, LocalScope, initializeCleanEnvironments()) + }) + .addVertex({ tag: 'variable-definition', id: 'simple-1:1-1:6-0', name: 'N', scope: LocalScope }) + .addVertex({ tag: 'variable-definition', id: '4', name: 'N', scope: LocalScope, environment: envWithSimpleN }) + .addVertex({tag: 'use', id: '2', name: `${UnnamedArgumentPrefix}2` }) + .addVertex({ + tag: 'use', + id: '9', + name: `${UnnamedArgumentPrefix}9`, + environment: define({nodeId: '4', scope: 'local', name: 'N', used: 'always', kind: 'variable', definedAt: '6' }, LocalScope, initializeCleanEnvironments()) + }) + .addVertex({ + tag: 'use', + id: '13', + name: `${UnnamedArgumentPrefix}13`, + environment: define({nodeId: 'simple-3:1-3:6-0', scope: 'local', name: 'N', used: 'always', kind: 'variable', definedAt: 'simple-3:1-3:6-2' }, LocalScope, initializeCleanEnvironments()) + }) + .addVertex({ + tag: 'use', + id: '12', + name: 'N', + environment: define({nodeId: 'simple-3:1-3:6-0', scope: 'local', name: 'N', used: 'always', kind: 'variable', definedAt: 'simple-3:1-3:6-2' }, LocalScope, initializeCleanEnvironments()) + }) + .addEdge('3', '10', EdgeType.SameReadRead, 'always') + .addEdge('3', '2', EdgeType.Argument, 'always') + .addEdge('14', '13', EdgeType.Argument, 'always') + .addEdge('10', '9', EdgeType.Argument, 'always') + .addEdge('3', BuiltIn, EdgeType.Reads, 'always') + .addEdge('10', BuiltIn, EdgeType.Reads, 'always') + .addEdge('14', BuiltIn, EdgeType.Reads, 'always') + .addEdge('13', '12', EdgeType.Reads, 'always') + .addEdge('12', 'simple-3:1-3:6-0', EdgeType.Reads, 'always') + .addEdge('simple-3:1-3:6-0', '4', EdgeType.SameDefDef, 'always') + .addEdge('4', 'simple-1:1-1:6-0', EdgeType.SameDefDef, 'always') + ) + + const envWithConditionalN = define( + {nodeId: 'simple-1:10-1:15-0', scope: 'local', name: 'N', used: 'always', kind: 'variable', definedAt: 'simple-1:10-1:15-2' }, + LocalScope, + initializeCleanEnvironments() + ) + assertDataflow('conditional', shell, 'if (x) { source("simple") }\ncat(N)', new DataflowGraph() + .addVertex({ tag: 'variable-definition', id: 'simple-1:10-1:15-0', name: 'N', scope: LocalScope }) + .addVertex({ + tag: 'function-call', + name: 'source', + id: '4', + environment: initializeCleanEnvironments(), + args: [{ + nodeId: '3', name: `${UnnamedArgumentPrefix}3`, scope: LocalScope, used: 'always' } + ], + when: 'maybe' + }) + .addVertex({ + tag: 'function-call', + name: 'cat', + id: '10', + environment: envWithConditionalN, + args: [{ + nodeId: '9', name: `${UnnamedArgumentPrefix}9`, scope: LocalScope, used: 'always' + }] + }) + .addVertex({tag: 'use', id: '0', name: 'x', scope: LocalScope}) + .addVertex({tag: 'use', id: '8', name: 'N', environment: envWithConditionalN}) + .addVertex({tag: 'use', id: '3', name: `${UnnamedArgumentPrefix}3`}) + .addVertex({tag: 'use', id: '9', name: `${UnnamedArgumentPrefix}9`, environment: envWithConditionalN}) + .addEdge('4', '3', EdgeType.Argument, 'always') + .addEdge('4', BuiltIn, EdgeType.Reads, 'maybe') + .addEdge('8', 'simple-1:10-1:15-0', EdgeType.Reads, 'always') + .addEdge('9', '8', EdgeType.Reads, 'always') + .addEdge('10', '9', EdgeType.Argument, 'always') + .addEdge('10', BuiltIn, EdgeType.Reads, 'always') + ) + + // missing sources should just be ignored + assertDataflow('missing source', shell, 'source("missing")', new DataflowGraph() + .addVertex({ + tag: 'function-call', + name: 'source', + id: '3', + environment: initializeCleanEnvironments(), + args: [{ + nodeId: '2', name: `${UnnamedArgumentPrefix}2`, scope: LocalScope, used: 'always' + }] + }) + .addVertex({tag: 'use', id: '2', name: `${UnnamedArgumentPrefix}2`}) + .addEdge('3', '2', EdgeType.Argument, 'always') + .addEdge('3', BuiltIn, EdgeType.Reads, 'always') + ) + + const recursive2Id = (id: number) => sourcedDeterministicCountingIdGenerator('recursive2', {start: {line: 2, column: 1}, end: {line: 2, column: 6}}, id)() + const envWithX = define( + {nodeId: '0', scope: 'local', name: 'x', used: 'always', kind: 'variable', definedAt: '2' }, + LocalScope, + initializeCleanEnvironments() + ) + assertDataflow('recursive source', shell, sources.recursive1, new DataflowGraph() + .addVertex({ + tag: 'function-call', + name: 'source', + id: '6', + environment: envWithX, + args: [{ + nodeId: '5', name: `${UnnamedArgumentPrefix}5`, scope: LocalScope, used: 'always' } + ], + when: 'always' + }) + .addVertex({ + tag: 'function-call', + name: 'source', + id: recursive2Id(7), + environment: envWithX, + args: [{ + nodeId: recursive2Id(6), name: `${UnnamedArgumentPrefix}${recursive2Id(6)}`, scope: LocalScope, used: 'always' } + ], + when: 'always' + }) + .addVertex({ + tag: 'function-call', + name: 'cat', + id: recursive2Id(3), + environment: envWithX, + args: [{ + nodeId: recursive2Id(2), name: `${UnnamedArgumentPrefix}${recursive2Id(2)}`, scope: LocalScope, used: 'always' } + ], + when: 'always' + }) + .addVertex({ tag: 'variable-definition', id: '0', name: 'x', scope: LocalScope }) + .addVertex({tag: 'use', id: '5', name: `${UnnamedArgumentPrefix}5`, environment: envWithX }) + .addVertex({tag: 'use', id: recursive2Id(6), name: `${UnnamedArgumentPrefix}${recursive2Id(6)}`, environment: envWithX }) + .addVertex({tag: 'use', id: recursive2Id(2), name: `${UnnamedArgumentPrefix}${recursive2Id(2)}`, environment: envWithX }) + .addVertex({tag: 'use', id: recursive2Id(1), name: 'x', environment: envWithX }) + .addEdge('6', '5', EdgeType.Argument, 'always') + .addEdge('6', BuiltIn, EdgeType.Reads, 'always') + .addEdge(recursive2Id(3), BuiltIn, EdgeType.Reads, 'always') + .addEdge(recursive2Id(3), recursive2Id(2), EdgeType.Argument, 'always') + .addEdge(recursive2Id(2), recursive2Id(1), EdgeType.Reads, 'always') + .addEdge(recursive2Id(1), '0', EdgeType.Reads, 'always') + .addEdge(recursive2Id(7), recursive2Id(6), EdgeType.Argument, 'always') + .addEdge(recursive2Id(7), BuiltIn, EdgeType.Reads, 'always') + ) + + // we currently don't support (and ignore) source calls with non-constant arguments! + assertDataflow('non-constant source', shell, 'x <- "recursive1"\nsource(x)', new DataflowGraph() + .addVertex({ + tag: 'function-call', + name: 'source', + id: '6', + environment: envWithX, + args: [{ + nodeId: '5', name: `${UnnamedArgumentPrefix}5`, scope: LocalScope, used: 'always' } + ], + when: 'always' + }) + .addVertex({ tag: 'variable-definition', id: '0', name: 'x', scope: LocalScope }) + .addVertex({tag: 'use', id: '5', name: `${UnnamedArgumentPrefix}5`, environment: envWithX }) + .addVertex({tag: 'use', id: '4', name: 'x', environment: envWithX }) + .addEdge('6', '5', EdgeType.Argument, 'always') + .addEdge('6', BuiltIn, EdgeType.Reads, 'always') + .addEdge('5', '4', EdgeType.Reads, 'always') + .addEdge('4', '0', EdgeType.Reads, 'always') + ) +})) diff --git a/test/functionality/r-bridge/lang/ast/parse-values.ts b/test/functionality/r-bridge/lang/ast/parse-values.ts index 0ad6f4a951..52ec27fc43 100644 --- a/test/functionality/r-bridge/lang/ast/parse-values.ts +++ b/test/functionality/r-bridge/lang/ast/parse-values.ts @@ -29,7 +29,7 @@ describe('Constant Parsing', request: 'text', content: '{', ensurePackageInstalled: true - }, shell)) + }, shell) as Promise) ) describe('numbers', () => { for(const number of RNumberPool) {