Skip to content

Conversation

@a3d4
Copy link
Contributor

@a3d4 a3d4 commented Feb 8, 2021

Fixes #9231.

@a3d4 a3d4 requested review from cameel and chriseth February 8, 2021 06:20
}
// ----
// TypeError 5172: (25-26): Name has to refer to a struct, enum or contract.
// DeclarationError 7920: (25-26): Identifier not found or not unique.
Copy link
Contributor

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?

Copy link
Contributor Author

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;}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this test pass?

Copy link
Contributor Author

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.

@a3d4 a3d4 force-pushed the fix-9231-struct-member-names-shadow-type-names branch from a49107d to d30aa71 Compare February 16, 2021 23:16
@a3d4 a3d4 requested a review from hrkrshnn February 18, 2021 12:50
@chriseth
Copy link
Contributor

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:

abstract contract C {
  function f(uint C) public pure;
}

as opposed to

contract C {
  function f(uint C) public pure {};
}

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?

@a3d4
Copy link
Contributor Author

a3d4 commented Feb 18, 2021

@chriseth

The second should create an error, the first does not have to.

Currently both samples produce the same warning:

This declaration shadows an existing declaration.

Warning: This declaration shadows an existing declaration.
 --> d:/T/9231.sol:2:14:
  |
2 |   function f(uint C) public pure;
  |              ^^^^^^
Note: The shadowed declaration is here:
 --> d:/T/9231.sol:1:1:
  |
1 | abstract contract C {
  | ^ (Relevant source part starts here and spans across multiple lines).
Warning: This declaration shadows an existing declaration.
 --> d:/T/9231.sol:2:14:
  |
2 |   function f(uint C) public pure {}
  |              ^^^^^^
Note: The shadowed declaration is here:
 --> d:/T/9231.sol:1:1:
  |
1 | contract C {
  | ^ (Relevant source part starts here and spans across multiple lines).

The same is true for event parameter names.

Do you mean something like this:

contract C {
  event e(uint C);
}

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.

@a3d4 a3d4 force-pushed the fix-9231-struct-member-names-shadow-type-names branch from 873d46f to 4312277 Compare February 18, 2021 23:15
@a3d4
Copy link
Contributor Author

a3d4 commented Feb 19, 2021

@chriseth
We can just handle parameters of abstract functions the same way as event parameters, see 4312277.

Or we can extend Declaration to indicate that certain names should not participate in the name lookup, see 31cc995.

@chriseth
Copy link
Contributor

The solution looks good!

@leonardoalt
Copy link

The implemented or discussed solution?

@a3d4 a3d4 force-pushed the fix-9231-struct-member-names-shadow-type-names branch from 53b0276 to 2905ea5 Compare March 5, 2021 01:09
@a3d4
Copy link
Contributor Author

a3d4 commented Mar 5, 2021

@chriseth

All names are self-sufficient, but for some, there is no code that can refer to them, isn't it?

I would say that name is self-sufficient if it does not require any qualifier to identify to the corresponding entity :).
But I agree, isVisibleAsUnqualifiedName sounds more precise.

@leonardoalt
Copy link

Is this ready for review again?

@a3d4
Copy link
Contributor Author

a3d4 commented Mar 31, 2021

@leonardoalt

Is this ready for review again?

Commits are not squashed and there is no changelog entry, but otherwise I believe it can be reviewed.

Copy link
Collaborator

@cameel cameel left a 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.

@cameel cameel force-pushed the fix-9231-struct-member-names-shadow-type-names branch from 2905ea5 to e531fc8 Compare April 27, 2021 18:26
@cameel cameel requested review from bshastry and hrkrshnn April 27, 2021 18:37
@cameel cameel force-pushed the fix-9231-struct-member-names-shadow-type-names branch from e531fc8 to 1ff57a4 Compare April 27, 2021 18:49
@cameel
Copy link
Collaborator

cameel commented Apr 27, 2021

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.
Copy link
Contributor

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?

Copy link
Collaborator

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cameel

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.)

Copy link
Collaborator

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 {}
}
// ----
Copy link
Contributor

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?

Copy link
Collaborator

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.


abstract contract A {
function f(uint A) public pure {} // warning
function g(uint A) public virtual; // OK
Copy link
Contributor

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?

Copy link
Collaborator

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.

@cameel cameel force-pushed the fix-9231-struct-member-names-shadow-type-names branch from 1ff57a4 to 9082d2a Compare April 28, 2021 14:29
@cameel cameel requested a review from bshastry April 28, 2021 14:32
@cameel cameel self-assigned this Apr 28, 2021
@cameel cameel force-pushed the fix-9231-struct-member-names-shadow-type-names branch from 9082d2a to 059a1c9 Compare April 28, 2021 14:35
@cameel cameel force-pushed the fix-9231-struct-member-names-shadow-type-names branch from 059a1c9 to 6d8b819 Compare April 29, 2021 21:26
@cameel cameel requested a review from hrkrshnn April 29, 2021 21:27
@cameel cameel force-pushed the fix-9231-struct-member-names-shadow-type-names branch from 6d8b819 to d7cee56 Compare April 29, 2021 22:10
@cameel
Copy link
Collaborator

cameel commented Apr 30, 2021

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
hrkrshnn previously approved these changes May 3, 2021
Copy link
Contributor

@hrkrshnn hrkrshnn left a 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.
Copy link
Contributor

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?

Copy link
Collaborator

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.

Copy link
Contributor

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?

Copy link
Collaborator

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.

@cameel cameel force-pushed the fix-9231-struct-member-names-shadow-type-names branch 2 times, most recently from e34ce54 to d3166c7 Compare May 11, 2021 15:43
@cameel
Copy link
Collaborator

cameel commented May 11, 2021

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 Error: Name has to refer to a struct, enum or contract.
This is the same problem this PR solves for structs but this time for function parameters. And unfortunately it does not nicely fit the Declaration::isVisibleAsUnqualifiedName() approach. That's because the function tells us if a declaration can never be referenced with name alone and this is not the case for functions that have a body.

To solve it I'd have to add a special case in resolveName() and that's what isVisibleAsUnqualifiedName() was supposed to let us avoid. So I'm not sure how to proceed here.

Comment on lines +1 to +20
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.
Copy link
Collaborator

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).

@cameel
Copy link
Collaborator

cameel commented May 12, 2021

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).

@cameel
Copy link
Collaborator

cameel commented Jun 3, 2021

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;}

@chriseth
Copy link
Contributor

chriseth commented Jun 3, 2021

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 {} instead of ;, because functions with empty body can have modifiers, while function without implementation cannot have modifiers. This is important because we said that shadowing is not a problem as long as there is no code the shadowing can cover. Modifier invocations are code that are inside the scope of the function, i.e. the parameters are visible in the modifier invocation.

@cameel
Copy link
Collaborator

cameel commented Jun 3, 2021

This should be an error as long as we have {} instead of ;,

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);
}

Modifier invocations are code that are inside the scope of the function, i.e. the parameters are visible in the modifier invocation.

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 h1() and h2()) is in line with what I would expect, i.e. you cannot use the struct type because it is shadowed, but the error message is wrong. The type is callable, it's just not the type that is being used.

@chriseth
Copy link
Contributor

chriseth commented Jun 4, 2021

I assume "Type is not callable" refers to

ms(StructType(StructType))
   ^^^^^^^^^^

and I don't see why the error is wrong. StructType resolves to the name of the variable, and since the variable is not of function type, you cannot call it.

@cameel
Copy link
Collaborator

cameel commented Jun 4, 2021

I said it's wrong because StructType is not a type if it resolves to the name of a variable. It should just say that it's not callable. Or was it meant to say that the type of StructType is not callable?

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?

@chriseth
Copy link
Contributor

chriseth commented Jun 4, 2021

It could say "type of the expression is not callable" (if StructType were a variable of function type, it would work). I think everything is fine now.

@chriseth chriseth force-pushed the fix-9231-struct-member-names-shadow-type-names branch from d3166c7 to 362fc66 Compare June 9, 2021 10:37
@chriseth chriseth enabled auto-merge June 9, 2021 10:38
@chriseth chriseth merged commit 0fff4e6 into argotorg:develop Jun 9, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Struct member names can shadow type names within the struct

6 participants