UNPKG

solhint-community

Version:
298 lines (259 loc) 8.84 kB
const _ = require('lodash') const BaseChecker = require('../base-checker') const TreeTraversing = require('../../common/tree-traversing') const { contractWith, funcWith } = require('../../../test/common/contract-builder') const { findParentType, findFirstParent } = new TreeTraversing() const ruleId = 'no-unused-vars' const meta = { type: 'best-practises', docs: { description: 'Ensure defined names are used', category: 'Best Practise Rules', examples: { good: [ { description: 'imported name is used', code: ` import {A} from './A.sol'; contract Foo is A{ } `, }, { description: 'defined stack variable is used', code: contractWith('function fun(uint a) public { uint b = bytes32(a); b += 1; }'), }, { description: 'note: function parameters of functions with empty blocks are not checked', code: contractWith('function fun(uint d) public returns (uint c) { }'), }, { description: 'note: function parameters of functions without blocks are not checked', code: contractWith('function fun(uint a, uint b) public returns (uint c);'), }, { description: 'note: state variables are not checked', code: contractWith('uint public foo;'), }, ], bad: [ { description: 'imported name is not used', code: ` import {A} from './A.sol'; contract Foo { } `, }, { description: 'stack variable is not used', code: funcWith('uint a = 0;'), }, { description: 'function parameter is not used', code: contractWith('function fun(uint a) public returns (uint){ return 42; }'), }, ], }, }, recommended: true, defaultSetup: 'warn', schema: null, } class NoUnusedVarsChecker extends BaseChecker { constructor(reporter, tokens) { super(reporter, ruleId, meta) this.tokens = tokens } // imported variables need a scope to be tracked in SourceUnit(node) { VarUsageScope.activate(node) } 'SourceUnit:exit'(node) { // get inheritdoc statements from the lexed (but not parsed) input const keywords = this.tokens.filter((it) => it.type === 'Keyword') const inheritdocStatements = keywords.filter( ({ value }) => /^\/\/\/ *@inheritdoc/.test(value) || /^\/\*\* *@inheritdoc/.test(value) ) // mark inheritdoc'd names as used in the SourceUnit scope, since it only makes // sense to extend the documentation of a name defined somewhere else. // this doesn't break since all items that can have natspec (from the language docs): // > Documentation is inserted above each contract, interface, library, function, and event // cannot be inside a block inheritdocStatements.forEach(({ value }) => { const match = value.match(/@inheritdoc *([a-zA-Z0-9_]*)/) if (match && match[1]) { node.scope.trackVarUsage(match[1]) } }) node.scope.unusedVariables().forEach(this._error.bind(this)) } Block(node) { if (node.parent?.type === 'FunctionDefinition') { return } VarUsageScope.activate(node) } 'Block:exit'(node) { if (node.parent?.type === 'FunctionDefinition') { return } node.scope.unusedVariables().forEach(this._error.bind(this)) } 'FunctionDefinition:exit'(node) { node.scope.unusedVariables().forEach(this._error.bind(this)) } VariableDeclarationStatement(node) { // skip the definitions of function parameters, they're taken care of by // Block visitor if (node.parent.type === 'FunctionDefinition') return node.variables.forEach((variable) => this._addVariable(variable)) } AssemblyLocalDefinition(node) { node.names.forEach((variable) => this._addVariable(variable)) } FunctionDefinition(node) { // don't apply this rule for function _declarations_ as parameters can't // really be used, but giving them names aids in documentation. if (node.body === null) { return } if (node.isConstructor) { // track usage of extended contracts in member initialization list node.modifiers.forEach((it) => this._trackVarUsage(it)) } VarUsageScope.activate(node) // filters 'payable' keyword as name when defining function like this: // function foo(address payable) public view {}. I believe this is a parser limitation node.parameters .filter((parameter) => { if ( parameter.typeName.name === 'address' && parameter.typeName.stateMutability === null && parameter.name === 'payable' ) return null else return parameter.name }) .forEach(({ name, typeName }) => { node.scope.addVar(typeName, name) }) node.modifiers.forEach((modifier) => modifier.arguments.forEach((argument) => this._trackVarUsage(argument)) ) } UserDefinedTypeName(node) { this._trackVarUsage(node, true) } UsingForDeclaration(node) { if (node.libraryName) { this._trackVarUsage(node) } // It's okay to not search upwards for the scope containing it, since it's // defined at the grammar level that using-for directives can only be childs // of source units or contract definitions const scope = VarUsageScope.of(node) // the name can be either the imported thing, or a member access of it node.functions.forEach((it) => scope.trackVarUsage(it.split('.')[0])) } Identifier(node) { this._trackVarUsage(node) } ImportDirective(node) { if (node.unitAlias) { node.unitAliasIdentifier.parent = node this._addVariable(node, node.unitAliasIdentifier.name) } else { node.symbolAliasesIdentifiers.forEach((it) => { const idNode = it[1] || it[0] // I have to define the parent manually because of a bug with ast-parents // see #119 Object.defineProperty(idNode, 'parent', { value: node, configurable: true, enumerable: false, writable: true, }) this._addVariable(idNode) }) } } AssemblyCall(node) { const firstChild = node.arguments.length === 0 && node if (firstChild) { firstChild.name = firstChild.functionName this._trackVarUsage(firstChild) } } _addVariable(idNode, customName) { const scope = VarUsageScope.of(idNode) if (idNode && scope) { scope.addVar(idNode, customName || idNode.name) } } _trackVarUsage(node, trackInsideVarDeclaration = false) { if (!trackInsideVarDeclaration && this._isVarDeclaration(node)) return // nodes passed here can be of various types, and have its 'name' in different fields // plus the name itself can be a member access of the name that's actually defined const name = (node.name || node.namePath || node.libraryName).split('.')[0] // if I can't find the name here, search upwards for a scope that might // have it while (!VarUsageScope.of(node).hasVar(name)) { node = node.parent } VarUsageScope.of(node).trackVarUsage(name) } _error({ name, node }) { this.warn(node, `Variable "${name}" is unused`) } _isVarDeclaration(node) { const importDirective = findParentType(node, 'ImportDirective') const variableDeclaration = findParentType(node, 'VariableDeclaration') const identifierList = findParentType(node, 'IdentifierList') const parameterList = findParentType(node, 'ParameterList') const assemblyLocalDefinition = findParentType(node, 'AssemblyLocalDefinition') // otherwise `let t := a` doesn't track usage of `a` const isAssemblyLocalDefinition = assemblyLocalDefinition && assemblyLocalDefinition.names && assemblyLocalDefinition.names.includes(node) return !!( variableDeclaration || identifierList || parameterList || isAssemblyLocalDefinition || importDirective ) } } class VarUsageScope { static of(node) { return findFirstParent(node, (it) => !!it.scope)?.scope } static activate(node) { node.scope = new VarUsageScope() } static isActivated(node) { return !!node.scope } constructor() { this.vars = {} } addVar(node, name) { this.vars[name] = { node, usage: 0 } } hasVar(name) { return this.vars[name] !== undefined } trackVarUsage(name) { const curVar = this.vars[name] if (curVar) { curVar.usage += 1 } } unusedVariables() { return _(this.vars) .pickBy((val) => val.usage === 0) .map((info, varName) => ({ name: varName, node: info.node })) .value() } } module.exports = NoUnusedVarsChecker