8.5. Remove unused definitions#
In the previous section we've seen how to find unused definitions. Let's now proceed to remove them. For that, we are going to use BaseRewriter
, an abstract class that allow us to replace nodes in a tree.
The task, simply put, is to delete those nodes whose id is present in the list of unused definitions (constructed previously). For this example, we focus only in three definitions: functions, state variables, and modifiers. Expanding to other definitions is trivial.
Our RemoveUnusedDefs
class inherits from BaseRewriter
, and overrides those methods of interest: rewriteFunctionDefinition
, rewriteStateVariableDefinition
, and rewriteModifierDefinition
. In turn, each method forwards the execution to the helper function removeUnused
, which is the one checking if the node id is present in the provided list of unused definitions.
If the node corresponds to an unused definition, then the helper ——and therefore, the overwritten method—— returns undefined
. This is the way in which we signal the BaseRewriter
class to remove the node.
If the node is not in the list, it's returned as-is.
import { Definition } from "@nomicfoundation/slang/bindings";
import { BaseRewriter, Node, NonterminalNode } from "@nomicfoundation/slang/cst";
export class RemoveUnusedDefs extends BaseRewriter {
constructor(private readonly unusedDefinitions: Definition[]) {
super();
}
private removeUnused(node: NonterminalNode): Node | undefined {
const foundUnused = this.unusedDefinitions.find((definition) => definition.id == node.id);
if (foundUnused) {
// returning `undefined` signals that the node must be deleted
return undefined;
} else {
return node;
}
}
public override rewriteFunctionDefinition(node: NonterminalNode): Node | undefined {
return this.removeUnused(node);
}
public override rewriteStateVariableDefinition(node: NonterminalNode): Node | undefined {
return this.removeUnused(node);
}
public override rewriteModifierDefinition(node: NonterminalNode): Node | undefined {
return this.removeUnused(node);
}
}
We can test the functionality on the same Solidity example from last section, noting that functions checkOwner
in Ownable
and unusedDecrement
in Owner
are not present in the result, as well as the state variable _unused
in the latter contract.
import assert from "node:assert";
import { findUnusedDefinitions } from "../../04-find-unused-definitions/examples/find-unused-definitions.mjs";
import { CONTRACT_VFS } from "../../04-find-unused-definitions/examples/test-find-unused-definitions.test.mjs";
import { buildCompilationUnit } from "../../common/compilation-builder.mjs";
import { RemoveUnusedDefs } from "./remove-unused-defs.mjs";
const EXPECTED_VFS = new Map<string, string>([
[
"contract.sol",
`
abstract contract Ownable {
address _owner;
constructor() {
_owner = msg.sender;
}
modifier onlyOwner() {
require(_owner == msg.sender);
_;
}
}
contract Counter is Ownable {
uint _count;
constructor(uint initialCount) {
_count = initialCount;
}
function count() public view returns (uint) {
return _count;
}
function increment(uint delta, uint multiplier) public onlyOwner returns (uint) {
require(delta > 0, "Delta must be positive");
_count += delta;
return _count;
}
}
`,
],
]);
test("remove unused definitions", async () => {
const unit = await buildCompilationUnit(CONTRACT_VFS, "0.8.0", "contract.sol");
const unused = findUnusedDefinitions(unit, "Counter");
const removeUnused = new RemoveUnusedDefs(unused);
for (const file of unit.files()) {
const newNode = removeUnused.rewriteNode(file.tree);
assert.strictEqual(newNode?.unparse(), EXPECTED_VFS.get(file.id));
}
});