Warn on unused local variables#2139
Warn on unused local variables#2139chriseth merged 7 commits intoargotorg:developfrom roadriverrail:warn_unused
Conversation
| /// Flag that indicates whether a public function does not contain the "payable" modifier. | ||
| bool m_nonPayablePublic = false; | ||
|
|
||
| std::map<VariableDeclaration const*, int> m_localVarUseCount; |
|
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.
Is it possible to silence the warning by using function f(uint) { ... }?
There was a problem hiding this comment.
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.
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.
Can you add a test for silencing?
There was a problem hiding this comment.
@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.
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.
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.
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.
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.
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.
I see. A VariableDeclaration is never a state variable?
There was a problem hiding this comment.
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.
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.
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.
| 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.
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.
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.
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.
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.
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.
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.
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.
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.
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
| 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.
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.
I think none of these brackets are needed.
|
|
||
| std::map<VariableDeclaration const*, int> m_localVarUseCount; | ||
|
|
||
| bool m_inFunction = false; |
| /// 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.
Maybe a comment wouldn't hurt here saying counter starts from 1.
There was a problem hiding this comment.
Really, the counter starts at 0 and is incremented on every use.
There was a problem hiding this comment.
Yeah, it was a typo. Declaration is not part of "use".
There was a problem hiding this comment.
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.
Cannot we actually just have an empty function body here?
| uint a; | ||
| } | ||
| } | ||
| )"; |
| var a = 1; | ||
| } | ||
| } | ||
| )"; |
| char const* text = R"( | ||
| contract C { | ||
| function f() { | ||
| a = 7; |
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_inFunction = false; | ||
| m_nonPayablePublic = false; | ||
| for (auto const& var : m_localVarUseCount) |
There was a problem hiding this comment.
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.
Please remove space before closing parenthesis and braces below.
| return supply; | ||
| } | ||
|
|
||
| function transfer(address _to, uint256 _value) returns (bool success) { |
There was a problem hiding this comment.
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.
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.
| contract test { | ||
| uint256 stateVariable1; | ||
| function fun(uint256 arg1) { uint256 y; } | ||
| function fun(uint256) { uint256 y; } |
There was a problem hiding this comment.
This should still flag y as unused. Can you change that to use the variables instead?
There was a problem hiding this comment.
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.
| contract test { | ||
| function fun() { uint x; } | ||
| function fun() { uint x; } | ||
| function fun() { uint x; x; } |
There was a problem hiding this comment.
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.
@axic noted we can also just make these empty function bodies.
| ASTPointer<SourceUnit> sourceUnit; | ||
| char const* text = R"( | ||
| contract test { | ||
| function functionName(bytes32 input) returns (bytes32 out); |
There was a problem hiding this comment.
This change should not be necessary, right?
There was a problem hiding this comment.
In light of a recent change to waive unused variable checks, that is correct.
| 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.
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.
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.
m_localVarUseCount.clear() could be moved here.
There was a problem hiding this comment.
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.
Following that logic shouldn't then the function be set to nullptr in an else case?
There was a problem hiding this comment.
The new commit makes it consistent and will throw once we add nested function declarations.
| bool StaticAnalyzer::visit(Identifier const& _identifier) | ||
| { | ||
| if (m_currentFunction) | ||
| { |
| // 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.
Would m_localVarUseCount[_variable] += 0; less confusing than the big text?
There was a problem hiding this comment.
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.
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.
We said that returning a value counts as using a variable.
| contract test { | ||
| uint256 variable; | ||
| function f() { uint32 variable; } | ||
| function f() { uint32 variable; variable = 2; } |
There was a problem hiding this comment.
Perhaps uint32 variable = 2; would be enough, though it makes no difference.
| uint[] storage x; | ||
| uint[] memory y; | ||
| uint[] memory z; | ||
| x;y;z; |
There was a problem hiding this comment.
Will these trigger the empty statement warning? Also probably it could be spaced in three lines?
There was a problem hiding this comment.
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.
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