From b0b5e24a659d045b8d9a0c6d4f680c8e6c285e33 Mon Sep 17 00:00:00 2001 From: Ellpeck Date: Tue, 11 Nov 2025 13:55:58 +0100 Subject: [PATCH 01/10] feat: also give enrichers access to the flowr config --- src/search/search-executor/search-enrichers.ts | 6 ++++-- src/search/search-executor/search-generators.ts | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/search/search-executor/search-enrichers.ts b/src/search/search-executor/search-enrichers.ts index fd7f99570a1..1b7995b0301 100644 --- a/src/search/search-executor/search-enrichers.ts +++ b/src/search/search-executor/search-enrichers.ts @@ -23,13 +23,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 { /** * A function that is applied to each element of the search to enrich it with additional data. */ - readonly enrichElement?: (element: FlowrSearchElement, search: FlowrSearchElements, data: {dataflow: DataflowInformation, normalize: NormalizedAst, cfg: ControlFlowInformation}, args: ElementArguments | undefined, previousValue: ElementContent | undefined) => AsyncOrSync + readonly enrichElement?: (element: FlowrSearchElement, search: FlowrSearchElements, data: {dataflow: DataflowInformation, normalize: NormalizedAst, cfg: ControlFlowInformation, config: FlowrConfigOptions}, args: ElementArguments | undefined, previousValue: ElementContent | undefined) => AsyncOrSync readonly enrichSearch?: (search: FlowrSearchElements, data: ReadonlyFlowrAnalysisProvider, args: SearchArguments | undefined, previousValue: SearchContent | undefined) => AsyncOrSync /** * The mapping function used by the {@link Mapper.Enrichment} mapper. @@ -231,7 +232,8 @@ export async function enrichElement, data: { dataflow: DataflowInformation, normalize: NormalizedAst, - cfg: ControlFlowInformation + cfg: ControlFlowInformation, + config: FlowrConfigOptions }, enrichment: E, args?: EnrichmentElementArguments): Promise { const enrichmentData = Enrichments[enrichment] as unknown as EnrichmentData, EnrichmentElementArguments>; const prev = e?.enrichments; diff --git a/src/search/search-executor/search-generators.ts b/src/search/search-executor/search-generators.ts index 0f1409e02f0..2a532db3081 100644 --- a/src/search/search-executor/search-generators.ts +++ b/src/search/search-executor/search-generators.ts @@ -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>]; - 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[]>; } From e221cfba5d6fdb9b628048e293e7459d28f3ed43 Mon Sep 17 00:00:00 2001 From: Ellpeck Date: Tue, 11 Nov 2025 14:08:52 +0100 Subject: [PATCH 02/10] refactor: reuse the existing cfg for last-call --- src/search/search-executor/search-enrichers.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/search/search-executor/search-enrichers.ts b/src/search/search-executor/search-enrichers.ts index 1b7995b0301..f9d81a1364e 100644 --- a/src/search/search-executor/search-enrichers.ts +++ b/src/search/search-executor/search-enrichers.ts @@ -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'; @@ -160,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', From 130fedf84bdb6af1632ff684c4b2fa7f148e01bc Mon Sep 17 00:00:00 2001 From: Ellpeck Date: Tue, 11 Nov 2025 14:54:52 +0100 Subject: [PATCH 03/10] feat-fix: account for branch coverage in seeded randomness rule --- src/linter/rules/seeded-randomness.ts | 8 +++++--- test/functionality/linter/lint-seeded-randomness.test.ts | 5 +++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/linter/rules/seeded-randomness.ts b/src/linter/rules/seeded-randomness.ts index 265844ecfdb..aafb17a3db4 100644 --- a/src/linter/rules/seeded-randomness.ts +++ b/src/linter/rules/seeded-randomness.ts @@ -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 @@ -80,6 +81,7 @@ export const SEEDED_RANDOMNESS = { })) // filter by calls that aren't preceded by a randomness producer .filter(element => { + const consumerAlwaysHappens = happensInEveryBranch((dataflow.graph.getVertex(element.searchElement.node.info.id) as DataflowGraphVertexInfo).cds); 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'); @@ -87,7 +89,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)) { + if(isConstantArgument(dataflow.graph, f, 0) && (!consumerAlwaysHappens || happensInEveryBranch(f.cds))) { metadata.callsWithFunctionProducers++; return false; } else { @@ -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) && (!consumerAlwaysHappens || happensInEveryBranch(a.cds))) { metadata.callsWithAssignmentProducers++; return false; } else { diff --git a/test/functionality/linter/lint-seeded-randomness.test.ts b/test/functionality/linter/lint-seeded-randomness.test.ts index 03480fd1de8..3730905e995 100644 --- a/test/functionality/linter/lint-seeded-randomness.test.ts +++ b/test/functionality/linter/lint-seeded-randomness.test.ts @@ -18,6 +18,11 @@ 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('non-constant seed', parser, 'num<-1 + 7;\nset.seed(num);\nrunif(1);', 'seeded-randomness', [ { range: [3,1,3,8], function: 'runif', certainty: LintingResultCertainty.Certain } From e91227a9986ae8614cb19c7d59365268a70620fe Mon Sep 17 00:00:00 2001 From: Ellpeck Date: Tue, 11 Nov 2025 14:58:49 +0100 Subject: [PATCH 04/10] test: add a test that doesn't work lol --- test/functionality/linter/lint-seeded-randomness.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/functionality/linter/lint-seeded-randomness.test.ts b/test/functionality/linter/lint-seeded-randomness.test.ts index 3730905e995..1513913fa56 100644 --- a/test/functionality/linter/lint-seeded-randomness.test.ts +++ b/test/functionality/linter/lint-seeded-randomness.test.ts @@ -23,6 +23,8 @@ describe('flowR linter', withTreeSitter(parser => { 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,44,1,51],function: 'runif', certainty: LintingResultCertainty.Certain }], { consumerCalls: 1, callsWithFunctionProducers: 0, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0 }); 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 } From 4e89a579708ce6d7bc143dd72d2298b9762d809d Mon Sep 17 00:00:00 2001 From: Ellpeck Date: Tue, 11 Nov 2025 15:17:54 +0100 Subject: [PATCH 05/10] feat-fix: this seems even worse ngl --- src/linter/rules/seeded-randomness.ts | 6 +++--- test/functionality/linter/lint-seeded-randomness.test.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/linter/rules/seeded-randomness.ts b/src/linter/rules/seeded-randomness.ts index aafb17a3db4..1980344217e 100644 --- a/src/linter/rules/seeded-randomness.ts +++ b/src/linter/rules/seeded-randomness.ts @@ -81,7 +81,7 @@ export const SEEDED_RANDOMNESS = { })) // filter by calls that aren't preceded by a randomness producer .filter(element => { - const consumerAlwaysHappens = happensInEveryBranch((dataflow.graph.getVertex(element.searchElement.node.info.id) as DataflowGraphVertexInfo).cds); + 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'); @@ -89,7 +89,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) && (!consumerAlwaysHappens || happensInEveryBranch(f.cds))) { + if(isConstantArgument(dataflow.graph, f, 0) && (dfgElement.cds === f.cds || happensInEveryBranch(f.cds))) { metadata.callsWithFunctionProducers++; return false; } else { @@ -102,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) && (!consumerAlwaysHappens || happensInEveryBranch(a.cds))) { + if(isConstantArgument(dataflow.graph, a, 1-argIdx) && (dfgElement.cds === a.cds || happensInEveryBranch(a.cds))) { metadata.callsWithAssignmentProducers++; return false; } else { diff --git a/test/functionality/linter/lint-seeded-randomness.test.ts b/test/functionality/linter/lint-seeded-randomness.test.ts index 1513913fa56..f4fd10a4823 100644 --- a/test/functionality/linter/lint-seeded-randomness.test.ts +++ b/test/functionality/linter/lint-seeded-randomness.test.ts @@ -24,7 +24,7 @@ describe('flowR linter', withTreeSitter(parser => { 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,44,1,51],function: 'runif', certainty: LintingResultCertainty.Certain }], { consumerCalls: 1, callsWithFunctionProducers: 0, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0 }); + [{ 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 } From 417269bf7058b1ca5094b84edfe96141d044a6e0 Mon Sep 17 00:00:00 2001 From: Ellpeck Date: Fri, 21 Nov 2025 12:42:13 +0100 Subject: [PATCH 06/10] refactor: some improvements based on flos opinions --- src/linter/rules/seeded-randomness.ts | 8 ++++---- .../linter/lint-seeded-randomness.test.ts | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/linter/rules/seeded-randomness.ts b/src/linter/rules/seeded-randomness.ts index 1980344217e..a58f0f373b7 100644 --- a/src/linter/rules/seeded-randomness.ts +++ b/src/linter/rules/seeded-randomness.ts @@ -15,7 +15,7 @@ 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, DataflowGraphVertexInfo } from '../../dataflow/graph/vertex'; +import type { DataflowGraphVertexFunctionCall } 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'; @@ -81,7 +81,7 @@ 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 dfgElement = dataflow.graph.getVertex(element.searchElement.node.info.id); 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'); @@ -89,7 +89,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) && (dfgElement.cds === f.cds || happensInEveryBranch(f.cds))) { + if(isConstantArgument(dataflow.graph, f, 0) && (!dfgElement || dfgElement.cds === f.cds || happensInEveryBranch(f.cds))) { metadata.callsWithFunctionProducers++; return false; } else { @@ -102,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) && (dfgElement.cds === a.cds || happensInEveryBranch(a.cds))) { + if(isConstantArgument(dataflow.graph, a, 1-argIdx) && (!dfgElement || dfgElement.cds === a.cds || happensInEveryBranch(a.cds))) { metadata.callsWithAssignmentProducers++; return false; } else { diff --git a/test/functionality/linter/lint-seeded-randomness.test.ts b/test/functionality/linter/lint-seeded-randomness.test.ts index f4fd10a4823..2b4ce784b8f 100644 --- a/test/functionality/linter/lint-seeded-randomness.test.ts +++ b/test/functionality/linter/lint-seeded-randomness.test.ts @@ -18,13 +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', + assertLinter('condition unclear', parser, 'if(u) { 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('condition unclear after definite seed', parser, 'set.seed(17); if(u) { set.seed(17); }\nrunif(1);', 'seeded-randomness', [], { consumerCalls: 1, callsWithFunctionProducers: 1, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0 }); + assertLinter('condition exhaustive', parser, 'if(u) { 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(u) { runif(1) }', 'seeded-randomness', [], { consumerCalls: 1, callsWithFunctionProducers: 1, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0 }); + assertLinter('separate conditions', parser, 'if (u) { set.seed(17) }; if (u) { runif(1) }', 'seeded-randomness', + [{ range: [1,35,1,42],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 } From 18fad275048886429138872aa4ecdbfa14a423fb Mon Sep 17 00:00:00 2001 From: Ellpeck Date: Fri, 21 Nov 2025 12:54:02 +0100 Subject: [PATCH 07/10] feat-fix: property test for control dependency overlap --- src/linter/rules/seeded-randomness.ts | 39 +++++++++++----- .../linter/lint-seeded-randomness.test.ts | 44 +++++++++---------- 2 files changed, 50 insertions(+), 33 deletions(-) diff --git a/src/linter/rules/seeded-randomness.ts b/src/linter/rules/seeded-randomness.ts index a58f0f373b7..59505a4229d 100644 --- a/src/linter/rules/seeded-randomness.ts +++ b/src/linter/rules/seeded-randomness.ts @@ -39,10 +39,11 @@ export interface SeededRandomnessConfig extends MergeableRecord { } export interface SeededRandomnessMeta extends MergeableRecord { - consumerCalls: number - callsWithFunctionProducers: number - callsWithAssignmentProducers: number - callsWithNonConstantProducers: number + consumerCalls: number + callsWithFunctionProducers: number + callsWithAssignmentProducers: number + callsWithNonConstantProducers: number + callsWithOtherBranchProducers: number } export const SEEDED_RANDOMNESS = { @@ -66,7 +67,8 @@ export const SEEDED_RANDOMNESS = { consumerCalls: 0, callsWithFunctionProducers: 0, callsWithAssignmentProducers: 0, - callsWithNonConstantProducers: 0 + callsWithNonConstantProducers: 0, + callsWithOtherBranchProducers: 0 }; return { results: elements.getElements() @@ -82,16 +84,23 @@ 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); + const cds = dfgElement ? new Set(dfgElement.cds) : new Set(); 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; + let otherBranch = false; // function calls are already taken care of through the LastCall enrichment itself for(const f of func ?? []) { - if(isConstantArgument(dataflow.graph, f, 0) && (!dfgElement || dfgElement.cds === f.cds || happensInEveryBranch(f.cds))) { - metadata.callsWithFunctionProducers++; - return false; + if(isConstantArgument(dataflow.graph, f, 0)) { + const fCds = new Set(f.cds).difference(cds); + if(fCds.size <= 0 || happensInEveryBranch([...fCds])){ + metadata.callsWithFunctionProducers++; + return false; + } else { + otherBranch = true; + } } else { nonConstant = true; } @@ -102,9 +111,14 @@ 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) && (!dfgElement || dfgElement.cds === a.cds || happensInEveryBranch(a.cds))) { - metadata.callsWithAssignmentProducers++; - return false; + if(isConstantArgument(dataflow.graph, a, 1-argIdx)) { + const aCds = new Set(a.cds).difference(cds); + if(aCds.size <= 0 || happensInEveryBranch([...aCds])) { + metadata.callsWithAssignmentProducers++; + return false; + } else { + otherBranch = true; + } } else { nonConstant = true; } @@ -114,6 +128,9 @@ export const SEEDED_RANDOMNESS = { if(nonConstant) { metadata.callsWithNonConstantProducers++; } + if(otherBranch) { + metadata.callsWithOtherBranchProducers++; + } return true; }) diff --git a/test/functionality/linter/lint-seeded-randomness.test.ts b/test/functionality/linter/lint-seeded-randomness.test.ts index 2b4ce784b8f..66f18818224 100644 --- a/test/functionality/linter/lint-seeded-randomness.test.ts +++ b/test/functionality/linter/lint-seeded-randomness.test.ts @@ -5,50 +5,50 @@ import { LintingResultCertainty } from '../../../src/linter/linter-format'; describe('flowR linter', withTreeSitter(parser => { describe('R3 seeded randomness', () => { - assertLinter('none', parser, 'cat("hello")', 'seeded-randomness', [], { consumerCalls: 0, callsWithFunctionProducers: 0, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0 }); + assertLinter('none', parser, 'cat("hello")', 'seeded-randomness', [], { consumerCalls: 0, callsWithFunctionProducers: 0, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); assertLinter('simple no producer', parser, 'runif(1)', 'seeded-randomness', - [{ range: [1,1,1,8], function: 'runif', certainty: LintingResultCertainty.Certain }], { consumerCalls: 1, callsWithFunctionProducers: 0, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0 }); - assertLinter('simple no consumer', parser, 'set.seed(17)', 'seeded-randomness', [], { consumerCalls: 0, callsWithFunctionProducers: 0, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0 }); - assertLinter('simple both', parser, 'set.seed(17)\nrunif(1)', 'seeded-randomness', [], { consumerCalls: 1, callsWithFunctionProducers: 1, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0 }); + [{ range: [1,1,1,8], function: 'runif', certainty: LintingResultCertainty.Certain }], { consumerCalls: 1, callsWithFunctionProducers: 0, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); + assertLinter('simple no consumer', parser, 'set.seed(17)', 'seeded-randomness', [], { consumerCalls: 0, callsWithFunctionProducers: 0, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); + assertLinter('simple both', parser, 'set.seed(17)\nrunif(1)', 'seeded-randomness', [], { consumerCalls: 1, callsWithFunctionProducers: 1, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); assertLinter('simple after', parser, 'runif(1)\nset.seed(17)', 'seeded-randomness', - [{ range: [1,1,1,8], function: 'runif', certainty: LintingResultCertainty.Certain }], { consumerCalls: 1, callsWithFunctionProducers: 0, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0 }); + [{ range: [1,1,1,8], function: 'runif', certainty: LintingResultCertainty.Certain }], { consumerCalls: 1, callsWithFunctionProducers: 0, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); - assertLinter('loop valid', parser, 'for(i in 1:10) { set.seed(17); runif(1); }', 'seeded-randomness', [], { consumerCalls: 1, callsWithFunctionProducers: 1, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0 }); + assertLinter('loop valid', parser, 'for(i in 1:10) { set.seed(17); runif(1); }', 'seeded-randomness', [], { consumerCalls: 1, callsWithFunctionProducers: 1, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); 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 }); + [{ range: [2,1,2,8], function: 'runif', certainty: LintingResultCertainty.Certain }], { consumerCalls: 1, callsWithFunctionProducers: 0, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); + assertLinter('condition true', parser, 'if(TRUE) { set.seed(17); }\nrunif(1);', 'seeded-randomness', [], { consumerCalls: 1, callsWithFunctionProducers: 1, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); assertLinter('condition unclear', parser, 'if(u) { 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(u) { set.seed(17); }\nrunif(1);', 'seeded-randomness', [], { consumerCalls: 1, callsWithFunctionProducers: 1, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0 }); - assertLinter('condition exhaustive', parser, 'if(u) { 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(u) { runif(1) }', 'seeded-randomness', [], { consumerCalls: 1, callsWithFunctionProducers: 1, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0 }); + [{ range: [2,1,2,8], function: 'runif', certainty: LintingResultCertainty.Certain }], { consumerCalls: 1, callsWithFunctionProducers: 0, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 1 }); + assertLinter('condition unclear after definite seed', parser, 'set.seed(17); if(u) { set.seed(17); }\nrunif(1);', 'seeded-randomness', [], { consumerCalls: 1, callsWithFunctionProducers: 1, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); + assertLinter('condition exhaustive', parser, 'if(u) { set.seed(17); } else { set.seed(18); }\nrunif(1);', 'seeded-randomness', [], { consumerCalls: 1, callsWithFunctionProducers: 1, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); + assertLinter('condition reversed', parser, 'set.seed(17);\nif(u) { runif(1) }', 'seeded-randomness', [], { consumerCalls: 1, callsWithFunctionProducers: 1, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); assertLinter('separate conditions', parser, 'if (u) { set.seed(17) }; if (u) { runif(1) }', 'seeded-randomness', - [{ range: [1,35,1,42],function: 'runif', certainty: LintingResultCertainty.Certain }], { consumerCalls: 1, callsWithFunctionProducers: 0, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 1 }); + [{ range: [1,35,1,42],function: 'runif', certainty: LintingResultCertainty.Certain }], { consumerCalls: 1, callsWithFunctionProducers: 0, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 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 } - ], { consumerCalls: 1, callsWithFunctionProducers: 0, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 1 }); + ], { consumerCalls: 1, callsWithFunctionProducers: 0, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 1, callsWithOtherBranchProducers: 0 }); assertLinter('random seed', parser, 'set.seed(runif(1));\nrunif(1);', 'seeded-randomness', [ { range: [1,10,1,17], function: 'runif', certainty: LintingResultCertainty.Certain }, { range: [2,1,2,8], function: 'runif', certainty: LintingResultCertainty.Certain } - ], { consumerCalls: 2, callsWithFunctionProducers: 0, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 2 }); + ], { consumerCalls: 2, callsWithFunctionProducers: 0, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 2, callsWithOtherBranchProducers: 0 }); - assertLinter('multiple seeds', parser, 'set.seed(1);\nset.seed(2);\nrunif(1);', 'seeded-randomness', [], { consumerCalls: 1, callsWithFunctionProducers: 1, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0 }); - assertLinter('multiple consumers', parser, 'set.seed(1);\nset.seed(2);\nrunif(1);\nrunif(1);', 'seeded-randomness', [], { consumerCalls: 2, callsWithFunctionProducers: 2, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0 }); + assertLinter('multiple seeds', parser, 'set.seed(1);\nset.seed(2);\nrunif(1);', 'seeded-randomness', [], { consumerCalls: 1, callsWithFunctionProducers: 1, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); + assertLinter('multiple consumers', parser, 'set.seed(1);\nset.seed(2);\nrunif(1);\nrunif(1);', 'seeded-randomness', [], { consumerCalls: 2, callsWithFunctionProducers: 2, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); assertLinter('custom set.seed', parser, 'set.seed <- function(x) {}\nset.seed(17)\nrunif(1)', 'seeded-randomness', - [{ range: [3,1,3,8], function: 'runif', certainty: LintingResultCertainty.Certain }], { consumerCalls: 1, callsWithFunctionProducers: 0, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0 }); + [{ range: [3,1,3,8], function: 'runif', certainty: LintingResultCertainty.Certain }], { consumerCalls: 1, callsWithFunctionProducers: 0, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); assertLinter('set .Random.seed', parser, '.Random.seed <- 17\nrunif(1)', 'seeded-randomness', [], - { consumerCalls: 1, callsWithAssignmentProducers: 1, callsWithFunctionProducers: 0, callsWithNonConstantProducers: 0 }); + { consumerCalls: 1, callsWithAssignmentProducers: 1, callsWithFunctionProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); assertLinter('set .Random.seed with assignment inbetween', parser, '.Random.seed <- 17\nx <- 7 \nrunif(1)', 'seeded-randomness', [], - { consumerCalls: 1, callsWithAssignmentProducers: 1, callsWithFunctionProducers: 0, callsWithNonConstantProducers: 0 }); + { consumerCalls: 1, callsWithAssignmentProducers: 1, callsWithFunctionProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); assertLinter('set .Random.seed reverse', parser, '17 -> .Random.seed\nrunif(1)', 'seeded-randomness', [], - { consumerCalls: 1, callsWithAssignmentProducers: 1, callsWithFunctionProducers: 0, callsWithNonConstantProducers: 0 }); + { consumerCalls: 1, callsWithAssignmentProducers: 1, callsWithFunctionProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); assertLinter('set .Random.seed override <-', parser, '`<-`<-function(){}\n.Random.seed <- 17\nrunif(1)', 'seeded-randomness', [{ range: [3,1,3,8], function: 'runif', certainty: LintingResultCertainty.Certain }], - { consumerCalls: 1, callsWithAssignmentProducers: 0, callsWithFunctionProducers: 0, callsWithNonConstantProducers: 0 }); + { consumerCalls: 1, callsWithAssignmentProducers: 0, callsWithFunctionProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); assertLinter('set in function call', parser, 'runif(set.seed(17))', 'seeded-randomness', [{ range: [1,1,1,19], function: 'runif', certainty: LintingResultCertainty.Certain }]); From 1710604039904272e4b96a47389f492d548920a4 Mon Sep 17 00:00:00 2001 From: Ellpeck Date: Fri, 21 Nov 2025 12:56:12 +0100 Subject: [PATCH 08/10] feat: demote unclear branch dependencies to unclear certainty --- src/linter/rules/seeded-randomness.ts | 20 +++++++++---------- .../linter/lint-seeded-randomness.test.ts | 4 ++-- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/linter/rules/seeded-randomness.ts b/src/linter/rules/seeded-randomness.ts index 59505a4229d..a4a6ffba6ab 100644 --- a/src/linter/rules/seeded-randomness.ts +++ b/src/linter/rules/seeded-randomness.ts @@ -82,7 +82,7 @@ export const SEEDED_RANDOMNESS = { }; })) // filter by calls that aren't preceded by a randomness producer - .filter(element => { + .flatMap(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 @@ -97,7 +97,7 @@ export const SEEDED_RANDOMNESS = { const fCds = new Set(f.cds).difference(cds); if(fCds.size <= 0 || happensInEveryBranch([...fCds])){ metadata.callsWithFunctionProducers++; - return false; + return []; } else { otherBranch = true; } @@ -115,7 +115,7 @@ export const SEEDED_RANDOMNESS = { const aCds = new Set(a.cds).difference(cds); if(aCds.size <= 0 || happensInEveryBranch([...aCds])) { metadata.callsWithAssignmentProducers++; - return false; + return []; } else { otherBranch = true; } @@ -131,14 +131,12 @@ export const SEEDED_RANDOMNESS = { if(otherBranch) { metadata.callsWithOtherBranchProducers++; } - return true; - }) - - .map(element => ({ - certainty: LintingResultCertainty.Certain, - function: element.target, - range: element.range - })), + return [{ + certainty: otherBranch ? LintingResultCertainty.Uncertain : LintingResultCertainty.Certain, + function: element.target, + range: element.range + }]; + }), '.meta': metadata }; }, diff --git a/test/functionality/linter/lint-seeded-randomness.test.ts b/test/functionality/linter/lint-seeded-randomness.test.ts index 66f18818224..f79ee7effc5 100644 --- a/test/functionality/linter/lint-seeded-randomness.test.ts +++ b/test/functionality/linter/lint-seeded-randomness.test.ts @@ -19,12 +19,12 @@ describe('flowR linter', withTreeSitter(parser => { [{ range: [2,1,2,8], function: 'runif', certainty: LintingResultCertainty.Certain }], { consumerCalls: 1, callsWithFunctionProducers: 0, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); assertLinter('condition true', parser, 'if(TRUE) { set.seed(17); }\nrunif(1);', 'seeded-randomness', [], { consumerCalls: 1, callsWithFunctionProducers: 1, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); assertLinter('condition unclear', parser, 'if(u) { 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, callsWithOtherBranchProducers: 1 }); + [{ range: [2,1,2,8], function: 'runif', certainty: LintingResultCertainty.Uncertain }], { consumerCalls: 1, callsWithFunctionProducers: 0, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 1 }); assertLinter('condition unclear after definite seed', parser, 'set.seed(17); if(u) { set.seed(17); }\nrunif(1);', 'seeded-randomness', [], { consumerCalls: 1, callsWithFunctionProducers: 1, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); assertLinter('condition exhaustive', parser, 'if(u) { set.seed(17); } else { set.seed(18); }\nrunif(1);', 'seeded-randomness', [], { consumerCalls: 1, callsWithFunctionProducers: 1, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); assertLinter('condition reversed', parser, 'set.seed(17);\nif(u) { runif(1) }', 'seeded-randomness', [], { consumerCalls: 1, callsWithFunctionProducers: 1, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); assertLinter('separate conditions', parser, 'if (u) { set.seed(17) }; if (u) { runif(1) }', 'seeded-randomness', - [{ range: [1,35,1,42],function: 'runif', certainty: LintingResultCertainty.Certain }], { consumerCalls: 1, callsWithFunctionProducers: 0, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 1 }); + [{ range: [1,35,1,42],function: 'runif', certainty: LintingResultCertainty.Uncertain }], { consumerCalls: 1, callsWithFunctionProducers: 0, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 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 } From 2fca589010a5036e56024a3f48219158a61548dd Mon Sep 17 00:00:00 2001 From: Ellpeck Date: Mon, 24 Nov 2025 15:27:03 +0100 Subject: [PATCH 09/10] test: clean up and additional seeded randomness tests --- .../linter/lint-seeded-randomness.test.ts | 82 +++++++++++-------- 1 file changed, 49 insertions(+), 33 deletions(-) diff --git a/test/functionality/linter/lint-seeded-randomness.test.ts b/test/functionality/linter/lint-seeded-randomness.test.ts index f79ee7effc5..56fb3e96051 100644 --- a/test/functionality/linter/lint-seeded-randomness.test.ts +++ b/test/functionality/linter/lint-seeded-randomness.test.ts @@ -5,26 +5,55 @@ import { LintingResultCertainty } from '../../../src/linter/linter-format'; describe('flowR linter', withTreeSitter(parser => { describe('R3 seeded randomness', () => { - assertLinter('none', parser, 'cat("hello")', 'seeded-randomness', [], { consumerCalls: 0, callsWithFunctionProducers: 0, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); - assertLinter('simple no producer', parser, 'runif(1)', 'seeded-randomness', - [{ range: [1,1,1,8], function: 'runif', certainty: LintingResultCertainty.Certain }], { consumerCalls: 1, callsWithFunctionProducers: 0, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); - assertLinter('simple no consumer', parser, 'set.seed(17)', 'seeded-randomness', [], { consumerCalls: 0, callsWithFunctionProducers: 0, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); - assertLinter('simple both', parser, 'set.seed(17)\nrunif(1)', 'seeded-randomness', [], { consumerCalls: 1, callsWithFunctionProducers: 1, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); - assertLinter('simple after', parser, 'runif(1)\nset.seed(17)', 'seeded-randomness', - [{ range: [1,1,1,8], function: 'runif', certainty: LintingResultCertainty.Certain }], { consumerCalls: 1, callsWithFunctionProducers: 0, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); - - assertLinter('loop valid', parser, 'for(i in 1:10) { set.seed(17); runif(1); }', 'seeded-randomness', [], { consumerCalls: 1, callsWithFunctionProducers: 1, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); - - 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, callsWithOtherBranchProducers: 0 }); - assertLinter('condition true', parser, 'if(TRUE) { set.seed(17); }\nrunif(1);', 'seeded-randomness', [], { consumerCalls: 1, callsWithFunctionProducers: 1, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); - assertLinter('condition unclear', parser, 'if(u) { set.seed(17); }\nrunif(1);', 'seeded-randomness', - [{ range: [2,1,2,8], function: 'runif', certainty: LintingResultCertainty.Uncertain }], { consumerCalls: 1, callsWithFunctionProducers: 0, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 1 }); - assertLinter('condition unclear after definite seed', parser, 'set.seed(17); if(u) { set.seed(17); }\nrunif(1);', 'seeded-randomness', [], { consumerCalls: 1, callsWithFunctionProducers: 1, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); - assertLinter('condition exhaustive', parser, 'if(u) { set.seed(17); } else { set.seed(18); }\nrunif(1);', 'seeded-randomness', [], { consumerCalls: 1, callsWithFunctionProducers: 1, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); - assertLinter('condition reversed', parser, 'set.seed(17);\nif(u) { runif(1) }', 'seeded-randomness', [], { consumerCalls: 1, callsWithFunctionProducers: 1, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); - assertLinter('separate conditions', parser, 'if (u) { set.seed(17) }; if (u) { runif(1) }', 'seeded-randomness', - [{ range: [1,35,1,42],function: 'runif', certainty: LintingResultCertainty.Uncertain }], { consumerCalls: 1, callsWithFunctionProducers: 0, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 1 }); + describe('simple', () => { + assertLinter('none', parser, 'cat("hello")', 'seeded-randomness', [], { consumerCalls: 0, callsWithFunctionProducers: 0, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); + assertLinter('no producer', parser, 'runif(1)', 'seeded-randomness', + [{ range: [1,1,1,8], function: 'runif', certainty: LintingResultCertainty.Certain }], { consumerCalls: 1, callsWithFunctionProducers: 0, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); + assertLinter('no consumer', parser, 'set.seed(17)', 'seeded-randomness', [], { consumerCalls: 0, callsWithFunctionProducers: 0, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); + assertLinter('both', parser, 'set.seed(17)\nrunif(1)', 'seeded-randomness', [], { consumerCalls: 1, callsWithFunctionProducers: 1, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); + assertLinter('after', parser, 'runif(1)\nset.seed(17)', 'seeded-randomness', + [{ range: [1,1,1,8], function: 'runif', certainty: LintingResultCertainty.Certain }], { consumerCalls: 1, callsWithFunctionProducers: 0, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); + + assertLinter('multiple seeds', parser, 'set.seed(1);\nset.seed(2);\nrunif(1);', 'seeded-randomness', [], { consumerCalls: 1, callsWithFunctionProducers: 1, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); + assertLinter('multiple consumers', parser, 'set.seed(1);\nset.seed(2);\nrunif(1);\nrunif(1);', 'seeded-randomness', [], { consumerCalls: 2, callsWithFunctionProducers: 2, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); + }); + + describe('loops', () => { + assertLinter('invalid', parser, 'for(i in 1:10) { runif(1); }', 'seeded-randomness', + [{ range: [1,18,1,25], function: 'runif', certainty: LintingResultCertainty.Certain }]); + assertLinter('valid', parser, 'for(i in 1:10) { set.seed(17); runif(1); }', 'seeded-randomness', [], { consumerCalls: 1, callsWithFunctionProducers: 1, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); + }); + + describe('conditions', () => { + assertLinter('both false', parser, 'if(FALSE) { set.seed(17); \nrunif(1); }', 'seeded-randomness', [], { consumerCalls: 0, callsWithFunctionProducers: 0, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); + assertLinter('both true', parser, 'if(TRUE) { set.seed(17); \nrunif(1); }', 'seeded-randomness', [], { consumerCalls: 1, callsWithFunctionProducers: 1, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); + assertLinter('false', 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, callsWithOtherBranchProducers: 0 }); + assertLinter('true', parser, 'if(TRUE) { set.seed(17); }\nrunif(1);', 'seeded-randomness', [], { consumerCalls: 1, callsWithFunctionProducers: 1, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); + assertLinter('unclear', parser, 'if(u) { set.seed(17); }\nrunif(1);', 'seeded-randomness', + [{ range: [2,1,2,8], function: 'runif', certainty: LintingResultCertainty.Uncertain }], { consumerCalls: 1, callsWithFunctionProducers: 0, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 1 }); + assertLinter('unclear after definite seed', parser, 'set.seed(17); if(u) { set.seed(17); }\nrunif(1);', 'seeded-randomness', [], { consumerCalls: 1, callsWithFunctionProducers: 1, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); + assertLinter('exhaustive', parser, 'if(u) { set.seed(17); } else { set.seed(18); }\nrunif(1);', 'seeded-randomness', [], { consumerCalls: 1, callsWithFunctionProducers: 1, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); + assertLinter('reversed', parser, 'set.seed(17);\nif(u) { runif(1) }', 'seeded-randomness', [], { consumerCalls: 1, callsWithFunctionProducers: 1, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); + assertLinter('separate', parser, 'if (u) { set.seed(17) }; if (u) { runif(1) }', 'seeded-randomness', + [{ range: [1,35,1,42],function: 'runif', certainty: LintingResultCertainty.Uncertain }], { consumerCalls: 1, callsWithFunctionProducers: 0, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 1 }); + assertLinter('nested true', parser, 'if(TRUE) { if(TRUE) { set.seed(17); }\nrunif(1); }', 'seeded-randomness', [], { consumerCalls: 1, callsWithFunctionProducers: 1, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); + assertLinter('nested producer false', parser, 'if(TRUE) { 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, callsWithOtherBranchProducers: 0 }); + assertLinter('nested consumer', parser, 'if(a) {set.seed(17); if(b) { runif(1); } }', 'seeded-randomness', [], { consumerCalls: 1, callsWithFunctionProducers: 1, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); + }); + + describe('seed assignment', () => { + assertLinter('set .Random.seed', parser, '.Random.seed <- 17\nrunif(1)', 'seeded-randomness', [], + { consumerCalls: 1, callsWithAssignmentProducers: 1, callsWithFunctionProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); + assertLinter('set .Random.seed with assignment inbetween', parser, '.Random.seed <- 17\nx <- 7 \nrunif(1)', 'seeded-randomness', [], + { consumerCalls: 1, callsWithAssignmentProducers: 1, callsWithFunctionProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); + assertLinter('set .Random.seed reverse', parser, '17 -> .Random.seed\nrunif(1)', 'seeded-randomness', [], + { consumerCalls: 1, callsWithAssignmentProducers: 1, callsWithFunctionProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); + assertLinter('set .Random.seed override <-', parser, '`<-`<-function(){}\n.Random.seed <- 17\nrunif(1)', 'seeded-randomness', + [{ range: [3,1,3,8], function: 'runif', certainty: LintingResultCertainty.Certain }], + { consumerCalls: 1, callsWithAssignmentProducers: 0, callsWithFunctionProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); + }); 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 } @@ -34,22 +63,9 @@ describe('flowR linter', withTreeSitter(parser => { { range: [2,1,2,8], function: 'runif', certainty: LintingResultCertainty.Certain } ], { consumerCalls: 2, callsWithFunctionProducers: 0, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 2, callsWithOtherBranchProducers: 0 }); - assertLinter('multiple seeds', parser, 'set.seed(1);\nset.seed(2);\nrunif(1);', 'seeded-randomness', [], { consumerCalls: 1, callsWithFunctionProducers: 1, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); - assertLinter('multiple consumers', parser, 'set.seed(1);\nset.seed(2);\nrunif(1);\nrunif(1);', 'seeded-randomness', [], { consumerCalls: 2, callsWithFunctionProducers: 2, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); - assertLinter('custom set.seed', parser, 'set.seed <- function(x) {}\nset.seed(17)\nrunif(1)', 'seeded-randomness', [{ range: [3,1,3,8], function: 'runif', certainty: LintingResultCertainty.Certain }], { consumerCalls: 1, callsWithFunctionProducers: 0, callsWithAssignmentProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); - assertLinter('set .Random.seed', parser, '.Random.seed <- 17\nrunif(1)', 'seeded-randomness', [], - { consumerCalls: 1, callsWithAssignmentProducers: 1, callsWithFunctionProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); - assertLinter('set .Random.seed with assignment inbetween', parser, '.Random.seed <- 17\nx <- 7 \nrunif(1)', 'seeded-randomness', [], - { consumerCalls: 1, callsWithAssignmentProducers: 1, callsWithFunctionProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); - assertLinter('set .Random.seed reverse', parser, '17 -> .Random.seed\nrunif(1)', 'seeded-randomness', [], - { consumerCalls: 1, callsWithAssignmentProducers: 1, callsWithFunctionProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); - assertLinter('set .Random.seed override <-', parser, '`<-`<-function(){}\n.Random.seed <- 17\nrunif(1)', 'seeded-randomness', - [{ range: [3,1,3,8], function: 'runif', certainty: LintingResultCertainty.Certain }], - { consumerCalls: 1, callsWithAssignmentProducers: 0, callsWithFunctionProducers: 0, callsWithNonConstantProducers: 0, callsWithOtherBranchProducers: 0 }); - assertLinter('set in function call', parser, 'runif(set.seed(17))', 'seeded-randomness', [{ range: [1,1,1,19], function: 'runif', certainty: LintingResultCertainty.Certain }]); assertLinter('get in function call', parser, 'runif(runif(1))', 'seeded-randomness', [ From 3dc3426dd89a973d92657ebdccc3625a18bc3780 Mon Sep 17 00:00:00 2001 From: Ellpeck Date: Mon, 24 Nov 2025 15:34:01 +0100 Subject: [PATCH 10/10] refactor: style cleanup --- src/linter/rules/seeded-randomness.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/linter/rules/seeded-randomness.ts b/src/linter/rules/seeded-randomness.ts index a4a6ffba6ab..e72c87ae8a3 100644 --- a/src/linter/rules/seeded-randomness.ts +++ b/src/linter/rules/seeded-randomness.ts @@ -39,11 +39,11 @@ export interface SeededRandomnessConfig extends MergeableRecord { } export interface SeededRandomnessMeta extends MergeableRecord { - consumerCalls: number - callsWithFunctionProducers: number - callsWithAssignmentProducers: number - callsWithNonConstantProducers: number - callsWithOtherBranchProducers: number + consumerCalls: number + callsWithFunctionProducers: number + callsWithAssignmentProducers: number + callsWithNonConstantProducers: number + callsWithOtherBranchProducers: number } export const SEEDED_RANDOMNESS = { @@ -111,6 +111,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)){ + // 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)) { const aCds = new Set(a.cds).difference(cds); if(aCds.size <= 0 || happensInEveryBranch([...aCds])) {