solhint
Version:
Solidity Code Linter
325 lines (257 loc) • 8.83 kB
JavaScript
const _ = require('lodash')
const BaseChecker = require('../base-checker')
const TreeTraversing = require('../../common/tree-traversing')
const { severityDescription } = require('../../doc/utils')
const traversing = new TreeTraversing()
const ruleId = 'no-unused-private-funcs'
const DEFAULT_SEVERITY = 'warn'
const meta = {
type: 'best-practices',
docs: {
description:
'Private function "name" is not being used within its defining contract. Support overloads.',
category: 'Best Practices Rules',
options: [
{
description: severityDescription,
default: DEFAULT_SEVERITY,
},
],
},
// Users must opt-in explicitly
recommended: false,
defaultSetup: DEFAULT_SEVERITY,
schema: null,
}
/**
* Internal structure:
*
* this.contracts = {
* [contractKey: string]: {
* contractName: string,
* funcs: Array<{
* name: string,
* paramCategories: string[], // ['integer', 'string', 'bool', 'bytes', 'unknown', ...]
* paramCount: number,
* node: FunctionDefinition,
* used: boolean
* }>,
* calls: Array<{
* name: string,
* argCategories: string[],
* argCount: number
* }>
* }
* }
*/
class NoUnusedPrivateFuncsChecker extends BaseChecker {
constructor(reporter) {
super(reporter, ruleId, meta)
// Per-contract storage of private functions + calls
this.contracts = Object.create(null)
}
/** ===== Helpers: contracts & keys ===== */
_getEnclosingContract(node) {
// Walk up the AST until we find the ContractDefinition parent
return traversing.findParentType(node, 'ContractDefinition')
}
_getContractKey(contractNode) {
// Create a stable key for each contract within the source unit
if (!contractNode) return null
return (
contractNode.name ||
(contractNode.range ? contractNode.range.join(':') : null) ||
'<anonymous>'
)
}
_getContractName(contractNode) {
// Human readable name for reporting purposes
if (!contractNode) return '<unknown contract>'
return contractNode.name || '<anonymous contract>'
}
_ensureContractEntry(contractNode) {
const key = this._getContractKey(contractNode)
if (!key) return null
if (!this.contracts[key]) {
this.contracts[key] = {
contractName: this._getContractName(contractNode),
funcs: [],
calls: [],
}
}
return key
}
/** ===== Helpers: type categorization for overload resolution ===== */
_categoryFromParamType(typeName) {
// Normalize parameter type to a coarse-grained category
if (!typeName) return 'unknown'
if (typeName.type === 'ElementaryTypeName' && typeof typeName.name === 'string') {
const t = typeName.name
if (t === 'bool') return 'bool'
if (t === 'string') return 'string'
if (t === 'address' || t === 'address payable') return 'address'
if (t.startsWith('uint') || t.startsWith('int')) return 'integer'
if (t === 'bytes') return 'bytes'
if (t.startsWith('bytes')) return 'fixed-bytes' // bytes1..bytes32
// Anything else is “other elementary”: treat as unknown for overload matching
return 'unknown'
}
// Arrays, mappings, user defined types, etc.
return 'unknown'
}
_categoryFromArgument(arg) {
// Approximate type category from argument AST node
if (!arg) return 'unknown'
switch (arg.type) {
case 'NumberLiteral':
case 'HexNumber':
return 'integer'
case 'BooleanLiteral':
return 'bool'
case 'StringLiteral':
case 'UnicodeStringLiteral':
return 'string'
case 'HexLiteral':
return 'fixed-bytes'
default:
// Identifiers, MemberAccess, FunctionCall, etc.
// We do not know the exact type here.
return 'unknown'
}
}
/** ===== Helpers: registering private functions ===== */
_markPrivateFunction(node) {
// Ignore null nodes and constructors
if (!node || node.isConstructor) return
// Fallback / receive functions do not have a name
if (!node.name) return
// We only care about private functions
if (node.visibility !== 'private') return
const contractNode = this._getEnclosingContract(node)
if (!contractNode) return
const key = this._ensureContractEntry(contractNode)
if (!key) return
const contract = this.contracts[key]
const params = Array.isArray(node.parameters) ? node.parameters : []
const paramCategories = params.map((p) => this._categoryFromParamType(p.typeName))
// Store private function metadata for later matching
contract.funcs.push({
name: node.name,
paramCategories,
paramCount: params.length,
node,
used: false,
})
}
/** ===== Helpers: collecting calls ===== */
_getCalledName(node) {
const expr = node.expression
if (!expr) return null
// Direct call: foo(...)
if (expr.type === 'Identifier') {
return expr.name
}
// Member access on `this`: this.foo(...)
if (
expr.type === 'MemberAccess' &&
expr.expression &&
expr.expression.type === 'Identifier' &&
expr.expression.name === 'this'
) {
return expr.memberName
}
return null
}
_recordFunctionCall(node) {
const contractNode = this._getEnclosingContract(node)
if (!contractNode) return
const key = this._ensureContractEntry(contractNode)
if (!key) return
const contract = this.contracts[key]
const calledName = this._getCalledName(node)
if (!calledName) return
const args = Array.isArray(node.arguments) ? node.arguments : []
const argCategories = args.map((a) => this._categoryFromArgument(a))
// Store call information; we will resolve it after the whole source unit is visited
contract.calls.push({
name: calledName,
argCategories,
argCount: args.length,
})
}
/** ===== Helpers: resolving calls to functions (after traversal) ===== */
_resolveCallsAndMarkUsed() {
// For each contract, walk over all recorded calls and mark matching private functions as used
_.forEach(this.contracts, (contract) => {
contract.calls.forEach((call) => {
const { name, argCategories, argCount } = call
// 1) Filter by name + number of parameters
const candidates = contract.funcs.filter(
(f) => f.name === name && f.paramCount === argCount
)
if (candidates.length === 0) {
return
}
if (candidates.length === 1) {
// Only one overload can match → mark it as used
candidates[0].used = true
return
}
// 2) Try to narrow down by argument / parameter categories
const strongMatches = candidates.filter((f) => {
// All parameters must be compatible with arg categories
return f.paramCategories.every((cat, i) => {
const argCat = argCategories[i] || 'unknown'
if (argCat === 'unknown' || cat === 'unknown') {
// We don't have enough info to disambiguate on this position
return true
}
// Both known → must match exactly
return cat === argCat
})
})
if (strongMatches.length === 1) {
// Exactly one overload is clearly compatible
strongMatches[0].used = true
return
}
// 3) Ambiguous: mark all candidates as used (conservative, avoids false positives)
candidates.forEach((f) => {
f.used = true
})
})
})
}
/** ===== Reporting ===== */
_reportUnusedPrivateFunctions() {
// First, resolve all calls against the registered functions
this._resolveCallsAndMarkUsed()
// Then, report any private function that is still unused
_.forEach(this.contracts, (contract) => {
contract.funcs.forEach((f) => {
if (!f.used) {
const funcName = f.name
const contractName = contract.contractName || '<unknown contract>'
this.warn(
f.node,
`Private function "${funcName}" in contract "${contractName}" is never used within its defining contract`
)
}
})
})
}
/** ===== AST hooks ===== */
FunctionDefinition(node) {
// Collect all private functions (including overloads)
this._markPrivateFunction(node)
}
FunctionCall(node) {
// Collect calls for later resolution (after full traversal)
this._recordFunctionCall(node)
}
'SourceUnit:exit'() {
// When the whole source unit has been traversed, resolve calls and report unused private functions
this._reportUnusedPrivateFunctions()
}
}
module.exports = NoUnusedPrivateFuncsChecker