-
Notifications
You must be signed in to change notification settings - Fork 5.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Warn on unused local variables #2139
Conversation
@@ -69,6 +75,10 @@ class StaticAnalyzer: private ASTConstVisitor | |||
|
|||
/// Flag that indicates whether a public function does not contain the "payable" modifier. | |||
bool m_nonPayablePublic = false; | |||
|
|||
std::map<VariableDeclaration const*, int> m_localVarUseCount; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please indent this properly.
Can you add a test for |
@axic Sure. Just to ensure that we're all clear, I would claim that "var a = 1;" on its own does not count as using the variable. Correct? |
{ | ||
char const* text = R"( | ||
contract C { | ||
function f(uint a) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it possible to silence the warning by using function f(uint) { ... }
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I checked, and it is. It's pretty nonsensical to have a parameter you don't name and don't use, but it does silence the warning.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It can be useful if you have either default implementations or overriden methods that don't make use of all parameters in the declared interface.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add a test for silencing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@federicobond good point
@axic I will do that
int errorCount = 0; | ||
for (auto const& currentError: errors) | ||
{ | ||
if (currentError->type() != Error::Type::Warning) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If possible, I would prefer to keep this as strict as it is now. How many tests would need changes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have a solid count, but it's a significant portion of the tests in SolidityNameAndTypeResolution.cpp
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've gone ahead and silenced all the unused warnings in those tests. It's mostly un-naming parameters or adding meaningless statements to code blocks. A little ugly to me, but not bad.
return true; | ||
} | ||
|
||
void StaticAnalyzer::endVisit(Identifier const&) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please remove this here and in the header file (also endVisit for VariableDeclaration).
// There's no real way to get a good tracking on unnamed variables in someone's | ||
// code. These can show up in the returns clause of a function declaration, | ||
|
||
if (_variable.isLocalVariable() && _variable.name() != "") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you change the first part to solAssert(_variable.isLocalVariable(), "")
because all of them should be local variables.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. A VariableDeclaration is never a state variable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
VariableDeclaration "stateVariable1" Type: uint256 Gas costs: 0 Source: "uint256 stateVariable1" ElementaryTypeName uint256 Source: "uint256"
Looks like state variables are VariableDeclarations, which is what I'd thought, and that's why I was using the first part to filter them off.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I see. Since all the work is scoped inside functions, perhaps it would be better to set a separate flag that says "we are inside a function" and then assert that the flag is set if and only if the variable is a local variable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure. I can do that. I'd actually thought about using a flag like that, but I don't like to add flags if I don't have to, so I was just filtering on local variables.
m_nonPayablePublic = _function.isPublic() && !_function.isPayable(); | ||
return true; | ||
} | ||
|
||
void StaticAnalyzer::endVisit(FunctionDefinition const&) | ||
{ | ||
m_nonPayablePublic = false; | ||
for (auto var = m_localVarUseCount.begin(); var != m_localVarUseCount.end(); ++var) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you use a range-based for?
for (auto const& var: m_localVarUseCount)
if (var.second == 0)
warning(var.first->location(), "Unused local variable");
{ | ||
if (auto var = dynamic_cast<VariableDeclaration const*>(_identifier.annotation().referencedDeclaration)) | ||
{ | ||
if (var->isLocalVariable() && var->name() != "") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think you can reference a variable that does not have a name, because then the identifier would also have no name. So can you change this to solAssert(!var->name().empty, "")
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm. Good point. You can't visit an identifier of nothing.
{ | ||
if (var->isLocalVariable() && var->name() != "") | ||
{ | ||
if (m_localVarUseCount.find(var) != m_localVarUseCount.end()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This assumes that the variable declaration is visited first. Why do you need this check at all?
Please add a test for
contract c {
function f() {
a = 7;
uint a;
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Understood and will fix. I am curious, though, why Solidity doesn't enforce declaration-before-use. It seems to me like this doesn't make for more readable code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is a stupid rule inherited from javascript. We plan to "fix" that by adding syntax for let a: uint = 7
.
|
||
if (_variable.isLocalVariable() && _variable.name() != "") | ||
{ | ||
// We don't have to check if the variable was already inserted |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure this is actually correct. You could do something like m_localVarUseCount[&_variable]
- then the slot will be created if it does not exist and set to zero.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment is clearly ambiguous. I was attempting to say that we don't have to worry about something like:
uint a; uint a;
Because the parser would have already rejected it. I can just remove the comment. If redundant declarations had not already been filtered out, then m_localVarUseCount[&_variable] = 0
isn't particularly safe to use because it would cause the counter to reset to 0 when the next declaration was encountered.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Of course, I can't uncritically perform m_localVarUseCount[&_variable] = 0
if variables can be used before they're declared, but I can work around that.
Correct in my view. |
So, this is blowing the Travis CI build because Token.sol is full of "unused variables." I personally think that, in a function with no implementation, there's no point to policing unused variables, since often variable names help document the semantic value of the parameter to people who end up implementing the function. I'll see if I can find a way to suppress the warnings in this case. |
I think Can you add a test that interfaces are not checked for unused variables? Functions in interfaces cannot have bodies, so I assume it already works without problems. Interface example:
|
Can you please rebase this pull request (and also remove any extra merge commits while doing so)? |
Changelog.md
Outdated
@@ -3,6 +3,7 @@ | |||
Features: | |||
* Implement the Standard JSON Input / Output API | |||
* Support ``interface`` contracts. | |||
* Warns of unused local variables, parameters, and return parameters. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we prefix this with Static Analyser
?
Sure. I'll add the test. That shouldn't be an issue. I can also change
Token to an interface. I think rather the rule of turning off the unused
var warnings for all functions with no implementation will catch it all
pretty cleanly.
…On Mon, Apr 24, 2017 at 2:51 PM Alex Beregszaszi ***@***.***> wrote:
So, this is blowing the Travis CI build because Token.sol is full of
"unused variables."
I think Token.sol should be an interface.
Can you add a test that interfaces are not checked for unused variables?
Functions in interfaces cannot have bodies, so I assume it already works
without problems.
Interface example:
interface Token {
function transfer(address from, address to, uint value);
}
—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub
<#2139 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AAlhYGImjOLIC8NcsOBbqUGj7708zS-pks5rzRlTgaJpZM4NDK84>
.
|
Yep. I'll do so after adding the test you requested and fixing up
Changelog.
…On Mon, Apr 24, 2017 at 2:53 PM Alex Beregszaszi ***@***.***> wrote:
Can you please rebase this pull request (and also remove any extra merge
commits while doing so)?
—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub
<#2139 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AAlhYMmqTjr__s22bfwJnKaD1Y6UdPyVks5rzRmGgaJpZM4NDK84>
.
|
{ | ||
warning(var.first->location(), "Unused local variable"); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think none of these brackets are needed.
|
||
std::map<VariableDeclaration const*, int> m_localVarUseCount; | ||
|
||
bool m_inFunction = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have to be indented.
@@ -69,6 +73,10 @@ class StaticAnalyzer: private ASTConstVisitor | |||
|
|||
/// Flag that indicates whether a public function does not contain the "payable" modifier. | |||
bool m_nonPayablePublic = false; | |||
|
|||
std::map<VariableDeclaration const*, int> m_localVarUseCount; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe a comment wouldn't hurt here saying counter starts from 1.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Really, the counter starts at 0 and is incremented on every use.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, it was a typo. Declaration is not part of "use".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So than a comment saying the counter starts from 0 :)
function fun() { uint x; } | ||
function fun() { uint x; } | ||
function fun() { uint x; x; } | ||
function fun() { uint x; x; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cannot we actually just have an empty function body here?
uint a; | ||
} | ||
} | ||
)"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indent.
var a = 1; | ||
} | ||
} | ||
)"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indent.
char const* text = R"( | ||
contract C { | ||
function f() { | ||
a = 7; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indent.
I wouldn't do that in this PR. That wasn't done yet because there's no release which has interfaces and also that would break older compilers relying on |
m_nonPayablePublic = false; | ||
for (auto const& var : m_localVarUseCount) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please remove space in front of :
and braces below.
if (auto var = dynamic_cast<VariableDeclaration const*>(_identifier.annotation().referencedDeclaration)) | ||
{ | ||
solAssert(!var->name().empty(), ""); | ||
if (var->isLocalVariable() ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please remove space before closing parenthesis and braces below.
std/StandardToken.sol
Outdated
@@ -21,11 +21,11 @@ contract StandardToken is Token { | |||
return supply; | |||
} | |||
|
|||
function transfer(address _to, uint256 _value) returns (bool success) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, as you already also said at some point: It might be quite descriptive to keep these names (the variable might also just be named failure
).
Perhaps we can count "return with value" as usage of the return variables?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can code that up, but it feels like a real kludge that is avoiding the issue. The issue here is that return variables are getting used for two different things. The first is that they are used to create a variable you can assign a value to. The second is that they are strings which document the meaning of the return value. The compiler is very much written with the first use in mind. This is to say that the parser does not make a very strong distinction between an output parameter and any other locally declared variable.
So, I feel like if you're going to create a variable to document your output's meaning, you should probably also have to use the variable. I've gone ahead and made a change here to actually use success
rather than remove it. It doesn't feel much harder to assign your output parameters and let the return happen automatically than to do a simple return
.
If you don't like the way it looks, I'll look into "return with value" counting as a use of the return variables.
@@ -216,7 +216,7 @@ BOOST_AUTO_TEST_CASE(smoke_test) | |||
char const* text = R"( | |||
contract test { | |||
uint256 stateVariable1; | |||
function fun(uint256 arg1) { uint256 y; } | |||
function fun(uint256) { uint256 y; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should still flag y
as unused. Can you change that to use the variables instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will. By the way, the reason the test passes is because there is only 1 warning, so the "multiple errors" check doesn't fail, and since the only reported "error" is a warning, it doesn't count as an error on the final check.
@@ -237,8 +237,8 @@ BOOST_AUTO_TEST_CASE(double_function_declaration) | |||
{ | |||
char const* text = R"( | |||
contract test { | |||
function fun() { uint x; } | |||
function fun() { uint x; } | |||
function fun() { uint x; x; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will actually trigger another warning (in a different PR): Side-effect free statement.
I think you can silence both by using x = 1
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@axic noted we can also just make these empty function bodies.
@@ -553,7 +553,7 @@ BOOST_AUTO_TEST_CASE(function_no_implementation) | |||
ASTPointer<SourceUnit> sourceUnit; | |||
char const* text = R"( | |||
contract test { | |||
function functionName(bytes32 input) returns (bytes32 out); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change should not be necessary, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In light of a recent change to waive unused variable checks, that is correct.
@@ -2963,7 +2967,7 @@ BOOST_AUTO_TEST_CASE(cyclic_binary_dependency_via_inheritance) | |||
BOOST_AUTO_TEST_CASE(multi_variable_declaration_fail) | |||
{ | |||
char const* text = R"( | |||
contract C { function f() { var (x,y); } } | |||
contract C { function f() { var (x,y); x;y;} } | |||
)"; | |||
CHECK_ERROR(text, TypeError, ""); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add the correct error message if you change this, so it will not change to a different error without us noticing.
uint a; | ||
} | ||
} | ||
)"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please indent this one tab (also in the other tests below).
This should now be rebased, cleaned up, etc. |
(Also, I promise that I've fixed my |
Could you also add code that |
I can add that. I'm sure there's enough legacy code out there doing its returns that way that we kind of have to. |
I hope you don't mind me hijacking this PR, I would like to get this into the next release. |
@chriseth can you rebase please? |
Rebased. |
for (auto const& var: m_localVarUseCount) | ||
if (var.second == 0) | ||
warning(var.first->location(), "Unused local variable"); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
m_localVarUseCount.clear()
could be moved here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is safer to at least also keep it in visit()
, but I'll add other cleaning calls here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Following that logic shouldn't then the function be set to nullptr
in an else case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new commit makes it consistent and will throw once we add nested function declarations.
bool StaticAnalyzer::visit(Identifier const& _identifier) | ||
{ | ||
if (m_currentFunction) | ||
{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this parentheses needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
will remove
// but since [] will insert the default 0, we really just | ||
// need to access the map here and let it do the rest on its | ||
// own. | ||
m_localVarUseCount[&_variable]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would m_localVarUseCount[_variable] += 0;
less confusing than the big text?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will change and add a comment that it is not a no-op.
@@ -53,7 +53,7 @@ contract StandardToken is Token { | |||
return true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this function not caught by it? It has success
, but is never set. Same goes for doTransfer
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We said that returning a value counts as using a variable.
@@ -262,7 +262,7 @@ BOOST_AUTO_TEST_CASE(name_shadowing) | |||
char const* text = R"( | |||
contract test { | |||
uint256 variable; | |||
function f() { uint32 variable; } | |||
function f() { uint32 variable; variable = 2; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps uint32 variable = 2;
would be enough, though it makes no difference.
@@ -2570,6 +2570,7 @@ BOOST_AUTO_TEST_CASE(storage_location_local_variables) | |||
uint[] storage x; | |||
uint[] memory y; | |||
uint[] memory z; | |||
x;y;z; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will these trigger the empty statement warning? Also probably it could be spaced in three lines?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I actually think those might not even be necessary because most tests do not require "no warnings".
|
||
// string literal | ||
var i = true ? "hello" : "world"; | ||
i = "used"; //Avoid unused var warning |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extra space after //
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We do not have a proper style in the tests anyway....
Fails on this:
|
Analyze functions for all local variables, parameters, and named return variables which are never used in the function, and issue a warning.
There are many cases of code where the return parameters exist mostly as a form of documentation. This change ensures that they do not have to be used in the function body so long as there is a return supplying values
Analyze functions for all local variables, parameters, and named
return variables which are never used in the function, and issue
a warning.
This partially resolves issue #718