Skip to content

feat: warn about annotations in unannotated functions #10748

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

Closed
Closed
3 changes: 3 additions & 0 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -2067,6 +2067,9 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
and self.scope.active_class() is not None):
self.fail(message_registry.DEPENDENT_FINAL_IN_CLASS_BODY, s)

if s.was_annotated and not self.in_checked_function():
Copy link
Member

Choose a reason for hiding this comment

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

Is it possible to just use s.unanalyzed_type is not None instead of adding the new attribute?

self.msg.annotation_in_unchecked_function(context=s)

def check_type_alias_rvalue(self, s: AssignmentStmt) -> None:
if not (self.is_stub and isinstance(s.rvalue, OpExpr) and s.rvalue.op == '|'):
# We do this mostly for compatibility with old semantic analyzer.
Expand Down
6 changes: 4 additions & 2 deletions mypy/fastparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -776,7 +776,8 @@ def visit_Assign(self, n: ast3.Assign) -> AssignmentStmt:
lvalues = self.translate_expr_list(n.targets)
rvalue = self.visit(n.value)
typ = self.translate_type_comment(n, n.type_comment)
s = AssignmentStmt(lvalues, rvalue, type=typ, new_syntax=False)
s = AssignmentStmt(lvalues, rvalue, type=typ,
was_annotated=(typ is not None), new_syntax=False)
return self.set_line(s, n)

# AnnAssign(expr target, expr annotation, expr? value, int simple)
Expand All @@ -791,7 +792,8 @@ def visit_AnnAssign(self, n: ast3.AnnAssign) -> AssignmentStmt:
typ = TypeConverter(self.errors, line=line).visit(n.annotation)
assert typ is not None
typ.column = n.annotation.col_offset
s = AssignmentStmt([self.visit(n.target)], rvalue, type=typ, new_syntax=True)
s = AssignmentStmt([self.visit(n.target)], rvalue, type=typ,
was_annotated=True, new_syntax=True)
return self.set_line(s, n)

# AugAssign(expr target, operator op, expr value)
Expand Down
3 changes: 2 additions & 1 deletion mypy/fastparse2.py
Original file line number Diff line number Diff line change
Expand Up @@ -620,7 +620,8 @@ def visit_Assign(self, n: ast27.Assign) -> AssignmentStmt:
typ = self.translate_type_comment(n, n.type_comment)
stmt = AssignmentStmt(self.translate_expr_list(n.targets),
self.visit(n.value),
type=typ)
type=typ,
was_annotated=(typ is not None))
return self.set_line(stmt, n)

# AugAssign(expr target, operator op, expr value)
Expand Down
4 changes: 4 additions & 0 deletions mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -1653,6 +1653,10 @@ def add_fixture_note(self, fullname: str, ctx: Context) -> None:
'Consider adding [builtins fixtures/{}] to your test description'.format(
SUGGESTED_TEST_FIXTURES[fullname]), ctx)

def annotation_in_unchecked_function(self, context: Context) -> None:
self.note('By default the bodies of functions without signature annotations ' +
'are not type checked, consider using "--check-untyped-defs"', context)


def quote_type_string(type_string: str) -> str:
"""Quotes a type representation for use in messages."""
Expand Down
6 changes: 5 additions & 1 deletion mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1058,6 +1058,8 @@ class AssignmentStmt(Statement):
type: Optional["mypy.types.Type"] = None
# Original, not semantically analyzed type in annotation (used for reprocessing)
unanalyzed_type: Optional["mypy.types.Type"] = None
# Was a type annotation provided (either a type comment or PEP 526 style).
was_annotated: bool = False
# This indicates usage of PEP 526 type annotation syntax in assignment.
new_syntax: bool = False
# Does this assignment define a type alias?
Expand All @@ -1071,12 +1073,14 @@ class AssignmentStmt(Statement):
is_final_def = False

def __init__(self, lvalues: List[Lvalue], rvalue: Expression,
type: 'Optional[mypy.types.Type]' = None, new_syntax: bool = False) -> None:
type: 'Optional[mypy.types.Type]' = None, was_annotated: bool = False,
new_syntax: bool = False) -> None:
super().__init__()
self.lvalues = lvalues
self.rvalue = rvalue
self.type = type
self.unanalyzed_type = type
self.was_annotated = was_annotated or new_syntax # new_syntax implies was_annotated
self.new_syntax = new_syntax

def accept(self, visitor: StatementVisitor[T]) -> T:
Expand Down
4 changes: 2 additions & 2 deletions test-data/unit/check-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -1088,7 +1088,7 @@ reveal_type(Foo().Meta.name) # N: Revealed type is "builtins.str"

class A:
def __init__(self):
self.x = None # type: int
self.x = None # type: int # N: By default the bodies of functions without signature annotations are not type checked, consider using "--check-untyped-defs"
Copy link
Member

Choose a reason for hiding this comment

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

This is a special case, mypy does use type annotations in unannotated __init__(). So I would propose to not show this note in __init__().

Copy link
Member

Choose a reason for hiding this comment

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

Actually there still may be type errors like self.x: int = "no", so withdraw this comment.

a = None # type: A
a.x = 1
a.x = '' # E: Incompatible types in assignment (expression has type "str", variable has type "int")
Expand All @@ -1100,7 +1100,7 @@ a.x = 1
a.x = '' # E: Incompatible types in assignment (expression has type "str", variable has type "int")
class A:
def __init__(self):
self.x = None # type: int
self.x = None # type: int # N: By default the bodies of functions without signature annotations are not type checked, consider using "--check-untyped-defs"


-- Special cases
Expand Down
2 changes: 2 additions & 0 deletions test-data/unit/check-incremental.test
Original file line number Diff line number Diff line change
Expand Up @@ -3551,7 +3551,9 @@ class Baz:
def __init__(self):
self.x = 'lol' # type: str
[out]
tmp/c.py:3: note: By default the bodies of functions without signature annotations are not type checked, consider using "--check-untyped-defs"
[out2]
tmp/c.py:3: note: By default the bodies of functions without signature annotations are not type checked, consider using "--check-untyped-defs"
tmp/a.py:3: error: Unsupported operand types for + ("int" and "str")

[case testIncrementalWithIgnoresTwice]
Expand Down
2 changes: 1 addition & 1 deletion test-data/unit/check-inference-context.test
Original file line number Diff line number Diff line change
Expand Up @@ -814,7 +814,7 @@ if int():
from typing import List
class A:
def __init__(self):
self.x = [] # type: List[int]
self.x = [] # type: List[int] # N: By default the bodies of functions without signature annotations are not type checked, consider using "--check-untyped-defs"
a = A()
a.x = []
a.x = [1]
Expand Down
4 changes: 2 additions & 2 deletions test-data/unit/check-newsemanal.test
Original file line number Diff line number Diff line change
Expand Up @@ -2976,11 +2976,11 @@ def g() -> None:
import typing

def f():
bar = [] # type: typing.List[int]
bar = [] # type: typing.List[int] # N: By default the bodies of functions without signature annotations are not type checked, consider using "--check-untyped-defs"

def foo():
nonlocal bar
bar = [] # type: typing.List[int]
bar = [] # type: typing.List[int] # N: By default the bodies of functions without signature annotations are not type checked, consider using "--check-untyped-defs"

def g() -> None:
bar = [] # type: typing.List[int]
Expand Down
42 changes: 41 additions & 1 deletion test-data/unit/check-statements.test
Original file line number Diff line number Diff line change
Expand Up @@ -1792,6 +1792,46 @@ def foo() -> None:

def foo2():
global bar2
bar2 = [] # type: List[str]
bar2 = [] # type: List[str] # N: By default the bodies of functions without signature annotations are not type checked, consider using "--check-untyped-defs"
bar2
[builtins fixtures/list.pyi]


-- Annotated assignment statement
-- -------------------


[case testUnannotatedFunctionWithAnnotatedStatements]
def foo():
a = 1 # type: int # N: By default the bodies of functions without signature annotations are not type checked, consider using "--check-untyped-defs"
b = '' # type: int # N: By default the bodies of functions without signature annotations are not type checked, consider using "--check-untyped-defs"

[case testUnannotatedFunctionWithAnnotatedStatementsAndFlag]
# flags: --check-untyped-defs
def foo() -> None:
Copy link
Member

Choose a reason for hiding this comment

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

Same question here, you probably need to remove -> None here.

a = 1 # type: int
b = '' # type: int # E: Incompatible types in assignment (expression has type "str", variable has type "int")

[case testAnnotatedFunctionWithAnnotatedStatements]
def foo() -> None:
a = 1 # type: int
b = '' # type: int # E: Incompatible types in assignment (expression has type "str", variable has type "int")

[case testUnannotatedFunctionWithNewSyntaxAnnotatedStatements]
def foo():
a: int = 1 # N: By default the bodies of functions without signature annotations are not type checked, consider using "--check-untyped-defs"
b: int = '' # N: By default the bodies of functions without signature annotations are not type checked, consider using "--check-untyped-defs"
c: bool # N: By default the bodies of functions without signature annotations are not type checked, consider using "--check-untyped-defs"

[case testUnannotatedFunctionWithNewSyntaxAnnotatedStatementsAndFlag]
# flags: --check-untyped-defs
def foo() -> None:
Copy link
Member

Choose a reason for hiding this comment

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

What is the purpose of this test? I think you maybe wanted to check that the note is not shown if --check-untyped-defs is used, but then you need to remove -> None here.

a: int = 1
b: int = '' # E: Incompatible types in assignment (expression has type "str", variable has type "int")
c: bool

[case testAnnotatedFunctionWithNewSyntaxAnnotatedStatements]
def foo() -> None:
a: int = 1
b: int = '' # E: Incompatible types in assignment (expression has type "str", variable has type "int")
c: bool