diff --git a/src/linter/rules/network-functions.ts b/src/linter/rules/network-functions.ts index 9431b3d0aeb..32afc4d4812 100644 --- a/src/linter/rules/network-functions.ts +++ b/src/linter/rules/network-functions.ts @@ -8,29 +8,50 @@ import type { ParentInformation } from '../../r-bridge/lang-4.x/ast/model/proces 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'; +import type { FunctionInfo } from '../../queries/catalog/dependencies-query/function-info/function-info'; 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[] - /** only trigger if the function's read argument is linked to a value that matches this pattern */ + /** + * The list of function names or more detailed {@link NetworkFunction} information that should be marked in the given context if their arguments match. + */ + fns: readonly (string | NetworkFunction)[] + /** + * Only trigger if the function's read argument is linked to a value that matches this pattern. + * This value is only used for entries in {@link fns} that are either strings, or whose {@link NetworkFunction.onlyTriggerWithArgument} value is unset. + */ + onlyTriggerWithArgument?: RegExp | string +} +export interface NetworkFunction extends MergeableRecord{ + /** + * The name of the network function to find. + */ + name: string, + /** + * The {@link FunctionInfo} to use for querying the argument whose value should match {@link onlyTriggerWithArgument}. + * If this is not specified, flowR's default database of functions ({@link ReadFunctions}, {@link SourceFunctions} and {@link WriteFunctions}) is queried for appropriate information on the function's read argument. + */ + info?: Omit + /** + * Only trigger if the function's read argument is linked to a value that matches this pattern through {@link info}. + * If this is unset for any function, the default value from the {@link NetworkFunctionsConfig} is used. + */ onlyTriggerWithArgument?: RegExp | string } - -const FnPool = new Map(ReadFunctions.concat(SourceFunctions, WriteFunctions).map(f => [f.name, f] as const)); +const DefaultFunctionPool = new Map(ReadFunctions.concat(SourceFunctions, WriteFunctions).map(f => [f.name, f] as const)); export const NETWORK_FUNCTIONS = { - createSearch: (config) => functionFinderUtil.createSearch(config.fns), + createSearch: (config) => functionFinderUtil.createSearch(config.fns.map(f => typeof f === 'string' ? f : f.name)), processSearchResult: (e, c, d) => { + const fnPool = new Map([ + ...DefaultFunctionPool, + ...c.fns.flatMap(f => typeof f === 'string' ? [] : f.info === undefined ? [] : [[f.name, { name: f.name, ...f.info }] as const]) + ]); + const onlyTriggerLookup = new Map(c.fns.flatMap(f => typeof f === 'string' ? [] : [[f.name, f.onlyTriggerWithArgument] as const])); return functionFinderUtil.processSearchResult(e, c, d, async(es) => { const res: (FlowrSearchElement & { certainty: LintingResultCertainty })[] = []; for(const e of es) { - const val = await functionFinderUtil.requireArgumentValue( - e, - FnPool, - d, - c.onlyTriggerWithArgument); - + const val = await functionFinderUtil.requireArgumentValue(e, fnPool, d, onlyTriggerLookup.get(e.node.lexeme ?? '') ?? c.onlyTriggerWithArgument); if(val === Ternary.Never) { continue; } @@ -57,7 +78,8 @@ export const NETWORK_FUNCTIONS = { 'DELETE', 'PATCH', 'HEAD', 'content', 'handle', 'get_callback', 'VERB', 'fread', 'gzcon', 'readlines', 'readLines', 'source', 'load', 'curl_download', 'curl_fetch_memory', 'getURL', 'getForm', 'read_html', 'read_xml', 'html_nodes', 'html_text', 'fromJSON', 'read.xlsx', 'drive_download', 'drive_get', 's3read_using', 's3write_using', 'storage_download', 'AnnotationHub', 'ExperimentHub', 'scan', - 'socketConnection', 'request', 'curl' + 'socketConnection', 'request', 'curl', + 'copy_to', 'source_url', 'install_bioc', 'install_bitbucket', 'install_cran', 'install_dev', 'install_git', 'install_github', 'install_gitlab', 'install_svn', 'install_url', 'install_version', 'update_packages', 'github_pull', 'pak' ], onlyTriggerWithArgument: /^(https?|ftps?):\/\// } diff --git a/test/functionality/linter/lint-network-functions.test.ts b/test/functionality/linter/lint-network-functions.test.ts index d6a14d9bf8f..c3b17150221 100644 --- a/test/functionality/linter/lint-network-functions.test.ts +++ b/test/functionality/linter/lint-network-functions.test.ts @@ -53,6 +53,18 @@ describe('flowR linter', withTreeSitter(parser => { { totalCalls: 0, totalFunctionDefinitions: 0 }, { fns: ['read.csv'] } ); + assertLinter('trigger with custom url prefix', parser, 'read.csv("www.example.com")', + 'network-functions', + [{ certainty: LintingResultCertainty.Certain, function: 'read.csv', loc: [1, 1, 1, 27] }], + { totalCalls: 1, totalFunctionDefinitions: 1 }, + { fns: [{ name: 'read.csv', onlyTriggerWithArgument: /^www\./ }] } + ); + assertLinter('do not trigger with custom url prefix', parser, 'read.csv("https://example.com")', + 'network-functions', + [], + { totalCalls: 0, totalFunctionDefinitions: 0 }, + { fns: [{ name: 'read.csv', onlyTriggerWithArgument: /^www\./ }] } + ); assertLinter('do not trigger with multiple arguments', parser, 'download.file("data/local.csv", "local.csv")', 'network-functions', @@ -101,6 +113,23 @@ describe('flowR linter', withTreeSitter(parser => { { fns: ['read.csv'] } ); + assertLinter('Positional argument with custom config', parser, 'test.me(x, "http://example.com/data.csv")', + 'network-functions', + [ + { certainty: LintingResultCertainty.Certain, function: 'test.me', loc: [1, 1, 1, 41] } + ], + { totalCalls: 1, totalFunctionDefinitions: 1 }, + { fns: [{ name: 'test.me', info: { argIdx: 1 } }] } + ); + assertLinter('Named argument with custom config', parser, 'test.me(foo = "http://example.com/data.csv")', + 'network-functions', + [ + { certainty: LintingResultCertainty.Certain, function: 'test.me', loc: [1, 1, 1, 44] } + ], + { totalCalls: 1, totalFunctionDefinitions: 1 }, + { fns: [{ name: 'test.me', info: { argName: 'foo' } }] } + ); + assertLinter('Resolve value', parser, 'url <- "http://example.com/data.csv"; read.csv(url)', 'network-functions', [ @@ -126,4 +155,4 @@ describe('flowR linter', withTreeSitter(parser => { ); }); }); -})); \ No newline at end of file +}));