Skip to content
Open
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
2 changes: 1 addition & 1 deletion src/documentation/wiki-package-database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export class WikiPackageDatabase extends DocMaker<'wiki/Package Database.md'> {
flowR ships a database of CRAN package exports so it can resolve calls into the packages you load.
After \`library(ggplot2)\`, a call to \`ggplot()\` resolves to \`ggplot2::ggplot\`. This also backs
qualified names (a bare \`map()\` after \`library(purrr)\` is \`purrr::map\`, not the \`maps\` plot), the
${ctx.linkPage('wiki/Query API', 'dependencies and call-context queries')}, and the ${ctx.linkPage('wiki/Linter', '`undefined-symbol` rule')}.
${ctx.linkPage('wiki/Query API', 'dependencies and call-context queries')}, the ${ctx.linkPage('wiki/Linter', '`undefined-symbol` rule')}, and the ${ctx.linkPage('wiki/Linter', '`unused-import` rule')}.

## Configuration

Expand Down
4 changes: 3 additions & 1 deletion src/linter/linter-rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { SOFTWARE_HAS_LICENSE } from './rules/software-has-license';
import { SOFTWARE_HAS_TESTS } from './rules/software-has-tests';
import { NO_LEAKED_CREDENTIALS } from './rules/no-leaked-credentials';
import { UNDEFINED_SYMBOL } from './rules/undefined-symbol';
import { UNUSED_IMPORT } from './rules/unused-import';

/**
* The registry of currently supported linting rules.
Expand All @@ -38,7 +39,8 @@ export const LintingRules = {
'software-has-license': SOFTWARE_HAS_LICENSE,
'software-has-tests': SOFTWARE_HAS_TESTS,
'no-leaked-credentials': NO_LEAKED_CREDENTIALS,
'undefined-symbol': UNDEFINED_SYMBOL
'undefined-symbol': UNDEFINED_SYMBOL,
'unused-import': UNUSED_IMPORT
} as const;

export type LintingRuleNames = keyof typeof LintingRules;
Expand Down
114 changes: 114 additions & 0 deletions src/linter/rules/unused-import.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import {
LintingPrettyPrintContext,
type LintingResult,
LintingResultCertainty,
type LintingRule,
LintingRuleCertainty
} from '../linter-format';
import { SourceLocation } from '../../util/range';
import type { MergeableRecord } from '../../util/objects';
import { Q } from '../../search/flowr-search-builder';
import { LintingRuleTag } from '../linter-tags';
import { isNotUndefined } from '../../util/assert';
import type { Writable } from 'ts-essentials';
import { getOriginInDfg, OriginType } from '../../dataflow/origin/dfg-get-origin';
import { DfEdge, EdgeType } from '../../dataflow/graph/edge';
import type { NodeId } from '../../r-bridge/lang-4.x/ast/model/processing/node-id';
import type { PkgDb } from '../../project/plugins/package-version-plugins/pkgdb';
import { Enrichment } from '../../search/search-executor/search-enrichers';
import { DependencyInfo } from '../../queries/catalog/dependencies-query/dependencies-query-format';

Check failure on line 19 in src/linter/rules/unused-import.ts

View workflow job for this annotation

GitHub Actions / 👩‍🏫 Linting (local)

All imports in the declaration are only used as types. Use `import type`

export interface UnusedImportResult extends LintingResult{
readonly version: [string, string]
}

export interface UnusedImportConfig extends MergeableRecord {
/* Packages that only work on load and should therefore not be considered */
whitelist: string[]
};

export type UnusedImportMetadata = MergeableRecord;

/**
* Flags imported functions that are not required for the code to run. We assume this applies to packages that do not have
* an ingoing reads-edge. We only consider packages that could be resolved from the `flowr-pkgdb` database.
*/
export const UNUSED_IMPORT = {
createSearch: () => Q.fromQuery({ type: 'dependencies', 'enabledCategories': ['library'] }),
processSearchResult: async(elements, config, data) => {
const dataflow = await data.dataflow();
// needs a package database to compute
if(data.inspectContext().deps.loadedPackageDatabases().length === 0){
return { results: [], '.meta': {} };
}
const dependencyToVersion = Object.entries((config.pkgDb as PkgDb).pkgs).reduce((map, entry) => {
map.set(entry[0], entry[1][0]);

Check failure on line 45 in src/linter/rules/unused-import.ts

View workflow job for this annotation

GitHub Actions / 👩‍🏫 Linting (local)

Unsafe member access [0] on an `any` value
return map;
}, new Map());
const whitelist = new Set(config.whitelist);
const unknownIds = new Set<NodeId>(dataflow.graph.unknownSideEffects.values().map(e => { return typeof e === 'object' && 'id' in e ? e.id : e }));

Check failure on line 49 in src/linter/rules/unused-import.ts

View workflow job for this annotation

GitHub Actions / 👩‍🏫 Linting (local)

Closing curly brace should be on the same line as opening curly brace or on the line after the previous block

Check failure on line 49 in src/linter/rules/unused-import.ts

View workflow job for this annotation

GitHub Actions / 👩‍🏫 Linting (local)

Statement inside of curly braces should be on next line
let uncalledLib = elements.getElements().filter(element => {
if(unknownIds.has(element.node.info.id)){
return false;

Check failure on line 52 in src/linter/rules/unused-import.ts

View workflow job for this annotation

GitHub Actions / 👩‍🏫 Linting (local)

Expected indentation of 4 tabs but found 5
}

Check failure on line 53 in src/linter/rules/unused-import.ts

View workflow job for this annotation

GitHub Actions / 👩‍🏫 Linting (local)

Expected indentation of 3 tabs but found 4
const origins = getOriginInDfg(dataflow.graph, element.node.info.id);

Check failure on line 54 in src/linter/rules/unused-import.ts

View workflow job for this annotation

GitHub Actions / 👩‍🏫 Linting (local)

Expected indentation of 3 tabs but found 4
if(isNotUndefined(origins)) {

Check failure on line 55 in src/linter/rules/unused-import.ts

View workflow job for this annotation

GitHub Actions / 👩‍🏫 Linting (local)

Expected indentation of 3 tabs but found 4
const builtIn = origins.every(e => e.type === OriginType.BuiltInFunctionOrigin);

Check failure on line 56 in src/linter/rules/unused-import.ts

View workflow job for this annotation

GitHub Actions / 👩‍🏫 Linting (local)

Expected indentation of 4 tabs but found 5
if(!builtIn){

Check failure on line 57 in src/linter/rules/unused-import.ts

View workflow job for this annotation

GitHub Actions / 👩‍🏫 Linting (local)

Expected indentation of 4 tabs but found 5
return false;
}
}
return true;
});
//todo:idetifier get namespace
//packagedb kann :: erkennen, so kann man die libraries direkt rausfiltern
//dependency query additionalAnalysis function
// -> eigene catergorie mit den drei von additionalAnalysis und dann bekommt man die mit den punkten gar nicht überhaupt
//Todo: über identifier gehen um zu gucken ob es sich um einen identifier handel anstatt::
//all NodeIds that have an ingoing read-edge
const readEdges = new Set(dataflow.graph.edges().flatMap(e => e[1].entries()).filter(entry => {
//nodes with "::" are definitely read
if(typeof entry[0] === 'string' && entry[0].includes('::')/*Identifier.getNamespace(entry[1] as unknown as Identifier*/){
return true;
} else if(DfEdge.includesType(entry[1], EdgeType.Reads)){
return true;
} else{
return false;
}
}).map(e => e[0]));

uncalledLib = uncalledLib.filter(element => !readEdges.has(element.node.info.id));
const uncalledLibSet = new Set(uncalledLib.map(element => element.node.info.id));
const idToDependecyName = (elements.enrichmentContent(Enrichment.QueryData).queries as { dependencies: { library: DependencyInfo[] } }).dependencies.library.filter(element => uncalledLibSet.has(element.nodeId) && isNotUndefined(element.value) && !whitelist.has(element.value))
.reduce((map, element) => {
map.set(element.nodeId, element.value);
return map;
}, new Map());
return {
results:
uncalledLib.filter(element => {
//check that lib not whitelisted
return idToDependecyName.has(element.node.info.id);
}).map(element => ({
certainty: LintingResultCertainty.Uncertain,
involvedId: element.node.info.id,
loc: SourceLocation.fromNode(element.node),
version: [idToDependecyName.get(element.node.info.id), dependencyToVersion.get(idToDependecyName.get(element.node.info.id))]
})).filter(element => isNotUndefined(element.loc)) as Writable<UnusedImportResult>[],
'.meta': {}
};
},
prettyPrint: {
[LintingPrettyPrintContext.Query]: result => `Import at ${SourceLocation.format(result.loc)}`,
[LintingPrettyPrintContext.Full]: result => `Import at ${SourceLocation.format(result.loc)} is unused. Used version is ${result.version.join()}.`
},
info: {
name: 'Unused Import',
tags: [LintingRuleTag.Smell, LintingRuleTag.Readability],
certainty: LintingRuleCertainty.BestEffort,
description: 'Flags imported packages that are not required for the code to run. Packages that are only used on load might be mistaken as such and should therefore be added to the whitelist in the configuration.',
defaultConfig: {
whitelist: []
}
}
} as const satisfies LintingRule<UnusedImportResult, UnusedImportMetadata, UnusedImportConfig>;
61 changes: 61 additions & 0 deletions test/functionality/linter/lint-unused-import.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { describe } from 'vitest';
import { assertLinter } from '../_helper/linter';
import { withTreeSitter } from '../_helper/shell';
import { PkgDbBuilder } from '../../../src/project/plugins/package-version-plugins/pkgdb';
import { LintingResultCertainty } from '../../../src/linter/linter-format';

const b = new PkgDbBuilder();
b.addPackage('ggplot2', { latest: '3.5.1' });
b.addVersion('ggplot2', '3.5.1', { exported: ['ggplot', 'aes', 'geom_point'], internal: [], deprecated: [], cran: true });
b.addPackage('random1', { latest: '1.1.0' });
b.addVersion('random1', '1.0.0', { exported: ['test1', 'test2'], internal: [], deprecated: [], cran: true });
b.addVersion('random1', '1.1.0', { exported: ['test1', 'test3'], internal: [], deprecated: [], cran: true });
b.addPackage('p', { latest: '1.0' });
b.addVersion('p', '1.0', { exported: ['f'], internal: [], deprecated: [], cran: true });
const pkg = b.build('all', { version: 1, date: '2026-05-23', generated: 0 });


describe('flowR linter', withTreeSitter(parser => {
describe('unused import', () => {
assertLinter('Unused Import', parser, 'library(ggplot2)', 'unused-import', [
{
certainty: LintingResultCertainty.Uncertain,
loc: [1, 1, 1, 16],
version: ['ggplot2', '3.5.1']
},
], undefined, { pkgDb: pkg });
assertLinter('Used and unused imports', parser, 'library(p)\nlibrary(ggplot2)\nlibrary(random1)\nggplot()', 'unused-import', [
{
certainty: LintingResultCertainty.Uncertain,
loc: [1, 1, 1, 10],
version: ['p', '1.0']
},
{
certainty: LintingResultCertainty.Uncertain,
loc: [3, 1, 3, 16],
version: ['random1', '1.1.0']
},
], undefined, { pkgDb: pkg });
assertLinter('Used and unused imports with require', parser, 'require(ggplot2)\nrequire(random1)\naes()', 'unused-import', [
{
certainty: LintingResultCertainty.Uncertain,
loc: [2, 1, 2, 16],
version: ['random1', '1.1.0']
},
], undefined, { pkgDb: pkg });
assertLinter('Not in package database', parser, 'library(ggplot2)\nlibrary(random1)\nlibrary(notInDb)\naes()', 'unused-import', [
{
certainty: LintingResultCertainty.Uncertain,
loc: [2, 1, 2, 16],
version: ['random1', '1.1.0']
}
], undefined, { pkgDb: pkg });
assertLinter('Whitelisted package', parser, 'require(p)\nrequire(ggplot2)\nrequire(random1)\naes()', 'unused-import', [
{
certainty: LintingResultCertainty.Uncertain,
loc: [1, 1, 1, 10],
version: ['p', '1.0']
},
], undefined, { pkgDb: pkg, whitelist: ['random1'] });
});
}));