-
Notifications
You must be signed in to change notification settings - Fork 6.3k
Fix shadowing struct types by struct member names #10908
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
Fix shadowing struct types by struct member names #10908
Conversation
| } | ||
| // ---- | ||
| // TypeError 5172: (25-26): Name has to refer to a struct, enum or contract. | ||
| // DeclarationError 7920: (25-26): Identifier not found or not unique. |
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.
Isn't type error the correct error to output 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.
Could be, but then it would be the only type error in ReferencesResolver.
| struct S {E X; uint E;} | ||
| struct T {E T; uint E;} | ||
| struct U {E E;} | ||
| } |
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 this test pass?
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.
According to my understanding, yes, because we allow struct members to have same names as types.
a49107d to
d30aa71
Compare
|
This sounds like it is fixing a special case and a more general solution is possible. The issue at hand is that we do register a name that could shadow, but there is no code from which it could be accessed by that name. The same is true for a different but similar situation: as opposed to The second should create an error, the first does not have to. The same is true for event parameter names. Are these two situations similar or not? |
Currently both samples produce the same warning:
Do you mean something like this: It compiles without the shadowing warning or error (which is fine I guess). I'm having a closer look to see if we can have some unification. |
873d46f to
4312277
Compare
|
The solution looks good! |
|
The implemented or discussed solution? |
53b0276 to
2905ea5
Compare
I would say that name is self-sufficient if it does not require any qualifier to identify to the corresponding entity :). |
|
Is this ready for review again? |
Commits are not squashed and there is no changelog entry, but otherwise I believe it can be reviewed. |
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 functionality looks finished to me, the PR just needs some final polish.
There are a some unused leftovers from previous iterations, commits are unsquashed and minor style tweaks are possible in a few places. It needs to be updated for custom errors and, as stated above, the changelog entry is missing. Also, it could use a few more test cases.
2905ea5 to
e531fc8
Compare
e531fc8 to
1ff57a4
Compare
|
Changelog, tweaks and extra tests added, commits squashed, PR rebased. This is now ready for the final review. |
Changelog.md
Outdated
|
|
||
|
|
||
| Bugfixes: | ||
| * Name Resolver: Prevent member/parameter names from shadowing type names within struct/enum/error/event declarations. |
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.
Do we issue an error in case of shadowing? I'm trying to understand how we prevent shadowing, because users could still do it, 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.
There's no error. That's the point of the PR. There are some examples in the original issue (#9231) that should make this clearer.
For example before this PR you would get an error here:
enum E {a, b, c}
struct T {E T; uint E;}Error: Name has to refer to a struct, enum or contract.
--> test.sol:2:11:
|
2 | struct T {E T; uint E;}
| ^
That's because the name checker thinks that uint E is shadowing the type E inside the body of the struct.
The PR changes that by making name checker aware that names defined in some kinds of scopes (struct declarations, enum declarations, function declarations without a body, event/error declarations) cannot shadow anything inside that declaration.
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.
Prevent member/parameter names from shadowing type names within struct/enum/error/event declarations.
I think, strictly speaking, we do not prevent shadowing, we rather prevent recognition of non-existing shadowing as shadowing. Maybe
Allow member/parameter names to be the same as type names within struct/enum/error/event declarations (avoid false name shadowing errors)
BTW, thanks a lot for taking care of the PR! (I still do hope to come back to the project for real eventually, even if right now it does not sound realistic.)
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.
Well, yeah, I get your point but I have a really hard time expressing this in a changelog entry without making it confusing to users or taking a whole paragraph :) This is the best I could come up with. If anyone has a better suggestion, I can replace it.
BTW, thanks a lot for taking care of the PR! (I still do hope to come back to the project for real eventually, even if right now it does not sound realistic.)
Nice to heard that. I hope we'll get you back at some point :)
|
|
||
| function f() public pure {} | ||
| } | ||
| // ---- |
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.
Shouldn't this issue a compiler error? Because enum member shadows type 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.
No, because shadowing is legal. It's only when you shadow type names used later within the same scope that it's an error:
{
Type1 Type2 = 1;
Type2 x = 2;
}You cannot do that in an enum. I added this test mostly for completeness.
test/libsolidity/syntaxTests/errors/error_param_type_shadowed_by_param_name.sol
Show resolved
Hide resolved
|
|
||
| abstract contract A { | ||
| function f(uint A) public pure {} // warning | ||
| function g(uint A) public virtual; // OK |
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 "OK"? Shadowing still exists 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.
See #10908 (comment).
Having a parameter named like a type inside a function declaration that has no body cannot break anything so we do not consider that shadowing. It's clear what it's referring to. You don't want to warn about it and annoy the user. When there is a body, on the other hand, warning makes sense.
1ff57a4 to
9082d2a
Compare
9082d2a to
059a1c9
Compare
test/libsolidity/syntaxTests/errors/error_param_type_shadowed_by_param_name.sol
Show resolved
Hide resolved
059a1c9 to
6d8b819
Compare
6d8b819 to
d7cee56
Compare
|
Just to summarize the current status: there were some review comments above but all have been addressed. The PR is finished and just waiting for an approve. |
hrkrshnn
left a comment
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.
Looks good. One minor comment, approving anyway.
| /// @param _onlyVisibleAsUnqualifiedNames if true, only include declarations which can be | ||
| /// referenced outside of the current scope without being qualified with the name of the | ||
| /// scope. I.e. when they shadow names in the enclosing scope(s). This option is **not** | ||
| /// passed to the enclosing scope when used together with @a _recursive. |
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.
Do you think we should add an assert for _recursive and _onlyVisibleAsUnqualifiedNames both being 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.
I actually did that at first but the assertion fails :) We do call it with both being true and this is the expected behavior unfortunately.
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 the comment here correct, then?
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.
Trying to answer your question pulled me down a rabbit hole today and I found a new problem with the PR. So it needs more work unfortunately :(
Is the comment here correct, then?
Yes, it's correct in that respect because resolveName() does set _onlyVisibleAsUnqualifiedNames in the recursive call to false:
m_enclosingContainer->resolveName(_name, true, _alsoInvisible, false);The only thing that the assert would change would be not having to document this case. But it turns out that this case does actually happen in practice.
But your comment made me think whether this is really the right behavior. And I think it's not so I changed it to propagate the value of the parameter. It's not a problem in practice though because I think it's not possible to trigger this case it with any currently allowed syntax.
I tried to find a test case that would behave differently depending on whether we propagate _onlyVisibleAsUnqualifiedNames or not and I could not think of any. At first I thought this might be affected:
contract C {
enum E {a, b, c}
struct S {function (E X) external f; uint E;}
}Turns out it isn't. That's because apparently, unlike a function parameter list, the parameter list of a function type is not a scope. At least the E in E X is never checked for collision with uint E. Hypothetically though, if it was a scope, I think that resolveName() should not consider uint E from the outer scope a candidate - so _onlyVisibleAsUnqualifiedNames should be true when recursively visiting the outer scopes as well.
BTW, the comment wasn't really correct in another respect so I have rewritten it a bit.
e34ce54 to
d3166c7
Compare
|
I found a new case that is not covered by this PR. It's function parameters shadowing types of other parameters: contract C {
enum EnumType1 {A, B, C}
enum EnumType2 {A, B, C}
function z(EnumType1 EnumType2) external returns (EnumType2 EnumType1) {}
}This results in To solve it I'd have to add a special case in |
| contract C { | ||
| enum EnumType {A, B, C} | ||
|
|
||
| struct StructType { | ||
| uint x; | ||
| } | ||
|
|
||
| function f() external returns (StructType memory StructType) {} | ||
| function g() external returns (EnumType EnumType) {} | ||
| function h() external returns (EnumType StructType, StructType memory EnumType) {} | ||
|
|
||
| function z(uint EnumType) external returns (EnumType e) {} | ||
| } | ||
| // ---- | ||
| // Warning 2519: (124-152): This declaration shadows an existing declaration. | ||
| // Warning 2519: (192-209): This declaration shadows an existing declaration. | ||
| // Warning 2519: (249-268): This declaration shadows an existing declaration. | ||
| // Warning 2519: (270-296): This declaration shadows an existing declaration. | ||
| // Warning 2519: (317-330): This declaration shadows an existing declaration. | ||
| // TypeError 5172: (124-134): Name has to refer to a struct, enum or contract. |
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 TypeError is what I'm referring to in #10908 (comment).
|
We discussed it today and the prevailing opinion was that for functions with a body we do want the shadowing to be disallowed. So the behavior that I caught in #10908 (comment) is correct. As such, this PR is ready for another review. I added new test cases and did some minor reformatting but the only significant change after the last approve from @hrkrshnn was the one mentioned in #10908 (comment). |
|
Here's a snippet that summarizes the current state of the PR. Errors/warnings in each case before and after the PR: enum EnumType {A, B, C}
contract C {
// struct, event, error
//
// BEFORE: Error: Name has to refer to a struct, enum or contract.
//
// AFTER: OK
struct S {uint EnumType; EnumType x;}
error Ev(uint EnumType, EnumType x);
event Er(uint EnumType, EnumType x);
// function with a body
//
// BEFORE: Warning: This declaration shadows an existing declaration.
// BEFORE: Error: Name has to refer to a struct, enum or contract.
//
// AFTER: Warning: This declaration shadows an existing declaration.
// AFTER: Error: Name has to refer to a struct, enum or contract.
function f(uint EnumType, EnumType x) external {}
// function without a body
//
// BEFORE: Warning: This declaration shadows an existing declaration.
// BEFORE: Error: Name has to refer to a struct, enum or contract.
//
// AFTER: OK
function g(uint EnumType, EnumType x) external virtual;
// function type with named parameters
//
// BEFORE: Warning: Naming function type parameters is deprecated.
// BEFORE: Warning: This declaration shadows an existing declaration.
//
// AFTER: Warning: Naming function type parameters is deprecated.
// AFTER: Warning: This declaration shadows an existing declaration.
function (uint EnumType, EnumType x) external h;
}
// contract
//
// BEFORE: Warning: This declaration shadows an existing declaration.
// BEFORE: Error: Name has to refer to a struct, enum or contract.
//
// AFTER: Warning: This declaration shadows an existing declaration.
// AFTER: Error: Name has to refer to a struct, enum or contract.
contract A {uint EnumType; EnumType x;} |
|
About the specific example of contract C {
enum EnumType1 {A, B, C}
enum EnumType2 {A, B, C}
function z(EnumType1 EnumType2) external returns (EnumType2 EnumType1) {}
}This should be an error as long as we have |
OK. That's exactly what happens: contract C {
enum EnumType1 {A, B, C}
enum EnumType2 {A, B, C}
// BEFORE: Warning: This declaration shadows an existing declaration.
// BEFORE: Error: Name has to refer to a struct, enum or contract.
//
// AFTER: Warning: This declaration shadows an existing declaration.
// AFTER: Error: Name has to refer to a struct, enum or contract.
function z1(EnumType1 EnumType2) external returns (EnumType2 EnumType1) {}
// BEFORE: Warning: This declaration shadows an existing declaration.
// BEFORE: Error: Name has to refer to a struct, enum or contract.
//
// AFTER: OK
function z2(EnumType1 EnumType2) external virtual returns (EnumType2 EnumType1);
}
I just checked and looks like it's possible to shadow the type name inside modifier invocation. You do at least get a warning if the shadowing happens though. This case is unaffected by the PR (works the same before and after): enum EnumType {A, B, C}
struct StructType {uint x;}
contract C {
modifier ms(StructType memory s) { _; }
modifier mi(uint i) { _; }
// Warning: This declaration shadows an existing declaration.
function f(uint EnumType) mi(EnumType) external {}
// OK
function g(uint x) ms(StructType(x)) external {}
// Warning: This declaration shadows an existing declaration.
// Error: Type is not callable
function h1(uint StructType) ms(StructType(StructType)) external {}
// Warning: This declaration shadows an existing declaration.
function h2(uint StructType) external {
// Error: Type is not callable
StructType(StructType);
}
}The last situation (functions |
|
I assume "Type is not callable" refers to and I don't see why the error is wrong. |
|
I said it's wrong because Still, it's mostly a cosmetic issue and not affected by the PR. Are all the other errors/warnings I listed working as they should? |
|
It could say "type of the expression is not callable" (if |
d3166c7 to
362fc66
Compare
Fixes #9231.