-
-
Notifications
You must be signed in to change notification settings - Fork 3k
Improve "Name already defined" error message #5067
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
Improve "Name already defined" error message #5067
Conversation
I think anywhere this message appears it's useful to give the original definition, if possible. Tests would be good to add. You'll probably just have to change the existing tests though. |
Thanks for taking a look at this! I updated the tests to what I think should be the correct output, but in many cases the line numbers it outputs aren't what I expected (so the tests are currently failing). I'll have to figure out why these are wrong. |
So my code is failing on two tests because it's reporting the wrong line numbers. For imports, Is anyone familiar with this issue? It seems like I either need to detect that the original definition was an import, and don't report a line number, or maybe somehow figure out on which line the import happened. |
It looks like when So it seems that the options are either:
Which would be the right thing to do? cc @JelleZijlstra |
I would just not show the additional note in this case. Anyway, conditional imports will require special support which will probably add later. I will now make a review, sorry for waiting so long with this. |
No worries! I can make that change, the tests should pass after I fix that. |
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.
Thank you for working on this! The more detailed errors look definitely better.
@@ -406,7 +407,8 @@ def _visit_func_def(self, defn: FuncDef) -> None: | |||
# Redefinition. Conditional redefinition is okay. | |||
n = self.locals[-1][defn.name()].node | |||
if not self.set_original_def(n, defn): | |||
self.name_already_defined(defn.name(), defn) | |||
self.name_already_defined(defn.name(), defn, | |||
self.locals[-1][defn.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.
It looks like you missed one more case: global function definition, see else: # Top-level function
below. You will likely need to patch also self.check_no_global
in the same way you do above for self.name_already defined
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 self.check_no_global
already does the right thing?
Line 3225 in 7eebb8f
self.name_already_defined(n, ctx, self.globals[n]) |
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.
OK, if the previous context is always correct in check_no_global
, then no action is required here.
mypy/semanal.py
Outdated
@@ -1848,7 +1850,12 @@ def analyze_lvalue(self, lval: Lvalue, nested: bool = False, | |||
self.type.names[lval.name] = SymbolTableNode(MDEF, v) | |||
elif explicit_type: | |||
# Don't re-bind types | |||
self.name_already_defined(lval.name, lval) | |||
global_def = self.globals.get(lval.name) | |||
local_def = cast(SymbolTable, self.locals[-1]).get(lval.name) if self.locals and self.locals[-1] else None |
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 you rewrite it using an if statement, then the cast will be unnecessary.
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.
Fixed!
mypy/semanal.py
Outdated
@@ -3230,10 +3237,16 @@ def name_not_defined(self, name: str, ctx: Context) -> None: | |||
self.add_fixture_note(fullname, ctx) | |||
|
|||
def name_already_defined(self, name: str, ctx: Context, | |||
original_ctx: Optional[SymbolTableNode] = None) -> None: | |||
original_ctx: Optional[Union[SymbolTableNode, SymbolNode]] = None) -> None: |
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 general, it is not a good idea to use a union here, but it seems to me the old signature was "wrong", so it is fine in this case. Maybe you can add a short comment about this?
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 old signature was actually correct, the function used to only be called with SymbolTableNode
s. I added the option for passing a SymbolNode
instead because in _visit_overloaded_func_def
I only have a SymbolNode
to pass it, not a SymbolTableNode
.
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 I guess to fix this, the options are
- take
Optional[SymbolNode]
instead - take
Optiona[SymbolTableNode]
only, and in_visit_overloaded_func_def
create aSymbolTableNode
to pass to the function
Which do you think makes more sense?
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 mean that SymbolTableNode
is logically "wrong" because it is just an entry in the symbol table wrapper, while SymbolNode
is an actual error context (well to be more precise, SymbolNode
s are semantic objects, not syntactic, but at least they are closer, and should ideally point to the line of syntax node defining them). So I vote for SymbolNode
.
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.
Actually you might need .kind == MODULE_REF
which is only available on SymbolTableNode
. Potentially, this can be replaced by isinstance(node, (MypyFile, ImportedName))
, but I think it is better to just leave it as is.
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.
Okay, I'll leave it as-is then.
mypy/semanal.py
Outdated
else: | ||
node = None | ||
if node and node.line != -1: | ||
extra_msg = ' on line {}'.format(node.line) | ||
else: | ||
extra_msg = ' (possibly by an import)' |
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 have enough tests for this case? If not you can add one more.
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 when this would happen (when node
is None
or node.line
is -1
). There don't seem to be any tests covering it, so maybe this never happens?
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.
Actually, looks like there was one test where this happened. Do you know in what situations this happens? If so, I could add tests for those cases.
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 this might happen in case of an error or an unresolved circular import. If you can't find a simple additional test case then ignore this.
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.
Hmm, looks like there's already a test case for that situation (testDuplicateDefFromImport
in semanal-errors.test
). So I guess we can leave this as-is.
@@ -1347,7 +1347,7 @@ def dec(x: Any) -> Any: | |||
@dec | |||
def f() -> None: | |||
pass | |||
@dec # E: Name 'f' already defined | |||
@dec # E: Name 'f' already defined on line 4 |
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.
Wow, good job updating all these error messages! 👍
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 is almost ready, I left few more suggestions.
self.name_already_defined(lval.name, lval) | ||
global_def = self.globals.get(lval.name) | ||
if self.locals: | ||
locals_last = self.locals[-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.
@gvanrossum probably will be happy to see another case in mypy for PEP 572 :-)
@sid-kap You can ignore this comment.
OK, it looks like the comments I added in this round are optional, so this can be merged as soon as you fix the lint error reported by Travis. |
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.
LGTM now!
In
semanal.py
,name_already_defined
takes an optionaloriginal_ctx: Optional[SymbolTableNode]
argument which is used to print the line number of the original context where a name was defined already.In many cases, the code doesn't pass anything for this argument, even though there is an original context that makes sense. This PR does this for the
name_already_defined
call inadd_symbol
, as discussed in #4722.A few questions if you guys think this is a good idea:
add_symbol
insemanal_pass1.py
andsemanal_pass3.py
?semanal.py
where adding this additional argument could be helpful:mypy/mypy/semanal.py
Line 3177 in fae1f25
mypy/mypy/semanal.py
Line 3201 in fae1f25
The function is also called with just two parameters in
_visit_func_def
,_visit_overloaded_func_def
, andanalyze_lvalue
, but I'm not sure if there is aoriginal_ctx
that makes sense to pass there.