Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
12 changes: 2 additions & 10 deletions src/linter/linter-executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,13 @@ export async function executeLintingRule<Name extends LintingRuleNames>(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<Name> ?? {}),
searchTimeMs: searchTime,
processTimeMs: processTime
}
Expand Down
12 changes: 5 additions & 7 deletions src/linter/linter-format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -41,7 +39,7 @@ export interface LinterRuleInformation<Config extends MergeableRecord = never> {
* 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<Result extends LintingResult, Metadata extends MergeableRecord, Config extends MergeableRecord = never, Info = ParentInformation, Elements extends FlowrSearchElement<Info>[] = FlowrSearchElement<Info>[]> {
export interface LintingRule<Result extends LintingResult, Metadata extends MergeableRecord = never, Config extends MergeableRecord = never, Info = ParentInformation, Elements extends FlowrSearchElement<Info>[] = FlowrSearchElement<Info>[]> {
/**
* 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}.
Expand All @@ -51,9 +49,9 @@ export interface LintingRule<Result extends LintingResult, Metadata extends Merg
* Processes the search results of the search created through {@link createSearch}.
* This function is expected to return the linting results from this rule for the given search, ie usually the given script file.
*/
readonly processSearchResult: (elements: FlowrSearchElements<Info, Elements>, config: Config, data: { normalize: NormalizedAst, dataflow: DataflowInformation, cfg: ControlFlowInformation, analyzer: ReadonlyFlowrAnalysisProvider }) => AsyncOrSync<{
results: Result[],
'.meta': Metadata
readonly processSearchResult: (elements: FlowrSearchElements<Info, Elements>, config: Config, data: ReadonlyFlowrAnalysisProvider) => AsyncOrSync<{
results: Result[],
'.meta'?: Metadata
}>
/**
* A set of functions used to pretty-print the given linting result.
Expand Down
10 changes: 6 additions & 4 deletions src/linter/rules/absolute-path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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++;
Expand All @@ -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,
Expand All @@ -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,
Expand Down
14 changes: 8 additions & 6 deletions src/linter/rules/dataframe-access-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand Down
30 changes: 13 additions & 17 deletions src/linter/rules/dead-code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<DeadCodeResult>[]
),
'.meta': meta
)
};
},
prettyPrint: {
Expand All @@ -70,7 +66,7 @@ export const DEAD_CODE = {
description: 'Marks areas of code that are never reached during execution.',
defaultConfig: {}
}
} as const satisfies LintingRule<DeadCodeResult, DeadCodeMetadata, DeadCodeConfig>;
} as const satisfies LintingRule<DeadCodeResult, never, DeadCodeConfig>;

function combineResults(results: Writable<DeadCodeResult>[]): DeadCodeResult[] {
for(let i = results.length-1; i >= 0; i--){
Expand Down
2 changes: 1 addition & 1 deletion src/linter/rules/deprecated-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
6 changes: 3 additions & 3 deletions src/linter/rules/file-path-validity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,17 +82,17 @@ 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++;
return [];
}

// 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++;
Expand Down
35 changes: 18 additions & 17 deletions src/linter/rules/function-finder-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -46,23 +46,25 @@ export const functionFinderUtil = {
name: FlowrFilter.MatchesEnrichment,
args: {
enrichment: Enrichment.CallTargets,
test: testFunctionsIgnoringPackage(functions)
test: {
targets: testFunctionsIgnoringPackage(functions)
}
}
})
);
},
processSearchResult: <T extends FlowrSearchElement<ParentInformation>[]>(
async processSearchResult<T extends FlowrSearchElement<ParentInformation>[]>(
elements: FlowrSearchElements<ParentInformation, T>,
_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 => {
Expand Down Expand Up @@ -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<ParentInformation>,
pool: readonly FunctionInfo[],
data: { normalize: NormalizedAst, dataflow: DataflowInformation, analyzer: ReadonlyFlowrAnalysisProvider },
analyzer: ReadonlyFlowrAnalysisProvider,
requireValue: RegExp | string | undefined
): Ternary {
): Promise<Ternary> {
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){
Expand All @@ -127,5 +130,3 @@ export const functionFinderUtil = {
return Ternary.Never;
}
};


5 changes: 3 additions & 2 deletions src/linter/rules/naming-convention.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 {
Expand Down
40 changes: 22 additions & 18 deletions src/linter/rules/network-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ParentInformation> & { 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<ParentInformation> & { 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<ParentInformation> & {
certainty: LintingResultCertainty
};
x.certainty = val === Ternary.Always ? LintingResultCertainty.Certain : LintingResultCertainty.Uncertain;
res.push(x);
}
const x = e as unknown as FlowrSearchElement<ParentInformation> & { 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',
Expand Down
Loading
Loading