Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -7475,6 +7475,17 @@ def name_not_defined(self, name: str, ctx: Context, namespace: str | None = None
self.record_incomplete_ref()
return
message = f'Name "{name}" is not defined'
if (
not self.msg.prefer_simple_messages()
and "." not in name
and not (name.startswith("__") and name.endswith("__"))
and f"builtins.{name}" not in SUGGESTED_TEST_FIXTURES
):
alternatives = self._get_names_in_scope()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Since this is pretty expensive, we shouldn't do the analysis if the error will just be ignored. This way we'd only slow things down when there is a user-visible error. In some codebases, there could be thousands of ignored errors, and this could generate non-trivial amounts of extra work.

My suggestion would be to refactor is_ignored_error in mypy/errors.py so that most of the checking (but not the blocker check) is available through is_ignored_error_code(line, code, ignores), for example. Then you can check if codes.NAME_DEFINED is ignored here, and skip the expensive logic.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Actually, it would be fine to skip the logic if there is any ignore comment on the line, if that's easier to implement.

alternatives.discard(name)
matches = best_matches(name, alternatives, n=3)
if matches:
message += f"; did you mean {pretty_seq(matches, 'or')}?"
self.fail(message, ctx, code=codes.NAME_DEFINED)

if f"builtins.{name}" in SUGGESTED_TEST_FIXTURES:
Expand All @@ -7499,6 +7510,38 @@ def name_not_defined(self, name: str, ctx: Context, namespace: str | None = None
).format(module=module, name=lowercased[fullname].rsplit(".", 1)[-1])
self.note(hint, ctx, code=codes.NAME_DEFINED)

def _get_names_in_scope(self) -> set[str]:
"""Collect all names visible in the current scope for fuzzy matching suggestions.

This includes:
- Local variables (from function scopes)
- Class attributes (if it's inside a class)
- Global/module-level names
- Builtins
"""
names: set[str] = set()

for table in self.locals:
if table is not None:
names.update(table.keys())

if self.type is not None:
names.update(self.type.names.keys())
Copy link
Collaborator

Choose a reason for hiding this comment

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

Class namespace should only be included if we are not within a method (i.e. directly within class body only).


names.update(self.globals.keys())

b = self.globals.get("__builtins__", None)
if b and isinstance(b.node, MypyFile):
# Only include public builtins (not _private ones)
for builtin_name in b.node.names.keys():
if not (
len(builtin_name) > 1 and builtin_name[0] == "_" and builtin_name[1] != "_"
):
names.add(builtin_name)

# Filter out internal/dunder names that aren't useful as suggestions
return {n for n in names if not n.startswith("__")}

def already_defined(
self, name: str, ctx: Context, original_ctx: SymbolTableNode | SymbolNode | None, noun: str
) -> None:
Expand Down
19 changes: 19 additions & 0 deletions test-data/unit/check-errorcodes.test
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,25 @@ def f() -> None:
[file m.py]
[builtins fixtures/module.pyi]

[case testErrorCodeUndefinedNameSuggestion]
my_variable = 42
my_constant = 100

x = my_variabel # E: Name "my_variabel" is not defined; did you mean "my_variable"? [name-defined]

def calculate_sum(items: int) -> int:
return items

calculate_summ(1) # E: Name "calculate_summ" is not defined; did you mean "calculate_sum"? [name-defined]

class MyClass:
pass

y = MyClas() # E: Name "MyClas" is not defined; did you mean "MyClass"? [name-defined]

unknown_xyz # E: Name "unknown_xyz" is not defined [name-defined]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Ideas for additional tests:

  • Test a misspelling of a local variable.
  • Test a mispelling of a top-level definition within a function (misspelling targeting an outer namespace).
  • Test a misspelling of a class variable within class body.
  • Test a misspelling of name that is available through from import name.

[builtins fixtures/module.pyi]

[case testErrorCodeUnclassifiedError]
class A:
def __init__(self) -> int: \
Expand Down
2 changes: 1 addition & 1 deletion test-data/unit/pythoneval.test
Original file line number Diff line number Diff line change
Expand Up @@ -608,7 +608,7 @@ def f(x: _T) -> None: pass
s: FrozenSet
[out]
_program.py:2: error: Name "_T" is not defined
_program.py:3: error: Name "FrozenSet" is not defined
_program.py:3: error: Name "FrozenSet" is not defined; did you mean "frozenset"?
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you add an explicit test in some check-*.test file that ensures that misspellings of builtins are detected?


[case testVarArgsFunctionSubtyping]
import typing
Expand Down