diff --git a/src/linter/linter-executor.ts b/src/linter/linter-executor.ts index a835d28401b..36eba9286c8 100644 --- a/src/linter/linter-executor.ts +++ b/src/linter/linter-executor.ts @@ -20,21 +20,13 @@ export async function executeLintingRule(ruleName const searchTime = Date.now() - searchStart; const processStart = Date.now(); - const result = await rule.processSearchResult(searchResult, fullConfig, - { - /* we currently await them here for simplicity (no redundant awaits in the linting rules), but they could be passed as promises too */ - dataflow: await input.dataflow(), - normalize: await input.normalize(), - cfg: await input.controlflow(), - analyzer: input - } - ); + const result = await rule.processSearchResult(searchResult, fullConfig, input); const processTime = Date.now() - processStart; return { ...result, '.meta': { - ...result['.meta'], + ...(result['.meta'] as LintingRuleMetadata ?? {}), searchTimeMs: searchTime, processTimeMs: processTime } diff --git a/src/linter/linter-format.ts b/src/linter/linter-format.ts index 668b1dd52b9..84acedc1d5a 100644 --- a/src/linter/linter-format.ts +++ b/src/linter/linter-format.ts @@ -3,13 +3,11 @@ import type { FlowrSearchElement, FlowrSearchElements } from '../search/flowr-se import type { MergeableRecord } from '../util/objects'; import type { GeneratorNames } from '../search/search-executor/search-generators'; import type { TransformerNames } from '../search/search-executor/search-transformer'; -import type { NormalizedAst, ParentInformation } from '../r-bridge/lang-4.x/ast/model/processing/decorate'; +import type { ParentInformation } from '../r-bridge/lang-4.x/ast/model/processing/decorate'; import type { LintingRuleConfig, LintingRuleMetadata, LintingRuleNames, LintingRuleResult } from './linter-rules'; import type { AsyncOrSync, DeepPartial, DeepReadonly } from 'ts-essentials'; import type { LintingRuleTag } from './linter-tags'; import type { SourceLocation } from '../util/range'; -import type { DataflowInformation } from '../dataflow/info'; -import type { ControlFlowInformation } from '../control-flow/control-flow-graph'; import type { ReadonlyFlowrAnalysisProvider } from '../project/flowr-analyzer'; import type { NodeId } from '../r-bridge/lang-4.x/ast/model/processing/node-id'; import { isNotUndefined } from '../util/assert'; @@ -41,7 +39,7 @@ export interface LinterRuleInformation { * The base interface for a linting rule, which contains all of its relevant settings. * The registry of valid linting rules is stored in {@link LintingRules}. */ -export interface LintingRule[] = FlowrSearchElement[]> { +export interface LintingRule[] = FlowrSearchElement[]> { /** * Creates a flowR search that will then be executed and whose results will be passed to {@link processSearchResult}. * In the future, additional optimizations and transformations may be applied to the search between this function and {@link processSearchResult}. @@ -51,9 +49,9 @@ export interface LintingRule, config: Config, data: { normalize: NormalizedAst, dataflow: DataflowInformation, cfg: ControlFlowInformation, analyzer: ReadonlyFlowrAnalysisProvider }) => AsyncOrSync<{ - results: Result[], - '.meta': Metadata + readonly processSearchResult: (elements: FlowrSearchElements, config: Config, data: ReadonlyFlowrAnalysisProvider) => AsyncOrSync<{ + results: Result[], + '.meta'?: Metadata }> /** * A set of functions used to pretty-print the given linting result. diff --git a/src/linter/rules/absolute-path.ts b/src/linter/rules/absolute-path.ts index 71c7ee2f9c7..0983a423e66 100644 --- a/src/linter/rules/absolute-path.ts +++ b/src/linter/rules/absolute-path.ts @@ -136,13 +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 regex = config.absolutePathRegex ? new RegExp(config.absolutePathRegex) : undefined; + const normalize = await data.normalize(); + const dataflow = await data.dataflow(); return { results: elements.getElements().flatMap(element => { metadata.totalConsidered++; @@ -162,7 +164,7 @@ export const ABSOLUTE_PATH = { } 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); + const elem = normalize.idMap.get(r.nodeId); return { certainty: LintingResultCertainty.Certain, filePath: r.value, @@ -177,10 +179,10 @@ export const ABSOLUTE_PATH = { return []; } } else { - const dfNode = data.dataflow.graph.getVertex(node.info.id); + const dfNode = 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()) : []; + const strings = handler ? handler(dataflow.graph, dfNode, data.inspectContext()) : []; if(strings) { return strings.filter(s => isAbsolutePath(s, regex)).map(str => ({ certainty: LintingResultCertainty.Uncertain, diff --git a/src/linter/rules/dataframe-access-validation.ts b/src/linter/rules/dataframe-access-validation.ts index 33094e643e7..071306527b1 100644 --- a/src/linter/rules/dataframe-access-validation.ts +++ b/src/linter/rules/dataframe-access-validation.ts @@ -58,18 +58,20 @@ export interface DataFrameAccessValidationMetadata extends MergeableRecord { export const DATA_FRAME_ACCESS_VALIDATION = { createSearch: () => Q.all(), processSearchResult: async(elements, config, data) => { - let ctx = data.analyzer.inspectContext(); + const normalize = await data.normalize(); + const dataflow = await data.dataflow(); + let ctx = data.inspectContext(); ctx = { ...ctx, - config: FlowrConfig.amend(data.analyzer.flowrConfig, flowrConfig => { + config: FlowrConfig.amend(data.flowrConfig, flowrConfig => { if(config.readLoadedData !== undefined) { (flowrConfig.abstractInterpretation.dataFrame.readLoadedData as { readExternalFiles: boolean }).readExternalFiles = config.readLoadedData; } return flowrConfig; }) }; - const cfg = await data.analyzer.controlflow(undefined, CfgKind.NoFunctionDefs); - const inference = new DataFrameShapeInferenceVisitor({ controlFlow: cfg, dfg: data.dataflow.graph, normalizedAst: data.normalize, ctx }); + const cfg = await data.controlflow(undefined, CfgKind.NoFunctionDefs); + const inference = new DataFrameShapeInferenceVisitor({ controlFlow: cfg, dfg: dataflow.graph, normalizedAst: normalize, ctx }); inference.start(); const accessOperations = getAccessOperations(elements, inference); @@ -109,8 +111,8 @@ export const DATA_FRAME_ACCESS_VALIDATION = { ) .map(({ nodeId, operand, ...accessed }) => ({ ...accessed, - node: data.normalize.idMap.get(nodeId), - operand: operand === undefined ? undefined : data.normalize.idMap.get(operand), + node: normalize.idMap.get(nodeId), + operand: operand === undefined ? undefined : normalize.idMap.get(operand), })) .map(({ node, operand, ...accessed }) => ({ ...accessed, diff --git a/src/linter/rules/dead-code.ts b/src/linter/rules/dead-code.ts index 2fdec54323e..6d856671e61 100644 --- a/src/linter/rules/dead-code.ts +++ b/src/linter/rules/dead-code.ts @@ -9,11 +9,12 @@ import { SourceLocation } from '../../util/range'; import type { MergeableRecord } from '../../util/objects'; import { Q } from '../../search/flowr-search-builder'; import { LintingRuleTag } from '../linter-tags'; -import { Enrichment, enrichmentContent } from '../../search/search-executor/search-enrichers'; +import { Enrichment } from '../../search/search-executor/search-enrichers'; import { isNotUndefined } from '../../util/assert'; import { type CfgSimplificationPassName, DefaultCfgSimplificationOrder } from '../../control-flow/cfg-simplification'; import type { Writable } from 'ts-essentials'; import { RoleInParent } from '../../r-bridge/lang-4.x/ast/model/processing/role'; +import { FlowrFilter, FlowrFilterCombinator } from '../../search/flowr-search-filters'; export interface DeadCodeResult extends LintingResult { readonly loc: SourceLocation @@ -27,35 +28,30 @@ export interface DeadCodeConfig extends MergeableRecord { simplificationPasses?: CfgSimplificationPassName[] } -export interface DeadCodeMetadata extends MergeableRecord { - consideredNodes: number -} - export const DEAD_CODE = { createSearch: (config) => Q.all().with(Enrichment.CfgInformation, { checkReachable: true, simplificationPasses: config.simplificationPasses ?? [...DefaultCfgSimplificationOrder, 'analyze-dead-code'] - }), + }).filter(FlowrFilterCombinator.is({ + name: FlowrFilter.MatchesEnrichment, + args: { + enrichment: Enrichment.CfgInformation, + test: { + isReachable: true + } + } + }).or(FlowrFilterCombinator.is({ name: FlowrFilter.RoleInParent, args: { roleInParent: RoleInParent.ExpressionListGrouping } })).not()), processSearchResult: (elements, _config, _data) => { - const meta: DeadCodeMetadata = { - consideredNodes: 0 - }; return { results: combineResults( elements.getElements() - .filter(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) })) .filter(element => isNotUndefined(element.loc)) as Writable[] - ), - '.meta': meta + ) }; }, prettyPrint: { @@ -70,7 +66,7 @@ export const DEAD_CODE = { description: 'Marks areas of code that are never reached during execution.', defaultConfig: {} } -} as const satisfies LintingRule; +} as const satisfies LintingRule; function combineResults(results: Writable[]): DeadCodeResult[] { for(let i = results.length-1; i >= 0; i--){ diff --git a/src/linter/rules/deprecated-functions.ts b/src/linter/rules/deprecated-functions.ts index 6ff68da6c2b..af086848827 100644 --- a/src/linter/rules/deprecated-functions.ts +++ b/src/linter/rules/deprecated-functions.ts @@ -4,7 +4,7 @@ import { type FunctionsMetadata, type FunctionsResult, type FunctionsToDetectCon export const DEPRECATED_FUNCTIONS = { createSearch: (config) => functionFinderUtil.createSearch(config.fns), - processSearchResult: functionFinderUtil.processSearchResult, + processSearchResult: async(e, c, d) => await functionFinderUtil.processSearchResult(e, c, d), prettyPrint: functionFinderUtil.prettyPrint('deprecated'), info: { name: 'Deprecated Functions', diff --git a/src/linter/rules/file-path-validity.ts b/src/linter/rules/file-path-validity.ts index b6ad8500669..cb4ccd3163b 100644 --- a/src/linter/rules/file-path-validity.ts +++ b/src/linter/rules/file-path-validity.ts @@ -82,7 +82,7 @@ export const FILE_PATH_VALIDITY = { } // check if any write to the same file happens before the read, and exclude this case if so - const writesToFile = results.write.filter(r => samePath(r.value as string, matchingRead.value as string, data.analyzer.flowrConfig.solver.resolveSource?.ignoreCapitalization)); + const writesToFile = results.write.filter(r => samePath(r.value as string, matchingRead.value as string, data.flowrConfig.solver.resolveSource?.ignoreCapitalization)); const writesBefore = writesToFile.map(w => happensBefore(cfg, w.nodeId, element.node.info.id)); if(writesBefore.some(w => w === Ternary.Always)) { metadata.totalWritesBeforeAlways++; @@ -90,9 +90,9 @@ export const FILE_PATH_VALIDITY = { } // check if the file exists! - const paths = findSource(data.analyzer.flowrConfig.solver.resolveSource, matchingRead.value as string, { + const paths = findSource(data.flowrConfig.solver.resolveSource, matchingRead.value as string, { referenceChain: element.node.info.file ? [element.node.info.file] : [], - ctx: data.analyzer.inspectContext() + ctx: data.inspectContext() }); if(paths && paths.length) { metadata.totalValid++; diff --git a/src/linter/rules/function-finder-util.ts b/src/linter/rules/function-finder-util.ts index ab6ce3e9e44..cf10ca42892 100644 --- a/src/linter/rules/function-finder-util.ts +++ b/src/linter/rules/function-finder-util.ts @@ -4,17 +4,17 @@ import { Enrichment, enrichmentContent } from '../../search/search-executor/sear import { SourceLocation } from '../../util/range'; import { LintingPrettyPrintContext, type LintingResult, LintingResultCertainty } from '../linter-format'; import type { FlowrSearchElement, FlowrSearchElements } from '../../search/flowr-search'; -import type { NormalizedAst, ParentInformation } from '../../r-bridge/lang-4.x/ast/model/processing/decorate'; +import type { ParentInformation } from '../../r-bridge/lang-4.x/ast/model/processing/decorate'; import type { MergeableRecord } from '../../util/objects'; import { isNotUndefined } from '../../util/assert'; import { getArgumentStringValue } from '../../dataflow/eval/resolve/resolve-argument'; -import type { DataflowInformation } from '../../dataflow/info'; import { isFunctionCallVertex, VertexType } from '../../dataflow/graph/vertex'; import type { FunctionInfo } from '../../queries/catalog/dependencies-query/function-info/function-info'; import { Unknown } from '../../queries/catalog/dependencies-query/dependencies-query-format'; -import type { ReadonlyFlowrAnalysisProvider } from '../../project/flowr-analyzer'; import type { BrandedIdentifier } from '../../dataflow/environments/identifier'; import { Ternary } from '../../util/logic'; +import type { ReadonlyFlowrAnalysisProvider } from '../../project/flowr-analyzer'; +import type { AsyncOrSync } from 'ts-essentials'; export interface FunctionsResult extends LintingResult { function: string @@ -46,23 +46,25 @@ export const functionFinderUtil = { name: FlowrFilter.MatchesEnrichment, args: { enrichment: Enrichment.CallTargets, - test: testFunctionsIgnoringPackage(functions) + test: { + targets: testFunctionsIgnoringPackage(functions) + } } }) ); }, - processSearchResult: []>( + async processSearchResult[]>( elements: FlowrSearchElements, _config: unknown, _data: unknown, - refineSearch: (elements: T) => (T[number] & { certainty?: LintingResultCertainty })[] = e => e, - ) => { + refineSearch: (elements: T) => AsyncOrSync<(T[number] & { certainty?: LintingResultCertainty })[]> = e => e, + ) { const metadata: FunctionsMetadata = { totalCalls: 0, totalFunctionDefinitions: 0 }; - const results = refineSearch(elements.getElements()) + const results = (await refineSearch(elements.getElements())) .flatMap(element => { metadata.totalCalls++; return enrichmentContent(element, Enrichment.CallTargets).targets.map(target => { @@ -93,27 +95,28 @@ export const functionFinderUtil = { [LintingPrettyPrintContext.Full]: (result: FunctionsResult) => `Function \`${result.function}\` called at ${SourceLocation.format(result.loc)} is related to ${functionType}` }; }, - requireArgumentValue( + async requireArgumentValue( element: FlowrSearchElement, pool: readonly FunctionInfo[], - data: { normalize: NormalizedAst, dataflow: DataflowInformation, analyzer: ReadonlyFlowrAnalysisProvider }, + analyzer: ReadonlyFlowrAnalysisProvider, requireValue: RegExp | string | undefined - ): Ternary { + ): Promise { const info = pool.find(f => f.name === element.node.lexeme); /* if we have no additional info, we assume they always access the network */ if(info === undefined) { return Ternary.Always; } - const vert = data.dataflow.graph.getVertex(element.node.info.id); + const dataflow = await analyzer.dataflow(); + const vert = dataflow.graph.getVertex(element.node.info.id); if(isFunctionCallVertex(vert)){ const args = getArgumentStringValue( - data.analyzer.flowrConfig.solver.variables, - data.dataflow.graph, + analyzer.flowrConfig.solver.variables, + dataflow.graph, vert, info.argIdx, info.argName, info.resolveValue, - data.analyzer.inspectContext()); + analyzer.inspectContext()); // we obtain all values, at least one of them has to trigger for the request const argValues: string[] = args ? args.values().flatMap(v => [...v]).filter(isNotUndefined).toArray() : []; if(argValues.length === 0){ @@ -127,5 +130,3 @@ export const functionFinderUtil = { return Ternary.Never; } }; - - diff --git a/src/linter/rules/naming-convention.ts b/src/linter/rules/naming-convention.ts index 928f8aab535..9a02ffff788 100644 --- a/src/linter/rules/naming-convention.ts +++ b/src/linter/rules/naming-convention.ts @@ -206,7 +206,8 @@ export function createNamingConventionQuickFixes(graph: DataflowGraph, nodeId: N export const NAMING_CONVENTION = { createSearch: (_config) => Q.all().filter(VertexType.VariableDefinition), - processSearchResult: (elements, config, data) => { + processSearchResult: async(elements, config, data) => { + const dataflow = await data.dataflow(); const symbols = elements.getElements() .map(m => ({ certainty: LintingResultCertainty.Certain, @@ -223,7 +224,7 @@ export const NAMING_CONVENTION = { return { ...m, involvedId: id, - quickFix: fix ? createNamingConventionQuickFixes(data.dataflow.graph, id, fix, casing) : undefined + quickFix: fix ? createNamingConventionQuickFixes(dataflow.graph, id, fix, casing) : undefined } as NamingConventionResult; }); return { diff --git a/src/linter/rules/network-functions.ts b/src/linter/rules/network-functions.ts index 9a48c620b1c..8bc25866a89 100644 --- a/src/linter/rules/network-functions.ts +++ b/src/linter/rules/network-functions.ts @@ -16,26 +16,30 @@ export interface NetworkFunctionsConfig extends MergeableRecord { export const NETWORK_FUNCTIONS = { createSearch: (config) => functionFinderUtil.createSearch(config.fns), - processSearchResult: (e, c, d) => functionFinderUtil.processSearchResult(e, c, d, - es => { - const res: (FlowrSearchElement & { certainty: LintingResultCertainty })[] = []; - for(const e of es) { - const val = functionFinderUtil.requireArgumentValue( - e, - ReadFunctions, - d, - c.onlyTriggerWithArgument - ); - if(val === Ternary.Never) { - continue; + processSearchResult: (e, c, d) => { + return functionFinderUtil.processSearchResult(e, c, d, + async(es) => { + const res: (FlowrSearchElement & { certainty: LintingResultCertainty })[] = []; + for(const e of es) { + const val = await functionFinderUtil.requireArgumentValue( + e, + ReadFunctions, + d, + c.onlyTriggerWithArgument + ); + if(val === Ternary.Never) { + continue; + } + const x = e as unknown as FlowrSearchElement & { + certainty: LintingResultCertainty + }; + x.certainty = val === Ternary.Always ? LintingResultCertainty.Certain : LintingResultCertainty.Uncertain; + res.push(x); } - const x = e as unknown as FlowrSearchElement & { certainty: LintingResultCertainty }; - x.certainty = val === Ternary.Always ? LintingResultCertainty.Certain : LintingResultCertainty.Uncertain; - res.push(x); + return res; } - return res; - } - ), + ); + }, prettyPrint: functionFinderUtil.prettyPrint('network operations'), info: { name: 'Network Functions', diff --git a/src/linter/rules/problematic-inputs.ts b/src/linter/rules/problematic-inputs.ts index cb21a4093e7..76b2e7558f2 100644 --- a/src/linter/rules/problematic-inputs.ts +++ b/src/linter/rules/problematic-inputs.ts @@ -66,8 +66,6 @@ export interface ProblematicInputsConfig extends MergeableRecord { inputFns?: InputClassifierConfig } -export type ProblematicInputsMetadata = MergeableRecord; - export const PROBLEMATIC_INPUTS = { createSearch: config => { const considerArr = normalizeConsider(config); @@ -87,7 +85,7 @@ export const PROBLEMATIC_INPUTS = { const nid = element.node.info.id; const criterion = SlicingCriterion.fromId(nid); const q: InputSourcesQuery = { type: 'input-sources', criterion, config: config.inputFns }; - const all = await data.analyzer.query([q]); + const all = await data.query([q]); const inputSourcesResult = all['input-sources']; const sources = inputSourcesResult?.results?.[criterion] ?? []; @@ -103,10 +101,7 @@ export const PROBLEMATIC_INPUTS = { } } - return { - results, - '.meta': {} - }; + return { results }; }, /* helper to format input sources for pretty printing */ prettyPrint: { @@ -130,4 +125,4 @@ export const PROBLEMATIC_INPUTS = { consider: defaultConsider } } -} as const satisfies LintingRule; +} as const satisfies LintingRule; diff --git a/src/linter/rules/roxygen-arguments.ts b/src/linter/rules/roxygen-arguments.ts index a59d2dd702c..2d0a98090f9 100644 --- a/src/linter/rules/roxygen-arguments.ts +++ b/src/linter/rules/roxygen-arguments.ts @@ -12,14 +12,11 @@ import { LintingRuleTag } from '../linter-tags'; import { isNotUndefined } from '../../util/assert'; import type { Writable } from 'ts-essentials'; import { VertexType } from '../../dataflow/graph/vertex'; -import type { NodeId } from '../../r-bridge/lang-4.x/ast/model/processing/node-id'; -import type { RoxygenTag, RoxygenTagParam } from '../../r-bridge/roxygen2/roxygen-ast'; import { KnownRoxygenTags } from '../../r-bridge/roxygen2/roxygen-ast'; import { RFunctionDefinition } from '../../r-bridge/lang-4.x/ast/model/nodes/r-function-definition'; import type { RParameter } from '../../r-bridge/lang-4.x/ast/model/nodes/r-parameter'; -import { getDocumentationOf } from '../../r-bridge/roxygen2/documentation-provider'; -import type { AstIdMap } from '../../r-bridge/lang-4.x/ast/model/processing/decorate'; import type { RNode } from '../../r-bridge/lang-4.x/ast/model/model'; +import { Enrichment, enrichmentContent } from '../../search/search-executor/search-enrichers'; export interface RoxygenArgsResult extends LintingResult { readonly loc: SourceLocation @@ -31,14 +28,6 @@ export type RoxygenArgsConfig = MergeableRecord; export type RoxygenArgsMetadata = MergeableRecord; -function getDocumentation(id: NodeId, idMap: AstIdMap): readonly RoxygenTag[] | undefined { - const comment = getDocumentationOf(id, idMap); - if(comment === undefined){ - return undefined; - } - return Array.isArray(comment) ? comment : [comment] as readonly RoxygenTag[]; -} - function calculateArgumentDiff(inheritedParams: readonly string[], functionParam: readonly string[], roxygenParam: readonly string[]): false | { under: string[], over: string[] }{ //match documented against existing params @@ -65,8 +54,10 @@ function calculateArgumentDiff(inheritedParams: readonly string[], functionParam } export const ROXYGEN_ARGS = { - createSearch: () => Q.all().filter(VertexType.FunctionDefinition), - processSearchResult: (elements, _config, { normalize }) => { + createSearch: () => Q.all() + .filter(VertexType.FunctionDefinition) + .with(Enrichment.Roxygen), + processSearchResult: (elements, _config) => { return { results: elements.getElements() @@ -75,22 +66,20 @@ export const ROXYGEN_ARGS = { underDocumented: [] as string[], overDocumented: [] as string[] })) - .filter(({ element: { node }, underDocumented, overDocumented }) => { - const comments = getDocumentation(node.info.id, normalize.idMap); - if(!comments) { + .filter(({ element, underDocumented, overDocumented }) => { + const roxygen = enrichmentContent(element, Enrichment.Roxygen); + if(!roxygen.documentation.length) { return false; } - const parameters = getParameters(node); //get parameter names - const functionParamNames = parameters.map(p => p.name.content.toString()); - const inheritedParams = comments.filter(tag => (tag?.inherited && tag.type === KnownRoxygenTags.Param)).map(tag => ((tag as RoxygenTagParam).value.name)); - const roxygenParamNames = comments - .filter(tag => tag.type === KnownRoxygenTags.Param) - .map(tag => tag.value.name); + const params = roxygen.tags[KnownRoxygenTags.Param] ?? []; + const functionParamNames = getParameters(element.node).map(p => p.name.content.toString()); + const inheritedParams = params.filter(tag => tag.inherited).map(tag => tag.value.name); + const roxygenParamNames = params.map(tag => tag.value.name); if(functionParamNames === null || roxygenParamNames == null){ return false; } - const result = calculateArgumentDiff(inheritedParams, functionParamNames, roxygenParamNames); + const result = calculateArgumentDiff(inheritedParams ?? [], functionParamNames, roxygenParamNames); if(result === false){ return false; } @@ -124,4 +113,4 @@ export const ROXYGEN_ARGS = { function getParameters(node: RNode): RParameter[]{ return RFunctionDefinition.is(node) ? node.parameters : []; -} \ No newline at end of file +} diff --git a/src/linter/rules/seeded-randomness.ts b/src/linter/rules/seeded-randomness.ts index 60e063715a1..8c1d7d0063b 100644 --- a/src/linter/rules/seeded-randomness.ts +++ b/src/linter/rules/seeded-randomness.ts @@ -63,14 +63,17 @@ export const SEEDED_RANDOMNESS = { name: FlowrFilter.MatchesEnrichment, args: { enrichment: Enrichment.CallTargets, - test: testFunctionsIgnoringPackage(config.randomnessConsumers) + test: { + targets: testFunctionsIgnoringPackage(config.randomnessConsumers) + } } }) .with(Enrichment.LastCall, [ { 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, data) => { + const dataflow = await data.dataflow(); 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 = { @@ -104,7 +107,7 @@ export const SEEDED_RANDOMNESS = { // function calls are already taken care of through the LastCall enrichment itself for(const f of func ?? []) { - if(isConstantArgument(dataflow.graph, f, 0, analyzer.inspectContext())) { + if(isConstantArgument(dataflow.graph, f, 0, data.inspectContext())) { const fCds = new Set(f.cds).difference(cds); metadata.callsWithFunctionProducers++; if(fCds.size <= 0 || happensInEveryBranchSet(fCds)){ @@ -125,7 +128,7 @@ export const SEEDED_RANDOMNESS = { const dest = FunctionArgument.getReference(a.args[argIdx]); if(dest !== undefined && assignmentProducers.has(recoverName(dest, dataflow.graph.idMap) as string)) { // we either have arg index 0 or 1 for the assignmentProducers destination, so we select the assignment value as 1-argIdx here - if(isConstantArgument(dataflow.graph, a, 1 - argIdx, analyzer.inspectContext())) { + if(isConstantArgument(dataflow.graph, a, 1 - argIdx, data.inspectContext())) { const aCds = new Set(a.cds).difference(cds); if(aCds.size <= 0 || happensInEveryBranchSet(aCds)) { metadata.callsWithAssignmentProducers++; diff --git a/src/linter/rules/stop-with-call-arg.ts b/src/linter/rules/stop-with-call-arg.ts index 826cc2fcb19..66a67eba88e 100644 --- a/src/linter/rules/stop-with-call-arg.ts +++ b/src/linter/rules/stop-with-call-arg.ts @@ -30,7 +30,8 @@ export interface StopWithCallMetadata extends MergeableRecord { export const STOP_WITH_CALL_ARG = { createSearch: () => Q.var('stop').filter(VertexType.FunctionCall), - processSearchResult: (elements, _config, { dataflow, analyzer }) => { + processSearchResult: async(elements, _config, data) => { + const dataflow = await data.dataflow(); const meta: StopWithCallMetadata = { consideredNodes: 0 }; @@ -58,7 +59,7 @@ export const STOP_WITH_CALL_ARG = { const mapping = pMatch(fCall.args, stopParamMap); const mappedToStop = mapping.get('call.') ?? []; for(const argId of mappedToStop) { - const res = resolveIdToValue(argId, { graph: dataflow.graph, environment: fCall.environment, ctx: analyzer.inspectContext() }); + const res = resolveIdToValue(argId, { graph: dataflow.graph, environment: fCall.environment, ctx: data.inspectContext() }); const values = valueSetGuard(res); if(values?.type === 'set' && values.elements.length !== 0){ if(values.elements[0].type === 'logical'){ diff --git a/src/linter/rules/unused-definition.ts b/src/linter/rules/unused-definition.ts index bcf24d5d188..96a6387c74f 100644 --- a/src/linter/rules/unused-definition.ts +++ b/src/linter/rules/unused-definition.ts @@ -93,7 +93,9 @@ export const UNUSED_DEFINITION = { /* this can be done better once we have types */ createSearch: config => Q.all().filter( config.includeFunctionDefinitions ? FlowrFilterCombinator.is(VertexType.VariableDefinition).or(VertexType.FunctionDefinition) : VertexType.VariableDefinition), - processSearchResult: (elements, config, data): { results: UnusedDefinitionResult[], '.meta': UnusedDefinitionMetadata } => { + processSearchResult: async(elements, config, data): Promise<{ results: UnusedDefinitionResult[], '.meta': UnusedDefinitionMetadata }> => { + const normalize = await data.normalize(); + const dataflow = await data.dataflow(); const metadata: UnusedDefinitionMetadata = { totalConsidered: 0 }; @@ -101,7 +103,7 @@ export const UNUSED_DEFINITION = { results: onlyKeepSupersetOfUnused(elements.getElements().flatMap(element => { metadata.totalConsidered++; - const dfgVertex = data.dataflow.graph.getVertex(element.node.info.id); + const dfgVertex = dataflow.graph.getVertex(element.node.info.id); if(!dfgVertex || ( !isVariableDefinitionVertex(dfgVertex) && isFunctionDefinitionVertex(dfgVertex) && !config.includeFunctionDefinitions @@ -109,7 +111,7 @@ export const UNUSED_DEFINITION = { return undefined; } - const ingoingEdges = data.dataflow.graph.ingoingEdges(dfgVertex.id); + const ingoingEdges = dataflow.graph.ingoingEdges(dfgVertex.id); const interestedIn = isVariableDefinitionVertex(dfgVertex) ? InterestingEdgesVariable : InterestingEdgesFunction; const ingoingInteresting = ingoingEdges?.values().some(e => DfEdge.includesType(e, interestedIn)); @@ -125,7 +127,7 @@ export const UNUSED_DEFINITION = { variableName, involvedId: element.node.info.id, loc: SourceLocation.fromNode(element.node) ?? SourceLocation.invalid(), - quickFix: buildQuickFix(element.node, data.dataflow.graph, data.normalize) + quickFix: buildQuickFix(element.node, dataflow.graph, normalize) }] satisfies UnusedDefinitionResult[]; }).filter(isNotUndefined)), '.meta': metadata diff --git a/src/linter/rules/useless-loop.ts b/src/linter/rules/useless-loop.ts index 9c2d727591c..648e4d06a48 100644 --- a/src/linter/rules/useless-loop.ts +++ b/src/linter/rules/useless-loop.ts @@ -23,7 +23,10 @@ export interface UselessLoopMetadata extends MergeableRecord { export const USELESS_LOOP = { createSearch: () => Q.all().filter(VertexType.FunctionCall), - processSearchResult: (elements, useLessLoopConfig, { analyzer, dataflow, normalize, cfg }) => { + processSearchResult: async(elements, useLessLoopConfig, data) => { + const normalize = await data.normalize(); + const dataflow = await data.dataflow(); + const cfg = await data.controlflow(); const results = elements.getElements().filter(e => { const vertex = dataflow.graph.getVertex(e.node.info.id); return vertex @@ -31,7 +34,7 @@ export const USELESS_LOOP = { && vertex.origin !== 'unnamed' && useLessLoopConfig.loopyFunctions.has(vertex.origin[0]); }).filter(loop => - onlyLoopsOnce(loop.node.info.id, dataflow.graph, cfg, normalize, analyzer.inspectContext()) + onlyLoopsOnce(loop.node.info.id, dataflow.graph, cfg, normalize, data.inspectContext()) ).map(res => ({ certainty: LintingResultCertainty.Certain, name: res.node.lexeme as string, @@ -59,4 +62,4 @@ export const USELESS_LOOP = { loopyFunctions: loopyFunctions } } -} as const satisfies LintingRule; \ No newline at end of file +} as const satisfies LintingRule; diff --git a/src/search/flowr-search-filters.ts b/src/search/flowr-search-filters.ts index e163acad3f5..53e27ac136d 100644 --- a/src/search/flowr-search-filters.ts +++ b/src/search/flowr-search-filters.ts @@ -2,11 +2,13 @@ import { RType, ValidRTypes } from '../r-bridge/lang-4.x/ast/model/type'; import { ValidVertexTypes, VertexType } from '../dataflow/graph/vertex'; import type { ParentInformation } from '../r-bridge/lang-4.x/ast/model/processing/decorate'; import type { FlowrSearchElement } from './flowr-search'; -import type { CallTargetsContent } from './search-executor/search-enrichers'; -import { Enrichment, enrichmentContent } from './search-executor/search-enrichers'; +import type { Enrichment } from './search-executor/search-enrichers'; +import { enrichmentContent, EnrichmentElementContent } from './search-executor/search-enrichers'; import type { DataflowInformation } from '../dataflow/info'; -import { Identifier } from '../dataflow/environments/identifier'; import type { BuiltInProcName } from '../dataflow/environments/built-in-proc-name'; +import { expensiveTrace } from '../util/log'; +import { searchLogger } from './search-executor/search-generators'; +import type { RoleInParent } from '../r-bridge/lang-4.x/ast/model/processing/role'; export type FlowrFilterName = keyof typeof FlowrFilters; interface FlowrFilterWithArgs> { @@ -30,7 +32,12 @@ export enum FlowrFilter { * Only returns search elements whose {@link FunctionOriginInformation} match a given pattern or value. * This filter accepts {@link OriginKindArgs}, which includes the {@link DataflowGraphVertexFunctionCall.origin} to match for, whether to match for every or some origins, and whether to include non-function-calls in the filtered query. */ - OriginKind = 'origin-kind' + OriginKind = 'origin-kind', + /** + * Only returns search element whose {@link RoleInParent} matches a given {@link RoleInParent}. + * This filter accepts an object containing a `roleInParent` argument of type {@link RoleInParent}. + */ + RoleInParent = 'role-in-parent', } export type FlowrFilterFunction = (e: FlowrSearchElement, args: T, data: { dataflow: DataflowInformation }) => boolean; @@ -42,23 +49,55 @@ export const FlowrFilters = { return e.node.type !== RType.Argument || e.node.name !== undefined; }) satisfies FlowrFilterFunction, [FlowrFilter.MatchesEnrichment]: ((e: FlowrSearchElement, args: MatchesEnrichmentArgs) => { - if(args.enrichment === Enrichment.CallTargets) { - const c: CallTargetsContent = enrichmentContent(e, Enrichment.CallTargets); - if(c === undefined || c.targets === undefined) { - return false; - } - for(const fn of c.targets) { - if(typeof fn === 'string' && args.test.test(fn)) { - return true; + const content = enrichmentContent(e, args.enrichment); + return content && testRecursive(content, args.test); + + function testRecursive(realChild: Record, expectedChild: Record): boolean { + expensiveTrace(searchLogger, () => `Comparing ${JSON.stringify(realChild)} against ${JSON.stringify(expectedChild)}`); + + for(const [expectedKey, expectedValue] of Object.entries(expectedChild)) { + const realValue = realChild[expectedKey]; + if(!realValue) { + expensiveTrace(searchLogger, () => `Real value ${JSON.stringify(realValue)} does not exist for expected key ${expectedKey}`); + return false; + } + + if(Array.isArray(realValue)) { + const match = typeof expectedValue === 'object' ? expectedValue instanceof RegExp ? + // if we expect a regular expression but an array is supplied, test each value + (value: unknown) => expectedValue.test(typeof value === 'string' ? value : String(value)) : + // if we expect an object that is not a regular expression, match against our expected structure + (value: unknown) => testRecursive(value as Record, expectedValue as Record) : + // in any other case (primitives!), match against the exact value + (value: unknown) => expectedValue === value; + if(!(args.arrayMatch === 'every' ? realValue.every(match) : realValue.some(match))) { + expensiveTrace(searchLogger, () => `Array ${JSON.stringify(realValue)} does not match expected value ${JSON.stringify(expectedValue)} (array match ${args.arrayMatch})`); + return false; + } + } else if(typeof realValue === 'object') { + // for objects, we recursively match + if(!testRecursive(realValue as Record, expectedValue as Record)) { + expensiveTrace(searchLogger, () => `Object ${JSON.stringify(realValue)} does not match expected object ${JSON.stringify(expectedValue)}`); + return false; + } } - if(typeof fn === 'object' && 'node' in fn && fn.node.type === RType.FunctionCall && fn.node.named && args.test.test(Identifier.getName(fn.node.functionName.content))) { - return true; + + // for anything else, we match with our regular expression or string + if(expectedValue instanceof RegExp) { + if(!expectedValue.test(typeof realValue === 'string' ? realValue : String(realValue as unknown))) { + expensiveTrace(searchLogger, () => `Value ${JSON.stringify(realValue)} does not match expected regular expression ${expectedValue}`); + return false; + } + } else if(typeof expectedValue !== 'object') { + if(expectedValue !== realValue) { + expensiveTrace(searchLogger, () => `Value ${JSON.stringify(realValue)} does not match expected string ${JSON.stringify(expectedValue)}`); + return false; + } } } - return false; - } else { - const content = JSON.stringify(enrichmentContent(e, args.enrichment)); - return content !== undefined && args.test.test(content); + + expensiveTrace(searchLogger, () => `Object ${JSON.stringify(realChild)} matches ${JSON.stringify(expectedChild)}`); + return true; } }) satisfies FlowrFilterFunction>, [FlowrFilter.OriginKind]: ((e: FlowrSearchElement, args: OriginKindArgs, data: { dataflow: DataflowInformation }) => { @@ -71,13 +110,23 @@ export const FlowrFilters = { (origin: string) => (args.origin as RegExp).test(origin); const origins = Array.isArray(dfgNode.origin) ? dfgNode.origin : [dfgNode.origin]; return args.matchType === 'every' ? origins.every(match) : origins.some(match); - }) satisfies FlowrFilterFunction + }) satisfies FlowrFilterFunction, + [FlowrFilter.RoleInParent]: ((e: FlowrSearchElement, { roleInParent }) => { + return e.node.info.role === roleInParent; + }) satisfies FlowrFilterFunction<{ roleInParent: RoleInParent }>, } as const; export type FlowrFilterArgs = typeof FlowrFilters[F] extends FlowrFilterFunction ? Args : never; export interface MatchesEnrichmentArgs { - enrichment: E, - test: RegExp + enrichment: E, + /** + * The object to test the enrichment value against, which should be a partial {@link EnrichmentElementContent} with each value to test for replaced by a {@link RegExp} or value to match against. The test will pass if the partial structure matches and the enrichment value at each {@link RegExp}, string or primitive location matches the corresponding regular expression. For array entries, {@link arrayMatch} determines whether every element in the array has to match the given expected value, or only some. + */ + test: Record, + /** + * For array entries, the expected value in {@link test} is compared against each array entry in the real value. This property determines whether every element in the array has to match, or only some. If unset, this defaults to `some`. + */ + arrayMatch?: 'some' | 'every' } export interface OriginKindArgs { origin: BuiltInProcName | RegExp; @@ -110,7 +159,7 @@ interface BooleanUnaryNode { type LeafRType = { readonly type: 'r-type', readonly value: RType }; type LeafVertexType = { readonly type: 'vertex-type', readonly value: VertexType }; -type LeafSpecial = { readonly type: 'special', readonly value: FlowrFilterName | FlowrFilterWithArgs> }; +type LeafSpecial = { readonly type: 'special', readonly value: FlowrFilterName | FlowrFilterWithArgs> }; type Leaf = LeafRType | LeafVertexType | LeafSpecial; @@ -134,13 +183,13 @@ export class FlowrFilterCombinator { this.tree = this.unpack(init); } - public static is(value: BooleanNodeOrCombinator | ValidFilterTypes): FlowrFilterCombinator { + public static is(value: BooleanNodeOrCombinator | ValidFilterTypes): FlowrFilterCombinator { if(typeof value === 'string' && ValidFlowrFilters.has(value)) { return new this({ type: 'special', value: value as FlowrFilter }); } else if(typeof value === 'object') { - const name = (value as FlowrFilterWithArgs>)?.name; + const name = (value as FlowrFilterWithArgs>)?.name; if(name && ValidFlowrFilters.has(name)) { - return new this({ type: 'special', value: value as FlowrFilterWithArgs> }); + return new this({ type: 'special', value: value as FlowrFilterWithArgs> } as LeafSpecial); } else { return new this(value as BooleanNodeOrCombinator); } @@ -153,19 +202,19 @@ export class FlowrFilterCombinator { } } - public and(right: BooleanNodeOrCombinator | ValidFilterTypes): this { + public and(right: BooleanNodeOrCombinator | ValidFilterTypes): this { return this.binaryRight('and', right); } - public or(right: BooleanNodeOrCombinator | ValidFilterTypes): this { + public or(right: BooleanNodeOrCombinator | ValidFilterTypes): this { return this.binaryRight('or', right); } - public xor(right: BooleanNodeOrCombinator | ValidFilterTypes): this { + public xor(right: BooleanNodeOrCombinator | ValidFilterTypes): this { return this.binaryRight('xor', right); } - private binaryRight(op: BooleanBinaryNode['type'], right: BooleanNodeOrCombinator | ValidFilterTypes): this { + private binaryRight(op: BooleanBinaryNode['type'], right: BooleanNodeOrCombinator | ValidFilterTypes): this { this.tree = { type: op, left: this.tree, diff --git a/src/search/search-executor/search-enrichers.ts b/src/search/search-executor/search-enrichers.ts index 8f35df47e63..1c37462a17e 100644 --- a/src/search/search-executor/search-enrichers.ts +++ b/src/search/search-executor/search-enrichers.ts @@ -12,7 +12,7 @@ import { type NodeId, recoverName } from '../../r-bridge/lang-4.x/ast/model/proc import type { ControlFlowInformation } from '../../control-flow/control-flow-graph'; import type { Query, QueryResult } from '../../queries/query'; import { type CfgSimplificationPassName, cfgFindAllReachable, DefaultCfgSimplificationOrder } from '../../control-flow/cfg-simplification'; -import type { AsyncOrSync, AsyncOrSyncType } from 'ts-essentials'; +import type { AsyncOrSync, DeepWritable } from 'ts-essentials'; import type { ReadonlyFlowrAnalysisProvider } from '../../project/flowr-analyzer'; import { promoteCallName } from '../../queries/catalog/call-context-query/call-context-query-executor'; import { CfgKind } from '../../project/cfg-kind'; @@ -21,6 +21,8 @@ import { } from '../../queries/catalog/call-context-query/identify-link-to-last-call-relation'; import { Identifier } from '../../dataflow/environments/identifier'; import { Dataflow } from '../../dataflow/graph/df-helper'; +import type { KnownRoxygenTags, RoxygenTag } from '../../r-bridge/roxygen2/roxygen-ast'; +import { getDocumentationOf } from '../../r-bridge/roxygen2/documentation-provider'; export interface EnrichmentData { @@ -47,6 +49,7 @@ export enum Enrichment { CallTargets = 'call-targets', LastCall = 'last-call', CfgInformation = 'cfg-information', + Roxygen = 'roxygen', QueryData = 'query-data' } @@ -93,11 +96,15 @@ export interface CfgInformationArguments extends MergeableRecord { checkReachable?: boolean } +export interface RoxygenElementContent extends MergeableRecord { + documentation: readonly RoxygenTag[] + tags: { [T in KnownRoxygenTags]?: readonly (RoxygenTag & { type: T })[] } +} + export interface QueryDataElementContent extends MergeableRecord { /** The name of the query that this element originated from. To get each query's data, see {@link QueryDataSearchContent}. */ query: Query['type'] } - export interface QueryDataSearchContent extends MergeableRecord { queries: { [QueryType in Query['type']]: Awaited> } } @@ -208,7 +215,28 @@ export const Enrichments = { } return content; } - } satisfies EnrichmentData>, + } satisfies EnrichmentData, + [Enrichment.Roxygen]: { + enrichElement: async(e, _search, analyzer, _args, prev) => { + const content = (prev ?? { + documentation: [], + tags: {} + }) as DeepWritable; + + const normalize = await analyzer.normalize(); + const roxygen = getDocumentationOf(e.node.info.id, normalize.idMap); + if(roxygen !== undefined) { + const comments = (Array.isArray(roxygen) ? roxygen : [roxygen]) as RoxygenTag[]; + content.documentation.push(...comments); + for(const comment of comments) { + content.tags[comment.type] ??= []; + (content.tags[comment.type] as RoxygenTag[]).push(comment); + } + } + + return content; + } + } satisfies EnrichmentData, [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, diff --git a/test/functionality/linter/lint-dead-code.test.ts b/test/functionality/linter/lint-dead-code.test.ts index 86a278e1640..0e38ebbf96d 100644 --- a/test/functionality/linter/lint-dead-code.test.ts +++ b/test/functionality/linter/lint-dead-code.test.ts @@ -11,11 +11,11 @@ describe('flowR linter', withTreeSitter(parser => { assertLinter('none', parser, 'x <- 1', 'dead-code', []); assertLinter('always', parser, 'if(TRUE) 1 else 2', 'dead-code', [ { certainty: LintingResultCertainty.Certain, loc: [1, 17, 1, 17] } - ], { consideredNodes: 7 }); + ]); assertLinter('never', parser, 'if(FALSE) 1 else 2', 'dead-code', [ { certainty: LintingResultCertainty.Certain, loc: [1, 11, 1, 11] } - ], { consideredNodes: 7 }); - assertLinter('no analysis', parser, 'if(FALSE) 1 else 2', 'dead-code', [], { consideredNodes: 7 }, { simplificationPasses: DefaultCfgSimplificationOrder }); + ]); + assertLinter('no analysis', parser, 'if(FALSE) 1 else 2', 'dead-code', [], undefined, { simplificationPasses: DefaultCfgSimplificationOrder }); }); describe('stop', () => { diff --git a/test/functionality/search/search-line.test.ts b/test/functionality/search/search-line.test.ts index 5151a4316cf..3fbccaf0145 100644 --- a/test/functionality/search/search-line.test.ts +++ b/test/functionality/search/search-line.test.ts @@ -44,19 +44,25 @@ describe('flowR search', withTreeSitter(parser => { assertSearch('call-targets (none)', parser, "cat('hello')\nprint('world')", [], Q.all().filter({ name: FlowrFilter.MatchesEnrichment, args: { enrichment: Enrichment.CallTargets, - test: /print/ + test: { + targets: /library/ + } } }) ); assertSearch('call-targets (other)', parser, "cat('hello')\nprint('world')", [], Q.all().with(Enrichment.CallTargets).filter({ name: FlowrFilter.MatchesEnrichment, args: { enrichment: Enrichment.CallTargets, - test: /library/ + test: { + targets: /library/ + } } }) ); assertSearch('call-targets (match)', parser, "cat('hello')\nprint('world')", ['2@print'], Q.all().with(Enrichment.CallTargets).filter({ name: FlowrFilter.MatchesEnrichment, args: { enrichment: Enrichment.CallTargets, - test: /print/ + test: { + targets: /print/ + } } }) ); }); @@ -136,6 +142,20 @@ describe('flowR search', withTreeSitter(parser => { Q.all().with(Enrichment.CallTargets).map(Mapper.Enrichment, Enrichment.CallTargets).select(0), Q.all().to(Enrichment.CallTargets).select(0), ); + assertSearch('local multiple', parser, 'f1 <- function() {}\nf2 <- function() {}\n f1(); f2()', ['1@function'], + Q.all().with(Enrichment.CallTargets).filter({ name: FlowrFilter.MatchesEnrichment, args: { + enrichment: Enrichment.CallTargets, + test: { + targets: { + node: { + info: { + id: 4 + } + } + } + } + } }).map(Mapper.Enrichment, Enrichment.CallTargets) + ); assertSearchEnrichment('global', parser, 'cat("hello")', [{ [Enrichment.CallTargets]: { targets: ['cat'] } }], 'some', Q.all().with(Enrichment.CallTargets)); assertSearchEnrichment('global specific', parser, 'cat("hello")', [{ [Enrichment.CallTargets]: { targets: ['cat'] } }], 'every', Q.all().with(Enrichment.CallTargets).select(1)); // as built-in call target enrichments are not nodes, we don't return them as part of the mapper! @@ -158,25 +178,33 @@ describe('flowR search', withTreeSitter(parser => { assertSearch('reachable always', parser, 'if(TRUE) 1 else 2', ['1@if', '1@TRUE', '1@1', '$2', '$6'], Q.all().with(Enrichment.CfgInformation, cfgArgs).filter({ name: FlowrFilter.MatchesEnrichment, args: { enrichment: Enrichment.CfgInformation, - test: /"isReachable":true/ + test: { + isReachable: true + } } })); assertSearch('reachable never', parser, 'if(FALSE) 1 else 2', ['1@if', '1@FALSE', '1@2', '$4', '$6'], Q.all().with(Enrichment.CfgInformation, cfgArgs).filter({ name: FlowrFilter.MatchesEnrichment, args: { enrichment: Enrichment.CfgInformation, - test: /"isReachable":true/ + test: { + isReachable: /true/ + } } })); assertSearch('reachable no dead code', parser, 'if(FALSE) 1 else 2', [], Q.all().with(Enrichment.CfgInformation).filter({ name: FlowrFilter.MatchesEnrichment, args: { enrichment: Enrichment.CfgInformation, - test: /"isReachable":false/ + test: { + isReachable: false + } } })); assertSearch('reachable no reachable', parser, 'if(FALSE) 1 else 2', [], Q.all().with(Enrichment.CfgInformation).filter({ name: FlowrFilter.MatchesEnrichment, args: { enrichment: Enrichment.CfgInformation, - test: /"isReachable":false/ + test: { + isReachable: /false/ + } } })); });