Skip to content
195 changes: 185 additions & 10 deletions src/linter/rules/deprecated-functions.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,194 @@
import { LintingRuleCertainty, type LintingRule } from '../linter-format';
import type { Range } from 'semver';
import type { BrandedIdentifier } from '../../dataflow/environments/identifier';
import { FunctionArgument } from '../../dataflow/graph/graph';
import { isFunctionCallVertex } from '../../dataflow/graph/vertex';
import { EmptyArgument } from '../../r-bridge/lang-4.x/ast/model/nodes/r-function-call';
import { Enrichment, enrichmentContent } from '../../search/search-executor/search-enrichers';
import { isNotUndefined } from '../../util/assert';
import type { MergeableRecord } from '../../util/objects';
import { SourceLocation } from '../../util/range';
import type { LintingResult, LintingRule } from '../linter-format';
import { LintingPrettyPrintContext, LintingResultCertainty, LintingRuleCertainty } from '../linter-format';
import { LintingRuleTag } from '../linter-tags';
import { type FunctionsMetadata, type FunctionsResult, type FunctionsToDetectConfig, functionFinderUtil } from './function-finder-util';
import { type FunctionsMetadata, functionFinderUtil } from './function-finder-util';
import { parseRRange } from '../../util/r-version';

/**
* Information about an argument of a function that should be flagged as deprecated if it is called with this argument
*/
export interface DeprecatedArgumentInformation {
argIdx?: number,
argName?: string
ifValue?: RegExp | string
replacedBy?: string
Comment thread
gigalasr marked this conversation as resolved.
Outdated
sinceVersion?: Range
state?: DeprecationState
}

/**
* Information about a deprecated function
*/
export interface DeprecatedFunctionInformation {
name: string
/** Used to mark specific arguments as deprecated */
whenArgs?: DeprecatedArgumentInformation[]
Comment thread
gigalasr marked this conversation as resolved.
Outdated
replacedBy?: string
sinceVersion?: Range
state?: DeprecationState
}

export interface DeprecatedFunctionResultBase extends LintingResult {
Comment thread
gigalasr marked this conversation as resolved.
function: BrandedIdentifier

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why just branded? please use the full Identifier here

replacedBy?: string
sinceVersion?: Range
state?: DeprecationState
}

export interface DeprecatedFunctionResult extends DeprecatedFunctionResultBase {
type: 'deprecated-function'
}

export interface DeprecatedArgumentResult extends DeprecatedFunctionResultBase {
type: 'deprecated-argument'
arg: string | number
}

export type DeprecatedFunctionRuleResult = DeprecatedFunctionResult | DeprecatedArgumentResult;

export enum DeprecationState {
/** Still works, but marked for removal */
Deprecated = 'deprecated',
Comment thread
gigalasr marked this conversation as resolved.
/** No longer works, and marked for removal */
Defunct = 'defunct',
/** Replaced by another function but kept for compatibility */
Superseeded = 'superseeded'
}

export interface DeprecatedFunctionsConfig extends MergeableRecord{
/** Functions to mark as deprecated */
fns: (string | DeprecatedFunctionInformation)[]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not Identifier?

}


const DeprecatedFunctions = [
{ name: 'geom_violin', whenArgs: [{ argName: 'draw_quantiles', state: DeprecationState.Deprecated, replacedBy: 'quantile.linetype', sinceVersion: parseRRange('>= 4.0.0') }] },
'all_equal', 'arrange_all', 'distinct_all', 'filter_all', 'group_by_all', 'summarise_all', 'mutate_all', 'select_all', 'vars', 'all_vars', 'id', 'failwith', 'select_vars', 'rename_vars', 'select_var', 'current_vars', 'bench_tbls', 'compare_tbls', 'compare_tbls2', 'eval_tbls', 'eval_tbls2', 'location', 'changes', 'combine', 'do', 'funs', 'add_count_', 'add_tally_', 'arrange1_', 'count_', 'distinct_', 'do_', 'filter_', 'funs_', 'group_by_', 'group_indices_', 'mutate_', 'tally_', 'transmute_', 'rename_', 'rename_vars_', 'select_', 'select_vars_', 'slice_', 'summarise_', 'summarize_', 'summarise_each', 'src_local', 'tbl_df', 'add_rownames', 'group_nest', 'group_split', 'with_groups', 'nest_by', 'progress_estimated', 'recode', 'sample_n', 'top_n', 'transmute', 'fct_explicit_na', 'aes_', 'aes_auto', 'annotation_logticks', 'is.Coord', 'coord_flip', 'coord_map', 'is.facet', 'fortify', 'is.ggproto', 'guide_train', 'is.ggplot', 'qplot', 'is.theme', 'gg_dep', 'liply', 'isplit2', 'list_along', 'cross', 'invoke', 'at_depth', 'prepend', 'rerun', 'splice', '`%@%`', 'rbernoulli', 'rdunif', 'when', 'update_list', 'map_raw', 'accumulate', 'reduce_right', 'flatten', 'map_dfr', 'as_vector', 'transpose', 'melt_delim', 'melt_fwf', 'melt_table', 'read_table2', 'str_interp', 'as_tibble', 'data_frame', 'tibble_', 'data_frame_', 'lst_', 'as_data_frame', 'as.tibble', 'frame_data', 'trunc_mat', 'is.tibble', 'tidy_names', 'set_tidy_names', 'repair_names', 'extract_numeric', 'complete_', 'drop_na_', 'expand_', 'crossing_', 'nesting_', 'extract_', 'fill_', 'gather_', 'nest_', 'separate_rows_', 'separate_', 'spread_', 'unite_', 'unnest_', 'extract', 'gather', 'nest_legacy', 'separate_rows', 'separate', 'spread'
] satisfies (string | DeprecatedFunctionInformation)[];

export const DEPRECATED_FUNCTIONS = {
createSearch: (config) => functionFinderUtil.createSearch(config.fns),
processSearchResult: functionFinderUtil.processSearchResult,
prettyPrint: functionFinderUtil.prettyPrint('deprecated'),
info: {
createSearch: (config) => functionFinderUtil.createSearch(config.fns.map(fn => typeof fn === 'string' ? fn : fn.name)),
processSearchResult: async(elements, config, data) => {
const fnInfosEx = Object.fromEntries(
config.fns
.filter(f => typeof f === 'object')
.map(f => [f.name, f])
Comment thread
gigalasr marked this conversation as resolved.
Outdated
);

const dataflow = (await data.dataflow()).graph;
const idMap = (await data.normalize()).idMap;

const metadata: FunctionsMetadata = {
totalCalls: 0,
totalFunctionDefinitions: 0
};

const detectedFunctions = elements.getElements().flatMap(e => {
metadata.totalCalls++;
return enrichmentContent(e, Enrichment.CallTargets).targets.map(target => {
metadata.totalFunctionDefinitions++;
const sourceLocation = SourceLocation.fromNode(e.node);
if(sourceLocation !== undefined) {
return {
node: e.node, target: target as BrandedIdentifier, sourceLocation
};
}
});
}).filter(p => isNotUndefined(p));

const results = detectedFunctions.map(e => {
const info = fnInfosEx[e.target];
if(info === undefined) {
// No extra info => function is always marked as deprecated
return [{
type: 'deprecated-function',
certainty: LintingResultCertainty.Certain,
involvedId: e.node.info.id,
loc: e.sourceLocation,
function: e.target,
} satisfies DeprecatedFunctionResult];
} else if(info.whenArgs !== undefined) {
// If whenArgs is set, the function should only be deprecated if it has one of the provided args
return info.whenArgs.map(deprecatedArgInfo => {
const vertex = dataflow.getVertex(e.node.info.id);
if(vertex === undefined || !isFunctionCallVertex(vertex)) {
return undefined;
}

const arg = vertex.args.find((arg, idx) =>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we have argument finders like pMatch for this, please use them :D
There is also getValueOfArgument`

FunctionArgument.isNamed(arg) && arg.name === deprecatedArgInfo.argName ||
FunctionArgument.isPositional(arg) && idx === deprecatedArgInfo.argIdx
);
const argNode = arg === undefined || arg === EmptyArgument ? undefined : idMap.get(arg.nodeId);

if(argNode !== undefined) {
return {
type: 'deprecated-argument',
certainty: LintingResultCertainty.Certain,
involvedId: argNode.info.id,
function: e.target,
arg: (deprecatedArgInfo.argName ?? deprecatedArgInfo.argIdx) as string | number,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is the cast necessary?

state: deprecatedArgInfo.state,
replacedBy: deprecatedArgInfo.replacedBy,
sinceVersion: deprecatedArgInfo.sinceVersion,
loc: SourceLocation.fromNode(argNode) ?? e.sourceLocation
} satisfies DeprecatedArgumentResult;
}

// Don't mark function as deprecated, if we didn't find any deprecated args
}).filter(p => p !== undefined);
Comment thread
gigalasr marked this conversation as resolved.
Outdated
} else {
// Extra Info but no whenArgs => always marked as deprecated and extra info is provided
return [{
type: 'deprecated-function',
certainty: LintingResultCertainty.Certain,
involvedId: e.node.info.id,
loc: e.sourceLocation,
function: e.target,
state: info.state,
replacedBy: info.replacedBy,
sinceVersion: info.sinceVersion
} satisfies DeprecatedFunctionResult];
}
}).flat();

return {
results, '.meta': metadata
};
},
prettyPrint: {
[LintingPrettyPrintContext.Query]: (result: DeprecatedFunctionRuleResult) => `${result.type === 'deprecated-argument' ? `Argument \`${result.arg}\` of ` : ''}Function \`${result.function}\` at ${SourceLocation.format(result.loc)}`,
[LintingPrettyPrintContext.Full]: (result: DeprecatedFunctionRuleResult) => {
const str: string[] = [];
if(result.type === 'deprecated-argument') {
const argStr = typeof result.arg === 'number' ? `at position \`${result.arg}\`` : result.arg;
str.push(`Argument \`${argStr}\` of`);
}
str.push(`Function \`${result.function}\` is ${result.state ?? 'deprecated'}`);
if(result.sinceVersion) {
str.push(`since version ${result.sinceVersion.format()}`);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't we check whether the since Version range contract is even fulfilled?

}
if(result.replacedBy) {
str.push(`and is replaced by \`${result.replacedBy}\``);
}
return str.join(' ');
}
},
info: {
name: 'Deprecated Functions',
tags: [LintingRuleTag.Deprecated, LintingRuleTag.Smell, LintingRuleTag.Usability, LintingRuleTag.Reproducibility],
// ensures all deprecated functions found are actually deprecated through its limited config, but doesn't find all deprecated functions since the config is pre-crawled
certainty: LintingRuleCertainty.BestEffort,
description: 'Marks deprecated functions that should not be used anymore.',
defaultConfig: {
fns: ['all_equal', 'arrange_all', 'distinct_all', 'filter_all', 'group_by_all', 'summarise_all', 'mutate_all', 'select_all', 'vars', 'all_vars', 'id', 'failwith', 'select_vars', 'rename_vars', 'select_var', 'current_vars', 'bench_tbls', 'compare_tbls', 'compare_tbls2', 'eval_tbls', 'eval_tbls2', 'location', 'changes', 'combine', 'do', 'funs', 'add_count_', 'add_tally_', 'arrange_', 'count_', 'distinct_', 'do_', 'filter_', 'funs_', 'group_by_', 'group_indices_', 'mutate_', 'tally_', 'transmute_', 'rename_', 'rename_vars_', 'select_', 'select_vars_', 'slice_', 'summarise_', 'summarize_', 'summarise_each', 'src_local', 'tbl_df', 'add_rownames', 'group_nest', 'group_split', 'with_groups', 'nest_by', 'progress_estimated', 'recode', 'sample_n', 'top_n', 'transmute', 'fct_explicit_na', 'aes_', 'aes_auto', 'annotation_logticks', 'is.Coord', 'coord_flip', 'coord_map', 'is.facet', 'fortify', 'is.ggproto', 'guide_train', 'is.ggplot', 'qplot', 'is.theme', 'gg_dep', 'liply', 'isplit2', 'list_along', 'cross', 'invoke', 'at_depth', 'prepend', 'rerun', 'splice', '`%@%`', 'rbernoulli', 'rdunif', 'when', 'update_list', 'map_raw', 'accumulate', 'reduce_right', 'flatten', 'map_dfr', 'as_vector', 'transpose', 'melt_delim', 'melt_fwf', 'melt_table', 'read_table2', 'str_interp', 'as_tibble', 'data_frame', 'tibble_', 'data_frame_', 'lst_', 'as_data_frame', 'as.tibble', 'frame_data', 'trunc_mat', 'is.tibble', 'tidy_names', 'set_tidy_names', 'repair_names', 'extract_numeric', 'complete_', 'drop_na_', 'expand_', 'crossing_', 'nesting_', 'extract_', 'fill_', 'gather_', 'nest_', 'separate_rows_', 'separate_', 'spread_', 'unite_', 'unnest_', 'extract', 'gather', 'nest_legacy', 'separate_rows', 'separate', 'spread']
}
defaultConfig: { fns: DeprecatedFunctions }
}
} as const satisfies LintingRule<FunctionsResult, FunctionsMetadata, FunctionsToDetectConfig>;
} as const satisfies LintingRule<DeprecatedFunctionRuleResult, FunctionsMetadata, DeprecatedFunctionsConfig>;
2 changes: 1 addition & 1 deletion src/linter/rules/naming-convention.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export function getMostUsedCasing(symbols: { detectedCasing: CasingConvention }[
map.set(symbol.detectedCasing, o + 1);
}

// Return element with most occurances
// Return element with most occurrences
return [...map].reduce((p, c) => p[1] > c[1] ? p : c)[0];
}

Expand Down
8 changes: 3 additions & 5 deletions src/linter/rules/network-functions.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import { LintingResultCertainty, type LintingRule, LintingRuleCertainty } from '../linter-format';
import { functionFinderUtil, type FunctionsMetadata, type FunctionsResult } from './function-finder-util';
import type { FunctionsToDetectConfig, FunctionsMetadata, FunctionsResult } from './function-finder-util';
import { functionFinderUtil } from './function-finder-util';
import { LintingRuleTag } from '../linter-tags';
import type { MergeableRecord } from '../../util/objects';
import { ReadFunctions } from '../../queries/catalog/dependencies-query/function-info/read-functions';
import type { FlowrSearchElement } from '../../search/flowr-search';
import type { ParentInformation } from '../../r-bridge/lang-4.x/ast/model/processing/decorate';
import { Ternary } from '../../util/logic';
import { SourceFunctions } from '../../queries/catalog/dependencies-query/function-info/source-functions';
import { WriteFunctions } from '../../queries/catalog/dependencies-query/function-info/write-functions';

export interface NetworkFunctionsConfig extends MergeableRecord {
/** The list of function names that should be marked in the given context if their arguments match. */
fns: readonly string[]
export interface NetworkFunctionsConfig extends FunctionsToDetectConfig {
/** only trigger if the function's read argument is linked to a value that matches this pattern */
onlyTriggerWithArgument?: RegExp | string
}
Expand Down
4 changes: 3 additions & 1 deletion test/functionality/_helper/linter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ export function controlledPkgDb(pkg: string, exports: readonly string[]): PkgDb
}


type DistributiveOmit<T, K extends keyof T> = T extends unknown ? Omit<T, K> : never;

/**
* Asserts correct linting results while ignoring each linting result's {@link LintingRuleResult.involvedId}.
*/
Expand All @@ -52,7 +54,7 @@ export function assertLinter<Name extends LintingRuleNames>(
parser: KnownParser,
code: string,
ruleName: Name,
expected: Omit<LintingRuleResult<Name>, 'involvedId'>[] | ((df: DataflowInformation, ast: NormalizedAst) => Omit<LintingRuleResult<Name>, 'involvedId'>[]),
expected: DistributiveOmit<LintingRuleResult<Name>, 'involvedId'>[] | ((df: DataflowInformation, ast: NormalizedAst) => Omit<LintingRuleResult<Name>, 'involvedId'>[]),
expectedMetadata?: LintingRuleMetadata<Name>,
lintingRuleConfig?: DeepPartial<LintingRuleConfig<Name>> & LinterTestSetup
) {
Expand Down
43 changes: 35 additions & 8 deletions test/functionality/linter/lint-deprecated-functions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { describe } from 'vitest';
import { withTreeSitter } from '../_helper/shell';
import { assertLinter, controlledPkgDb } from '../_helper/linter';
import { LintingResultCertainty } from '../../../src/linter/linter-format';
import { DeprecationState } from '../../../src/linter/rules/deprecated-functions';
import { parseRRange } from '../../../src/util/r-version';

describe('flowR linter', withTreeSitter(parser => {
describe('deprecated functions', () => {
Expand All @@ -14,31 +16,31 @@ describe('flowR linter', withTreeSitter(parser => {
/* Given that we declare `cat` as deprecated, we expect all uses to be marked! */
assertLinter('cat', parser, 'cat("hello")\nprint("hello")\nx <- 1\ncat(x)',
'deprecated-functions', [
{ certainty: LintingResultCertainty.Certain, function: 'cat', loc: [1, 1, 1, 12] },
{ certainty: LintingResultCertainty.Certain, function: 'cat', loc: [4, 1, 4, 6] },
{ certainty: LintingResultCertainty.Certain, function: 'cat', loc: [1, 1, 1, 12], type: 'deprecated-function' },
{ certainty: LintingResultCertainty.Certain, function: 'cat', loc: [4, 1, 4, 6], type: 'deprecated-function' },
],
{ totalCalls: 2, totalFunctionDefinitions: 2 },
{ fns: ['cat'] }
);
/* Overwriting the `cat` function with a user defined implementation (even though it is useless), should cause the linter to not mark calls to the custom `cat` function as deprecated */
assertLinter('custom cat', parser, 'cat("hello")\nprint("hello")\ncat <- function(x) { }\nx <- 1\ncat(x)',
'deprecated-functions', [
{ certainty: LintingResultCertainty.Certain, function: 'cat', loc: [1, 1, 1, 12] }
{ certainty: LintingResultCertainty.Certain, function: 'cat', loc: [1, 1, 1, 12], type: 'deprecated-function' }
],
{ totalCalls: 1, totalFunctionDefinitions: 1 },
{ fns: ['cat'] }
);
/* Using the default linter configuration, a function such as `all_equal` should be marked as deprecated */
assertLinter('with defaults', parser, 'all_equal(foo)',
'deprecated-functions', [
{ certainty: LintingResultCertainty.Certain, function: 'all_equal', loc: [1, 1, 1, 14] }
{ certainty: LintingResultCertainty.Certain, function: 'all_equal', loc: [1, 1, 1, 14], type: 'deprecated-function' }
],
{ totalCalls: 1, totalFunctionDefinitions: 1 }
);
/* We should find deprecated functions even if they are nested in other function calls */
assertLinter('with defaults nested', parser, 'foo(all_equal(foo))',
'deprecated-functions', [
{ certainty: LintingResultCertainty.Certain, function: 'all_equal', loc: [1, 5, 1, 18] }
{ certainty: LintingResultCertainty.Certain, function: 'all_equal', loc: [1, 5, 1, 18], type: 'deprecated-function' }
],
{ totalCalls: 1, totalFunctionDefinitions: 1 }
);
Expand All @@ -47,23 +49,48 @@ describe('flowR linter', withTreeSitter(parser => {
first <- data.frame(x = c(1, 2, 3), y = c(1, 2, 3))
second <- data.frame(x = c(1, 3, 2), y = c(1, 3, 2))
dplyr::all_equal(first, second)`, 'deprecated-functions',
[{ certainty: LintingResultCertainty.Certain, function: 'dplyr::all_equal', loc: [4, 1, 4, 31] }],
[{ certainty: LintingResultCertainty.Certain, function: 'dplyr::all_equal', loc: [4, 1, 4, 31], type: 'deprecated-function' }],
{ totalCalls: 1, totalFunctionDefinitions: 1 });

describe('a deprecated function resolved via a loaded package is still flagged', () => {
// regression: the loaded-package export must still count as a built-in call target
assertLinter('with a (controlled) package database', parser, 'library(dplyr)\nrecode(x)',
'deprecated-functions',
[{ certainty: LintingResultCertainty.Certain, function: 'recode', loc: [2, 1, 2, 9] }],
[{ certainty: LintingResultCertainty.Certain, function: 'recode', loc: [2, 1, 2, 9], type: 'deprecated-function' }],
{ totalCalls: 1, totalFunctionDefinitions: 1 },
{ fns: ['recode'], pkgDb: controlledPkgDb('dplyr', ['recode', 'filter']) }
);
assertLinter('without any package database', parser, 'library(dplyr)\nrecode(x)',
'deprecated-functions',
[{ certainty: LintingResultCertainty.Certain, function: 'recode', loc: [2, 1, 2, 9] }],
[{ certainty: LintingResultCertainty.Certain, function: 'recode', loc: [2, 1, 2, 9], type: 'deprecated-function' }],
{ totalCalls: 1, totalFunctionDefinitions: 1 },
{ fns: ['recode'], noPkgDb: true }
);
});

describe('only detect deprecated args when present', () => {
assertLinter('deprecated arg but not present', parser, 'testFn()',
'deprecated-functions',
[],
{ totalCalls: 1, totalFunctionDefinitions: 1 },
{ fns: [{ name: 'testFn', whenArgs: [{ argName: 'badArg', state: DeprecationState.Deprecated }] } ] }
);

assertLinter('deprecated arg present', parser, 'testFn(badArg=5)',
'deprecated-functions',
[{
type: 'deprecated-argument',
certainty: LintingResultCertainty.Certain,
arg: 'badArg',
replacedBy: 'foo',
function: 'testFn',
state: DeprecationState.Deprecated,
sinceVersion: parseRRange('>= 4.0.0'),
loc: [1, 8, 1, 13]
}],
{ totalCalls: 1, totalFunctionDefinitions: 1 },
{ fns: [{ name: 'testFn', whenArgs: [{ argName: 'badArg', state: DeprecationState.Deprecated, replacedBy: 'foo', sinceVersion: parseRRange('>= 4.0.0') }] } ] }
);
});
});
}));
Loading