Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions src/linter/rules/seeded-randomness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ import type { BuiltInFunctionDefinition } from '../../dataflow/environments/buil
import { resolveIdToValue } from '../../dataflow/eval/resolve/alias-tracking';
import { valueSetGuard } from '../../dataflow/eval/values/general';
import { VariableResolve } from '../../config';
import type { DataflowGraphVertexFunctionCall } from '../../dataflow/graph/vertex';
import type { DataflowGraphVertexFunctionCall, DataflowGraphVertexInfo } from '../../dataflow/graph/vertex';
import { EmptyArgument } from '../../r-bridge/lang-4.x/ast/model/nodes/r-function-call';
import { asValue } from '../../dataflow/eval/values/r-value';
import { happensInEveryBranch } from '../../dataflow/info';

export interface SeededRandomnessResult extends LintingResult {
function: string
Expand Down Expand Up @@ -80,14 +81,15 @@ export const SEEDED_RANDOMNESS = {
}))
// filter by calls that aren't preceded by a randomness producer
.filter(element => {
const dfgElement = dataflow.graph.getVertex(element.searchElement.node.info.id) as DataflowGraphVertexInfo;
const producers = 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(f.name) ? 'assignment' : 'func');
let nonConstant = false;

// function calls are already taken care of through the LastCall enrichment itself
for(const f of func ?? []) {
if(isConstantArgument(dataflow.graph, f, 0)) {
if(isConstantArgument(dataflow.graph, f, 0) && (dfgElement.cds === f.cds || happensInEveryBranch(f.cds))) {
metadata.callsWithFunctionProducers++;
return false;
} else {
Expand All @@ -100,7 +102,7 @@ export const SEEDED_RANDOMNESS = {
const argIdx = assignmentArgIndexes.get(a.name) as number;
const dest = getReferenceOfArgument(a.args[argIdx]);
if(dest !== undefined && assignmentProducers.has(recoverName(dest, dataflow.graph.idMap) as string)){
if(isConstantArgument(dataflow.graph, a, 1-argIdx)) {
if(isConstantArgument(dataflow.graph, a, 1-argIdx) && (dfgElement.cds === a.cds || happensInEveryBranch(a.cds))) {
metadata.callsWithAssignmentProducers++;
return false;
} else {
Expand Down
10 changes: 5 additions & 5 deletions src/search/search-executor/search-enrichers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
identifyLinkToLastCallRelation
} from '../../queries/catalog/call-context-query/identify-link-to-last-call-relation';
import { guard, isNotUndefined } from '../../util/assert';
import { extractCfgQuick } from '../../control-flow/extract-cfg';
import { getOriginInDfg, OriginType } from '../../dataflow/origin/dfg-get-origin';
import { type NodeId, recoverName } from '../../r-bridge/lang-4.x/ast/model/processing/node-id';
import type { ControlFlowInformation } from '../../control-flow/control-flow-graph';
Expand All @@ -23,13 +22,14 @@ import type { ReadonlyFlowrAnalysisProvider } from '../../project/flowr-analyzer
import type { DataflowInformation } from '../../dataflow/info';
import { promoteCallName } from '../../queries/catalog/call-context-query/call-context-query-executor';
import { CfgKind } from '../../project/cfg-kind';
import type { FlowrConfigOptions } from '../../config';


export interface EnrichmentData<ElementContent extends MergeableRecord, ElementArguments = undefined, SearchContent extends MergeableRecord = never, SearchArguments = ElementArguments> {
/**
* A function that is applied to each element of the search to enrich it with additional data.
*/
readonly enrichElement?: (element: FlowrSearchElement<ParentInformation>, search: FlowrSearchElements<ParentInformation>, data: {dataflow: DataflowInformation, normalize: NormalizedAst, cfg: ControlFlowInformation}, args: ElementArguments | undefined, previousValue: ElementContent | undefined) => AsyncOrSync<ElementContent>
readonly enrichElement?: (element: FlowrSearchElement<ParentInformation>, search: FlowrSearchElements<ParentInformation>, data: {dataflow: DataflowInformation, normalize: NormalizedAst, cfg: ControlFlowInformation, config: FlowrConfigOptions}, args: ElementArguments | undefined, previousValue: ElementContent | undefined) => AsyncOrSync<ElementContent>
readonly enrichSearch?: (search: FlowrSearchElements<ParentInformation>, data: ReadonlyFlowrAnalysisProvider, args: SearchArguments | undefined, previousValue: SearchContent | undefined) => AsyncOrSync<SearchContent>
/**
* The mapping function used by the {@link Mapper.Enrichment} mapper.
Expand Down Expand Up @@ -159,9 +159,8 @@ export const Enrichments = {
const content = prev ?? { linkedIds: [] };
const vertex = data.dataflow.graph.get(e.node.info.id);
if(vertex !== undefined && vertex[0].tag === VertexType.FunctionCall) {
const cfg = extractCfgQuick(data.normalize);
for(const arg of args) {
const lastCalls = identifyLinkToLastCallRelation(vertex[0].id, cfg.graph, data.dataflow.graph, {
const lastCalls = identifyLinkToLastCallRelation(vertex[0].id, data.cfg.graph, data.dataflow.graph, {
...arg,
callName: promoteCallName(arg.callName),
type: 'link-to-last-call',
Expand Down Expand Up @@ -231,7 +230,8 @@ export async function enrichElement<Element extends FlowrSearchElement<ParentInf
e: Element, s: FlowrSearchElements<ParentInformation>, data: {
dataflow: DataflowInformation,
normalize: NormalizedAst,
cfg: ControlFlowInformation
cfg: ControlFlowInformation,
config: FlowrConfigOptions
}, enrichment: E, args?: EnrichmentElementArguments<E>): Promise<Element> {
const enrichmentData = Enrichments[enrichment] as unknown as EnrichmentData<EnrichmentElementContent<E>, EnrichmentElementArguments<E>>;
const prev = e?.enrichments;
Expand Down
2 changes: 1 addition & 1 deletion src/search/search-executor/search-generators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ async function generateFromQuery(input: ReadonlyFlowrAnalysisProvider, args: {
.enrich(input, Enrichment.QueryData, { queries: result });
return elements.mutate(s => Promise.all(s.map(async e => {
const [query, _] = [...nodesByQuery].find(([_, nodes]) => nodes.has(e)) as [Query['type'], Set<FlowrSearchElement<ParentInformation>>];
return await enrichElement(e, elements, { normalize, dataflow, cfg }, Enrichment.QueryData, { query });
return await enrichElement(e, elements, { normalize, dataflow, cfg, config: input.flowrConfig }, Enrichment.QueryData, { query });
}))) as unknown as FlowrSearchElements<ParentInformation, FlowrSearchElement<ParentInformation>[]>;
}

Expand Down
7 changes: 7 additions & 0 deletions test/functionality/linter/lint-seeded-randomness.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ describe('flowR linter', withTreeSitter(parser => {
assertLinter('condition', parser, 'if(FALSE) { set.seed(17); }\nrunif(1);', 'seeded-randomness',
[{ range: [2,1,2,8], function: 'runif', certainty: LintingResultCertainty.Certain }], { consumerCalls: 1, callsWithFunctionProducers: 0, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0 });
assertLinter('condition true', parser, 'if(TRUE) { set.seed(17); }\nrunif(1);', 'seeded-randomness', [], { consumerCalls: 1, callsWithFunctionProducers: 1, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0 });
assertLinter('condition unclear', parser, 'if(1 < 0) { set.seed(17); }\nrunif(1);', 'seeded-randomness',
[{ range: [2,1,2,8], function: 'runif', certainty: LintingResultCertainty.Certain }], { consumerCalls: 1, callsWithFunctionProducers: 0, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 1 });
assertLinter('condition unclear after definite seed', parser, 'set.seed(17); if(1 < 0) { set.seed(17); }\nrunif(1);', 'seeded-randomness', [], { consumerCalls: 1, callsWithFunctionProducers: 1, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0 });
assertLinter('condition exhaustive', parser, 'if(1 < 0) { set.seed(17); } else { set.seed(18); }\nrunif(1);', 'seeded-randomness', [], { consumerCalls: 1, callsWithFunctionProducers: 1, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0 });
assertLinter('condition reversed', parser, 'set.seed(17);\nif(1 < 0) { runif(1) }', 'seeded-randomness', [], { consumerCalls: 1, callsWithFunctionProducers: 1, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0 });
assertLinter('separate conditions', parser, 'if (2 < 1) { set.seed(17) }; if (1 < 0) { runif(1) }', 'seeded-randomness',
[{ range: [1,43,1,50],function: 'runif', certainty: LintingResultCertainty.Certain }], { consumerCalls: 1, callsWithFunctionProducers: 0, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 1 });

assertLinter('non-constant seed', parser, 'num<-1 + 7;\nset.seed(num);\nrunif(1);', 'seeded-randomness', [
{ range: [3,1,3,8], function: 'runif', certainty: LintingResultCertainty.Certain }
Expand Down