diff --git a/src/linter/rules/absolute-path.ts b/src/linter/rules/absolute-path.ts index 71c7ee2f9c7..13f6280ac1c 100644 --- a/src/linter/rules/absolute-path.ts +++ b/src/linter/rules/absolute-path.ts @@ -136,15 +136,15 @@ export const ABSOLUTE_PATH = { } return q.unique(); }, - processSearchResult: (elements, config, data): { results: AbsoluteFilePathResult[], '.meta': AbsoluteFilePathMetadata } => { + processSearchResult: async(elements, config, data): Promise<{ results: AbsoluteFilePathResult[], '.meta': AbsoluteFilePathMetadata }> => { const metadata: AbsoluteFilePathMetadata = { totalConsidered: 0, totalUnknown: 0 }; - const queryResults = elements.enrichmentContent(Enrichment.QueryData)?.queries; + const queryResults = (await elements.enrichmentContent(Enrichment.QueryData))?.queries; const regex = config.absolutePathRegex ? new RegExp(config.absolutePathRegex) : undefined; return { - results: elements.getElements().flatMap(element => { + results: (await Promise.all(elements.getElements().map(async element => { metadata.totalConsidered++; const node = element.node; const wd = inferWd(node.info.file, config.useAsWd); @@ -159,43 +159,46 @@ export const ABSOLUTE_PATH = { } else { return []; } - } else if(enrichmentContent(element, Enrichment.QueryData)) { - const result = queryResults[enrichmentContent(element, Enrichment.QueryData).query] as QueryResults<'dependencies'>['dependencies']; - const mappedStrings = result.read.filter(r => r.value !== undefined && r.value !== Unknown && isAbsolutePath(r.value, regex)).map(r => { - const elem = data.normalize.idMap.get(r.nodeId); - return { - certainty: LintingResultCertainty.Certain, - filePath: r.value, - loc: elem ? SourceLocation.fromNode(elem) ?? SourceLocation.invalid() : SourceLocation.invalid(), - quickFix: buildQuickFix(elem, r.value as string, wd) - }; - }); - if(mappedStrings.length > 0) { - return mappedStrings; - } else if(result.read.every(r => r.value !== Unknown)) { - // if we have no absolute paths, but all paths are known, we can return an empty array - return []; - } } else { - const dfNode = data.dataflow.graph.getVertex(node.info.id); - if(isFunctionCallVertex(dfNode)) { - const handler = dfNode.name ? PathFunctions.get(dfNode.name) : undefined; - const strings = handler ? handler(data.dataflow.graph, dfNode, data.analyzer.inspectContext()) : []; - if(strings) { - return strings.filter(s => isAbsolutePath(s, regex)).map(str => ({ - certainty: LintingResultCertainty.Uncertain, - filePath: str, - loc: SourceLocation.fromNode(element.node) ?? SourceLocation.invalid(), - quickFix: undefined - })); + const queryData = await enrichmentContent(element, Enrichment.QueryData); + if(queryData) { + const result = queryResults[queryData.query] as QueryResults<'dependencies'>['dependencies']; + const mappedStrings = result.read.filter(r => r.value !== undefined && r.value !== Unknown && isAbsolutePath(r.value, regex)).map(r => { + const elem = data.normalize.idMap.get(r.nodeId); + return { + certainty: LintingResultCertainty.Certain, + filePath: r.value, + loc: elem ? SourceLocation.fromNode(elem) ?? SourceLocation.invalid() : SourceLocation.invalid(), + quickFix: buildQuickFix(elem, r.value as string, wd) + }; + }); + if(mappedStrings.length > 0) { + return mappedStrings; + } else if(result.read.every(r => r.value !== Unknown)) { + // if we have no absolute paths, but all paths are known, we can return an empty array + return []; + } + } else { + const dfNode = data.dataflow.graph.getVertex(node.info.id); + if(isFunctionCallVertex(dfNode)) { + const handler = dfNode.name ? PathFunctions.get(dfNode.name) : undefined; + const strings = handler ? handler(data.dataflow.graph, dfNode, data.analyzer.inspectContext()) : []; + if(strings) { + return strings.filter(s => isAbsolutePath(s, regex)).map(str => ({ + certainty: LintingResultCertainty.Uncertain, + filePath: str, + loc: SourceLocation.fromNode(element.node) ?? SourceLocation.invalid(), + quickFix: undefined + })); + } } + // check whether the df node is a function call that returns a file path } - // check whether the df node is a function call that returns a file path } metadata.totalUnknown++; return undefined; - }).filter(isNotUndefined).map(r => compactRecord(r) as AbsoluteFilePathResult), + }))).flat().filter(isNotUndefined).map(r => compactRecord(r) as AbsoluteFilePathResult), '.meta': metadata }; }, diff --git a/src/linter/rules/dead-code.ts b/src/linter/rules/dead-code.ts index 2fdec54323e..dfd9db4f913 100644 --- a/src/linter/rules/dead-code.ts +++ b/src/linter/rules/dead-code.ts @@ -36,23 +36,26 @@ export const DEAD_CODE = { checkReachable: true, simplificationPasses: config.simplificationPasses ?? [...DefaultCfgSimplificationOrder, 'analyze-dead-code'] }), - processSearchResult: (elements, _config, _data) => { + processSearchResult: async(elements, _config, _data) => { const meta: DeadCodeMetadata = { consideredNodes: 0 }; return { results: combineResults( - elements.getElements() - .filter(element => { + (await Promise.all(elements.getElements() + .map(async element => { meta.consideredNodes++; - const cfgInformation = enrichmentContent(element, Enrichment.CfgInformation); - return element.node.info.role !== RoleInParent.ExpressionListGrouping && !cfgInformation.isReachable; - }) - .map(element => ({ - certainty: LintingResultCertainty.Certain, - involvedId: element.node.info.id, - loc: SourceLocation.fromNode(element.node) - })) + const cfgInformation = await enrichmentContent(element, Enrichment.CfgInformation); + if(element.node.info.role !== RoleInParent.ExpressionListGrouping && !cfgInformation.isReachable){ + return [({ + certainty: LintingResultCertainty.Certain, + involvedId: element.node.info.id, + loc: SourceLocation.fromNode(element.node) + })]; + } else { + return []; + } + }))).flat() .filter(element => isNotUndefined(element.loc)) as Writable[] ), '.meta': meta diff --git a/src/linter/rules/file-path-validity.ts b/src/linter/rules/file-path-validity.ts index b6ad8500669..b6a38be40ae 100644 --- a/src/linter/rules/file-path-validity.ts +++ b/src/linter/rules/file-path-validity.ts @@ -46,15 +46,15 @@ export const FILE_PATH_VALIDITY = { readFunctions: config.additionalReadFunctions, writeFunctions: config.additionalWriteFunctions }).with(Enrichment.CfgInformation), - processSearchResult: (elements, config, data): { results: FilePathValidityResult[], '.meta': FilePathValidityMetadata } => { - const cfg = elements.enrichmentContent(Enrichment.CfgInformation).cfg.graph; + processSearchResult: async(elements, config, data): Promise<{ results: FilePathValidityResult[], '.meta': FilePathValidityMetadata }> => { + const cfg = (await elements.enrichmentContent(Enrichment.CfgInformation)).cfg.graph; const metadata: FilePathValidityMetadata = { totalReads: 0, totalUnknown: 0, totalWritesBeforeAlways: 0, totalValid: 0 }; - const results = elements.enrichmentContent(Enrichment.QueryData).queries['dependencies']; + const results = (await elements.enrichmentContent(Enrichment.QueryData)).queries['dependencies']; return { results: elements.getElements().flatMap(element => { const matchingRead = results.read.find(r => r.nodeId === element.node.info.id); diff --git a/src/linter/rules/function-finder-util.ts b/src/linter/rules/function-finder-util.ts index ab6ce3e9e44..996719f5396 100644 --- a/src/linter/rules/function-finder-util.ts +++ b/src/linter/rules/function-finder-util.ts @@ -51,7 +51,7 @@ export const functionFinderUtil = { }) ); }, - processSearchResult: []>( + processSearchResult: async []>( elements: FlowrSearchElements, _config: unknown, _data: unknown, @@ -62,10 +62,10 @@ export const functionFinderUtil = { totalFunctionDefinitions: 0 }; - const results = refineSearch(elements.getElements()) - .flatMap(element => { + const results = (await Promise.all(refineSearch(elements.getElements()) + .map(async element => { metadata.totalCalls++; - return enrichmentContent(element, Enrichment.CallTargets).targets.map(target => { + return (await enrichmentContent(element, Enrichment.CallTargets)).targets.map(target => { metadata.totalFunctionDefinitions++; return { node: element.node, @@ -74,7 +74,7 @@ export const functionFinderUtil = { certainty: element.certainty }; }); - }); + }))).flat(); return { results: @@ -127,5 +127,3 @@ export const functionFinderUtil = { return Ternary.Never; } }; - - diff --git a/src/linter/rules/seeded-randomness.ts b/src/linter/rules/seeded-randomness.ts index 60e063715a1..78330bdc8d8 100644 --- a/src/linter/rules/seeded-randomness.ts +++ b/src/linter/rules/seeded-randomness.ts @@ -70,7 +70,7 @@ export const SEEDED_RANDOMNESS = { { callName: config.randomnessProducers.filter(p => p.type === 'function').map(p => p.name) }, { callName: getDefaultAssignments().flatMap(b => b.names).map(Identifier.getName), cascadeIf: () => CascadeAction.Continue } ]), - processSearchResult: (elements, config, { dataflow, analyzer }) => { + processSearchResult: async(elements, config, { dataflow, analyzer }) => { const assignmentProducers = new Set(config.randomnessProducers.filter(p => p.type === 'assignment').map(p => p.name)); const assignmentArgIndexes = new Map(getDefaultAssignments().flatMap(a => a.names.map(n => ([Identifier.getName(n), a.config?.swapSourceAndTarget ? 1 : 0])))); const metadata: SeededRandomnessMeta = { @@ -81,22 +81,25 @@ export const SEEDED_RANDOMNESS = { callsWithOtherBranchProducers: 0 }; return { - results: elements.getElements() + results: (await Promise.all((await Promise.all(elements.getElements() // map and filter consumers - .flatMap(element => enrichmentContent(element, Enrichment.CallTargets).targets.map(target => { - metadata.consumerCalls++; - return { - involvedId: element.node.info.id, - loc: SourceLocation.fromNode(element.node) ?? SourceLocation.invalid(), - target: target as BrandedIdentifier, - searchElement: element - }; - })) + .map(async element => { + const content = await enrichmentContent(element, Enrichment.CallTargets); + return content.targets.map(target => { + metadata.consumerCalls++; + return { + involvedId: element.node.info.id, + loc: SourceLocation.fromNode(element.node) ?? SourceLocation.invalid(), + target: target as BrandedIdentifier, + searchElement: element + }; + }); + }))).flat() // filter by calls that aren't preceded by a randomness producer - .flatMap(element => { + .map(async element => { const dfgElement = dataflow.graph.getVertex(element.searchElement.node.info.id); const cds = dfgElement ? new Set(dfgElement.cds) : new Set(); - const producers = enrichmentContent(element.searchElement, Enrichment.LastCall).linkedIds + const producers = (await enrichmentContent(element.searchElement, Enrichment.LastCall)).linkedIds .map(e => dataflow.graph.getVertex(e.node.info.id) as DataflowGraphVertexFunctionCall); const { assignment, func } = Object.groupBy(producers, f => assignmentArgIndexes.has(Identifier.getName(f.name)) ? 'assignment' : 'func'); let nonConstant = false; @@ -155,7 +158,7 @@ export const SEEDED_RANDOMNESS = { function: element.target, loc: element.loc }]; - }), + }))).flat(), '.meta': metadata }; }, diff --git a/src/search/flowr-search-filters.ts b/src/search/flowr-search-filters.ts index e163acad3f5..47ee1fd5e0d 100644 --- a/src/search/flowr-search-filters.ts +++ b/src/search/flowr-search-filters.ts @@ -7,6 +7,7 @@ import { Enrichment, enrichmentContent } from './search-executor/search-enricher import type { DataflowInformation } from '../dataflow/info'; import { Identifier } from '../dataflow/environments/identifier'; import type { BuiltInProcName } from '../dataflow/environments/built-in-proc-name'; +import type { AsyncOrSync } from 'ts-essentials'; export type FlowrFilterName = keyof typeof FlowrFilters; interface FlowrFilterWithArgs> { @@ -32,7 +33,7 @@ export enum FlowrFilter { */ OriginKind = 'origin-kind' } -export type FlowrFilterFunction = (e: FlowrSearchElement, args: T, data: { dataflow: DataflowInformation }) => boolean; +export type FlowrFilterFunction = (e: FlowrSearchElement, args: T, data: { dataflow: DataflowInformation }) => AsyncOrSync; export const ValidFlowrFilters: Set = new Set(Object.values(FlowrFilter)); export const ValidFlowrFiltersReverse = Object.fromEntries(Object.entries(FlowrFilter).map(([k, v]) => [v, k])); @@ -41,9 +42,9 @@ export const FlowrFilters = { [FlowrFilter.DropEmptyArguments]: ((e: FlowrSearchElement, _args: never) => { return e.node.type !== RType.Argument || e.node.name !== undefined; }) satisfies FlowrFilterFunction, - [FlowrFilter.MatchesEnrichment]: ((e: FlowrSearchElement, args: MatchesEnrichmentArgs) => { + [FlowrFilter.MatchesEnrichment]: (async(e: FlowrSearchElement, args: MatchesEnrichmentArgs) => { if(args.enrichment === Enrichment.CallTargets) { - const c: CallTargetsContent = enrichmentContent(e, Enrichment.CallTargets); + const c: CallTargetsContent = await enrichmentContent(e, Enrichment.CallTargets); if(c === undefined || c.targets === undefined) { return false; } @@ -57,7 +58,7 @@ export const FlowrFilters = { } return false; } else { - const content = JSON.stringify(enrichmentContent(e, args.enrichment)); + const content = JSON.stringify(await enrichmentContent(e, args.enrichment)); return content !== undefined && args.test.test(content); } }) satisfies FlowrFilterFunction>, @@ -241,19 +242,19 @@ interface FilterData { } const evalVisit = { - and: ({ left, right }: BooleanBinaryNode, data: FilterData) => - evalTree(left, data) && evalTree(right, data), - or: ({ left, right }: BooleanBinaryNode, data: FilterData) => - evalTree(left, data) || evalTree(right, data), - xor: ({ left, right }: BooleanBinaryNode, data: FilterData) => - evalTree(left, data) !== evalTree(right, data), - not: ({ operand }: BooleanUnaryNode, data: FilterData) => - !evalTree(operand, data), + and: async({ left, right }: BooleanBinaryNode, data: FilterData) => + (await evalTree(left, data)) && (await evalTree(right, data)), + or: async({ left, right }: BooleanBinaryNode, data: FilterData) => + (await evalTree(left, data)) || (await evalTree(right, data)), + xor: async({ left, right }: BooleanBinaryNode, data: FilterData) => + (await evalTree(left, data)) !== (await evalTree(right, data)), + not: async({ operand }: BooleanUnaryNode, data: FilterData) => + !(await evalTree(operand, data)), 'r-type': ({ value }: LeafRType, { element }: FilterData) => element.node.type === value, 'vertex-type': ({ value }: LeafVertexType, { data, element }: FilterData) => data.dataflow.graph.getVertex(element.node.info.id)?.tag === value, - 'special': ({ value }: LeafSpecial, { data, element }: FilterData) => { + 'special': async({ value }: LeafSpecial, { data, element }: FilterData) => { const name = typeof value === 'string' ? value : value.name; const args = typeof value === 'string' ? undefined as unknown as FlowrFilterArgs : value.args; const getHandler = FlowrFilters[name]; @@ -264,7 +265,7 @@ const evalVisit = { } }; -function evalTree(tree: BooleanNode, data: FilterData): boolean { +async function evalTree(tree: BooleanNode, data: FilterData): Promise { /* we ensure that the types fit */ return evalVisit[tree.type](tree as never, data); } @@ -272,9 +273,9 @@ function evalTree(tree: BooleanNode, data: FilterData): boolean { /** * Evaluates the given filter expression against the provided data. */ -export function evalFilter(filter: FlowrFilterExpression, data: FilterData): boolean { +export async function evalFilter(filter: FlowrFilterExpression, data: FilterData): Promise { if(filter instanceof FlowrFilterCombinator) { - return evalTree(filter.get(), data); + return await evalTree(filter.get(), data); } else if(typeof filter === 'string' && ValidFlowrFilters.has(filter)) { const handler = FlowrFilters[filter as FlowrFilter]; return handler(data.element, undefined as unknown as FlowrFilterArgs, data.data); @@ -284,6 +285,6 @@ export function evalFilter(filter: FlowrFilterExpres return handler(data.element, args, data.data); } else { const tree = FlowrFilterCombinator.is(filter as FlowrFilterExpression); - return evalTree(tree.get(), data); + return await evalTree(tree.get(), data); } } diff --git a/src/search/flowr-search.ts b/src/search/flowr-search.ts index a979f816807..0800fd793c9 100644 --- a/src/search/flowr-search.ts +++ b/src/search/flowr-search.ts @@ -8,7 +8,9 @@ import { type EnrichmentElementContent, type EnrichmentSearchArguments, type EnrichmentSearchContent - , Enrichments } from './search-executor/search-enrichers'; + , Enrichments, + LazyEnrichmentContent +} from './search-executor/search-enrichers'; import type { ReadonlyFlowrAnalysisProvider } from '../project/flowr-analyzer'; /** @@ -17,7 +19,7 @@ import type { ReadonlyFlowrAnalysisProvider } from '../project/flowr-analyzer'; */ export interface FlowrSearchElement { readonly node: RNode; - readonly enrichments?: { [E in Enrichment]?: EnrichmentElementContent } + readonly enrichments?: { [E in Enrichment]?: LazyEnrichmentContent> } } export interface FlowrSearchNodeBase | undefined> { @@ -70,7 +72,7 @@ export interface FlowrSearchGetFilter extends Record { /** Intentionally, we abstract away from an array to avoid the use of conventional typescript operations */ export class FlowrSearchElements[] = FlowrSearchElement[]> { private elements: Elements = [] as unknown as Elements; - private enrichments: { [E in Enrichment]?: EnrichmentSearchContent } = {}; + private enrichments: { [E in Enrichment]?: LazyEnrichmentContent> } = {}; public constructor(elements?: Elements) { if(elements) { @@ -112,18 +114,21 @@ export class FlowrSearchElements(data: ReadonlyFlowrAnalysisProvider, enrichment: E, args?: EnrichmentSearchArguments): Promise { + public enrich(data: ReadonlyFlowrAnalysisProvider, enrichment: E, args?: EnrichmentSearchArguments): this { const enrichmentData = Enrichments[enrichment] as unknown as EnrichmentData, EnrichmentElementArguments, EnrichmentSearchContent, EnrichmentSearchArguments>; - if(enrichmentData.enrichSearch !== undefined) { + const enricher = enrichmentData.enrichSearch; + if(enricher !== undefined) { + const prevEnrichment = this.enrichments?.[enrichment]; this.enrichments = { ...this.enrichments ?? {}, - [enrichment]: await enrichmentData.enrichSearch(this as FlowrSearchElements, data, args, this.enrichments?.[enrichment]) + [enrichment]: new LazyEnrichmentContent>(async() => + await enricher(this as FlowrSearchElements, data, args, prevEnrichment && (() => prevEnrichment.get()))) }; } return this; } - public enrichmentContent(enrichment: E): EnrichmentSearchContent { - return this.enrichments?.[enrichment] as EnrichmentSearchContent; + public async enrichmentContent(enrichment: E): Promise> { + return await (this.enrichments?.[enrichment] as LazyEnrichmentContent>)?.get(); } } diff --git a/src/search/search-executor/search-enrichers.ts b/src/search/search-executor/search-enrichers.ts index 8f35df47e63..39ae9915d27 100644 --- a/src/search/search-executor/search-enrichers.ts +++ b/src/search/search-executor/search-enrichers.ts @@ -22,13 +22,12 @@ import { import { Identifier } from '../../dataflow/environments/identifier'; import { Dataflow } from '../../dataflow/graph/df-helper'; - export interface EnrichmentData { /** * A function that is applied to each element of the search to enrich it with additional data. */ - readonly enrichElement?: (element: FlowrSearchElement, search: FlowrSearchElements, analyzer: ReadonlyFlowrAnalysisProvider, args: ElementArguments | undefined, previousValue: ElementContent | undefined) => AsyncOrSync - readonly enrichSearch?: (search: FlowrSearchElements, data: ReadonlyFlowrAnalysisProvider, args: SearchArguments | undefined, previousValue: SearchContent | undefined) => AsyncOrSync + readonly enrichElement?: (element: FlowrSearchElement, search: FlowrSearchElements, analyzer: ReadonlyFlowrAnalysisProvider, args: ElementArguments | undefined, previousValue: (() => Promise) | undefined) => AsyncOrSync + readonly enrichSearch?: (search: FlowrSearchElements, data: ReadonlyFlowrAnalysisProvider, args: SearchArguments | undefined, previousValue: (() => Promise) | undefined) => AsyncOrSync /** * The mapping function used by the {@link Mapper.Enrichment} mapper. */ @@ -39,6 +38,22 @@ export type EnrichmentElementArguments = typeof Enrichment export type EnrichmentSearchContent = typeof Enrichments[E] extends EnrichmentData ? SC : never; export type EnrichmentSearchArguments = typeof Enrichments[E] extends EnrichmentData ? SA : never; +export class LazyEnrichmentContent{ + private readonly enrichment: () => Promise; + private content: Content | undefined; + + constructor(enrichment: () => Promise) { + this.enrichment = enrichment; + } + + public async get(): Promise { + if(this.content === undefined) { + this.content = await this.enrichment(); + } + return this.content; + } +} + /** * An enumeration that stores the names of the available enrichments that can be applied to a set of search elements. * See {@link FlowrSearchBuilder.with} for more information on how to apply enrichments. @@ -146,7 +161,7 @@ export const Enrichments = { } if(prev) { - content.targets.push(...prev.targets); + content.targets.push(...(await prev()).targets); } return content; }, @@ -156,7 +171,7 @@ export const Enrichments = { [Enrichment.LastCall]: { enrichElement: async(e, _s, analyzer, args, prev) => { guard(args && args.length, `${Enrichment.LastCall} enrichment requires at least one argument`); - const content = prev ?? { linkedIds: [] }; + const content = prev ? (await prev()) : { linkedIds: [] }; const df = (await analyzer.dataflow()).graph; const vertex = df.getVertex(e.node.info.id); if(vertex?.tag === VertexType.FunctionCall) { @@ -178,8 +193,8 @@ export const Enrichments = { mapper: ({ linkedIds }) => linkedIds } satisfies EnrichmentData[]>, [Enrichment.CfgInformation]: { - enrichElement: (e, search, _data, _args, prev) => { - const searchContent: CfgInformationSearchContent = search.enrichmentContent(Enrichment.CfgInformation); + enrichElement: async(e, search, _data, _args, prev) => { + const searchContent: CfgInformationSearchContent = await search.enrichmentContent(Enrichment.CfgInformation); return { ...prev, isRoot: searchContent.cfg.graph.rootIds().has(e.node.info.id), @@ -195,12 +210,13 @@ export const Enrichments = { }; // short-circuit if we already have a cfg stored - if(!args.forceRefresh && prev?.simpleCfg) { - return prev; + const prevValue = prev && await prev(); + if(!args.forceRefresh && prevValue?.simpleCfg) { + return prevValue; } const content: CfgInformationSearchContent = { - ...prev, + ...prevValue, cfg: await data.controlflow(args.simplificationPasses, CfgKind.WithDataflow), }; if(args.checkReachable) { @@ -212,7 +228,7 @@ export const Enrichments = { [Enrichment.QueryData]: { // the query data enrichment is just a "pass-through" that passes the query data to the underlying search enrichElement: (_e, _search, _data, args, prev) => (args ?? prev) as QueryDataElementContent, - enrichSearch: (_search, _data, args, prev) => deepMergeObject(prev as QueryDataSearchContent, args) + enrichSearch: async(_search, _data, args, prev) => prev ? deepMergeObject(await prev(), args) : args as QueryDataSearchContent } satisfies EnrichmentData } as const; @@ -222,22 +238,23 @@ export const Enrichments = { * @param e - The search element whose enrichment content should be retrieved. * @param enrichment - The enrichment content, if present, else `undefined`. */ -export function enrichmentContent(e: FlowrSearchElement, enrichment: E): EnrichmentElementContent { - return e?.enrichments?.[enrichment] as EnrichmentElementContent; +export async function enrichmentContent(e: FlowrSearchElement, enrichment: E): Promise> { + return await (e?.enrichments?.[enrichment] as LazyEnrichmentContent>)?.get(); } /** * Enriches the given search element with the given enrichment type, using the provided analysis data. */ -export async function enrichElement, E extends Enrichment>( - e: Element, s: FlowrSearchElements, analyzer: ReadonlyFlowrAnalysisProvider, enrichment: E, args?: EnrichmentElementArguments): Promise { +export function enrichElement, E extends Enrichment>( + e: Element, s: FlowrSearchElements, analyzer: ReadonlyFlowrAnalysisProvider, enrichment: E, args?: EnrichmentElementArguments): Element { const enrichmentData = Enrichments[enrichment] as unknown as EnrichmentData, EnrichmentElementArguments>; const prev = e?.enrichments; + const prevEnrichment = prev?.[enrichment]; return { ...e, enrichments: { ...prev ?? {}, - [enrichment]: await enrichmentData.enrichElement?.(e, s, analyzer, args, prev?.[enrichment]) + [enrichment]: new LazyEnrichmentContent | undefined>(async() => await enrichmentData.enrichElement?.(e, s, analyzer, args, prevEnrichment && (() => prevEnrichment.get()))) } }; } diff --git a/src/search/search-executor/search-mappers.ts b/src/search/search-executor/search-mappers.ts index 930695f96e5..628333ca72a 100644 --- a/src/search/search-executor/search-mappers.ts +++ b/src/search/search-executor/search-mappers.ts @@ -3,21 +3,22 @@ import type { ParentInformation } from '../../r-bridge/lang-4.x/ast/model/proces import { type Enrichment, type EnrichmentData, type EnrichmentElementContent, enrichmentContent, Enrichments } from './search-enrichers'; import type { MergeableRecord } from '../../util/objects'; import type { ReadonlyFlowrAnalysisProvider } from '../../project/flowr-analyzer'; +import type { AsyncOrSync } from 'ts-essentials'; export enum Mapper { Enrichment = 'enrichment' } export interface MapperData { - mapper: (e: FlowrSearchElement, data: ReadonlyFlowrAnalysisProvider, args: Arguments) => FlowrSearchElement[] + mapper: (e: FlowrSearchElement, data: ReadonlyFlowrAnalysisProvider, args: Arguments) => AsyncOrSync[]> } export type MapperArguments = typeof Mappers[M] extends MapperData ? Arguments : never; const Mappers = { [Mapper.Enrichment]: { - mapper: (e: FlowrSearchElement, _data: ReadonlyFlowrAnalysisProvider, enrichment: Enrichment) => { + mapper: async(e: FlowrSearchElement, _data: ReadonlyFlowrAnalysisProvider, enrichment: Enrichment) => { const enrichmentData = Enrichments[enrichment] as unknown as EnrichmentData>; - const content = enrichmentContent(e, enrichment); + const content = await enrichmentContent(e, enrichment); return content !== undefined ? enrichmentData.mapper?.(content) ?? [] : []; } } satisfies MapperData @@ -26,7 +27,7 @@ const Mappers = { /** * Maps the given search element using the specified mapper and arguments. */ -export function map, MapperType extends Mapper>( - e: Element, data: ReadonlyFlowrAnalysisProvider, mapper: MapperType, args: MapperArguments): Element[] { - return (Mappers[mapper] as MapperData>).mapper(e, data, args) as Element[]; +export async function map, MapperType extends Mapper>( + e: Element, data: ReadonlyFlowrAnalysisProvider, mapper: MapperType, args: MapperArguments): Promise { + return (await (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 84dd0496042..df8eb76e87c 100644 --- a/src/search/search-executor/search-transformer.ts +++ b/src/search/search-executor/search-transformer.ts @@ -146,26 +146,26 @@ async function getFilter[ filter: FlowrFilterExpression }): Promise> { const dataflow = await data.dataflow(); - return elements.mutate( - e => e.filter(e => evalFilter(filter, { element: e, data: { dataflow } })) as Elements + return await elements.mutate( + async e => (await Promise.all(e.map(async e => await evalFilter(filter, { element: e, data: { dataflow } }) ? [e] : []))).flat() as Elements ) as unknown as CascadeEmpty; } -async function getWith[], FSE extends FlowrSearchElements>( +function getWith[], FSE extends FlowrSearchElements>( input: ReadonlyFlowrAnalysisProvider, elements: FSE, { info, args }: { info: Enrichment, args?: EnrichmentElementArguments - }): Promise[]>> { + }): FlowrSearchElements[]> { - return (await elements.enrich(input, info, args)).mutate( - async s => await Promise.all(s.map(e => enrichElement(e, elements, input, info, args))) as Elements + return (elements.enrich(input, info, args)).mutate( + s => s.map(e => enrichElement(e, elements, input, info, args)) as Elements ) as unknown as FlowrSearchElements[]>; } -function getMap[], FSE extends 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 +async function getMap[], FSE extends FlowrSearchElements>( + data: ReadonlyFlowrAnalysisProvider, elements: FSE, { mapper, args }: { mapper: Mapper, args: MapperArguments }): Promise> { + return await elements.mutate( + async elements => (await Promise.all(elements.map(async e => await map(e, data, mapper, args)))).flat() as Elements ) as unknown as FlowrSearchElements; } diff --git a/test/functionality/_helper/search.ts b/test/functionality/_helper/search.ts index 17ffc879c96..1fb08a367b8 100644 --- a/test/functionality/_helper/search.ts +++ b/test/functionality/_helper/search.ts @@ -14,6 +14,7 @@ import { FlowrAnalyzerBuilder } from '../../../src/project/flowr-analyzer-builde import type { FlowrAnalyzer } from '../../../src/project/flowr-analyzer'; import type { DataflowInformation } from '../../../src/dataflow/info'; import { Dataflow } from '../../../src/dataflow/graph/df-helper'; +import type { AsyncOrSync } from 'ts-essentials'; /** * Asserts the result of a search or a set of searches (all of which should return the same result)! @@ -23,7 +24,7 @@ export function assertSearch( name: string | TestLabel, parser: KnownParser, code: string, - expected: readonly (NodeId | SlicingCriterion)[] | ((result: FlowrSearchElement[]) => boolean), + expected: readonly (NodeId | SlicingCriterion)[] | ((result: FlowrSearchElement[]) => AsyncOrSync), ...searches: FlowrSearchLike[] ) { const effectiveName = decorateLabelContext(name, ['search']); @@ -68,8 +69,8 @@ export function assertSearch( arrayEqual(result.map(r => r.node.info.id).sort(), [...expected].sort()), `Expected search results to match. Wanted: [${expected.join(', ')}], got: [${result.map(r => r.node.info.id).join(', ')}]`); } else { - const expectedFunc = expected as (result: FlowrSearchElement[]) => boolean; - assert(expectedFunc([...result]), `Expected search results ${JSON.stringify(result)} to match expected function`); + const expectedFunc = expected as (result: FlowrSearchElement[]) => AsyncOrSync; + assert(await expectedFunc([...result]), `Expected search results ${JSON.stringify(result)} to match expected function`); } } /* v8 ignore next 4 */ catch(e: unknown) { console.error('Dataflow-Graph', Dataflow.visualize.mermaid.url(dataflow)); @@ -93,13 +94,13 @@ export function assertSearchEnrichment( matchType: 'some' | 'every', ...searches: FlowrSearchLike[] ) { - assertSearch(name, parser, code, results => { + assertSearch(name, parser, code, async results => { for(const expected of expectedEnrichments) { for(const [enrichment, content] of Object.entries(expected)) { let any = false; for(const result of results) { try { - assert.deepEqual(enrichmentContent(result, enrichment as Enrichment), content); + assert.deepEqual(await enrichmentContent(result, enrichment as Enrichment), content); any = true; } catch(e: unknown) { if(matchType === 'every') {