Skip to content
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

add inferred_any #471

Merged
merged 3 commits into from
Feb 9, 2022
Merged
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
2 changes: 2 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- Add `inferred_any`, an extremely noisy error code
that triggers whenever the type checker infers something as `Any` (#471)
- Optimize type compatibility checks on large unions (#469)
- Detect incorrect key types passed to `dict.__getitem__` (#468)
- Pick up the signature of `open()` from typeshed correctly (#463)
Expand Down
3 changes: 3 additions & 0 deletions pyanalyze/error_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ class ErrorCode(enum.Enum):
impossible_pattern = 74
bad_match = 75
bad_evaluator = 76
implicit_any = 77


# Allow testing unannotated functions without too much fuss
Expand All @@ -100,6 +101,7 @@ class ErrorCode(enum.Enum):
ErrorCode.missing_parameter_annotation,
ErrorCode.suggested_return_type,
ErrorCode.suggested_parameter_type,
ErrorCode.implicit_any,
}


Expand Down Expand Up @@ -209,6 +211,7 @@ class ErrorCode(enum.Enum):
ErrorCode.impossible_pattern: "Pattern can never match",
ErrorCode.bad_match: "Invalid type in match statement",
ErrorCode.bad_evaluator: "Invalid code in type evaluator",
ErrorCode.implicit_any: "Value is inferred as Any",
}


Expand Down
15 changes: 14 additions & 1 deletion pyanalyze/name_check_visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@
from .yield_checker import YieldChecker
from .type_object import TypeObject, get_mro
from .value import (
VOID,
AlwaysPresentExtension,
AnnotatedValue,
AnySource,
Expand Down Expand Up @@ -952,6 +953,7 @@ class NameCheckVisitor(node_visitor.ReplacingNodeVisitor):
_name_node_to_statement: Optional[Dict[ast.AST, Optional[ast.AST]]]
import_name_to_node: Dict[str, Union[ast.Import, ast.ImportFrom]]
expected_return_value: Optional[Value]
error_for_implicit_any: bool

def __init__(
self,
Expand Down Expand Up @@ -1042,6 +1044,9 @@ def __init__(
self.imports_added = set()
self.future_imports = set() # active future imports in this file
self.return_values = []
self.error_for_implicit_any = self.options.is_error_code_enabled(
ErrorCode.implicit_any
)

self._name_node_to_statement = None
# Cache the return values of functions within this file, so that we can use them to
Expand Down Expand Up @@ -1215,9 +1220,17 @@ def visit(self, node: ast.AST) -> Value:
finally:
self.node_context.contexts.pop()
if ret is None:
ret = AnyValue(AnySource.inference)
ret = VOID
if self.annotate:
node.inferred_value = ret
if self.error_for_implicit_any:
for val in ret.walk_values():
if isinstance(val, AnyValue) and val.source is not AnySource.explicit:
self._show_error_if_checking(
node,
f"Inferred value contains Any: {ret}",
ErrorCode.implicit_any,
)
return ret

def generic_visit(self, node: ast.AST) -> None:
Expand Down
20 changes: 20 additions & 0 deletions pyanalyze/value.py
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,26 @@ def can_assign(self, other: Value, ctx: CanAssignContext) -> CanAssign:
"""


@dataclass(frozen=True)
class VoidValue(Value):
"""Dummy Value used as the inferred type of AST nodes that
do not represent expressions.

This is useful so that we can infer a Value for every AST node,
but notice if we unexpectedly use it like an actual value.

"""

def __str__(self) -> str:
return "(void)"

def can_assign(self, other: Value, ctx: CanAssignContext) -> CanAssign:
return CanAssignError("Cannot assign to void")


VOID = VoidValue()


@dataclass(frozen=True)
class UninitializedValue(Value):
"""Value for variables that have not been initialized.
Expand Down
24 changes: 15 additions & 9 deletions pyanalyze/yield_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,18 @@
List,
Optional,
Sequence,
TYPE_CHECKING,
Tuple,
)

from pyanalyze.functions import FunctionNode

from .asynq_checker import AsyncFunctionKind
from .error_code import ErrorCode
from .functions import FunctionNode
from .value import Value, KnownValue, UnboundMethodValue, UNINITIALIZED_VALUE
from .analysis_lib import get_indentation, get_line_range_for_node
from .node_visitor import Replacement
from .stacked_scopes import VisitorState

if TYPE_CHECKING:
from .name_check_visitor import NameCheckVisitor
import pyanalyze


@dataclass
Expand Down Expand Up @@ -175,7 +173,7 @@ def _ensure_unique(self, varname: str) -> str:

@dataclass
class YieldChecker:
visitor: "NameCheckVisitor"
visitor: "pyanalyze.name_check_visitor.NameCheckVisitor"
variables_from_yield_result: Dict[str, bool] = field(default_factory=dict)
in_yield_result_assignment: bool = False
in_non_async_yield: bool = False
Expand Down Expand Up @@ -301,7 +299,7 @@ def _maybe_show_unnecessary_yield_error(
self.show_unnecessary_yield_error(unused, node, current_statement)

def _check_for_duplicate_yields(
self, node: ast.Yield, current_statement: ast.stmt
self, node: ast.Yield, current_statement: ast.AST
) -> None:
if not isinstance(node.value, ast.Tuple) or len(node.value.elts) < 2:
return
Expand Down Expand Up @@ -485,6 +483,8 @@ def _create_replacement_for_yield_nodes(
new_assign_lines, replace_yield = self._move_out_var_from_yield(
second_yield, indentation
)
if replace_yield is None:
return None

between_lines = lines[
first_yield.line_range[-1] : second_yield.line_range[0] - 1
Expand All @@ -505,6 +505,8 @@ def _create_replacement_for_yield_nodes(
lines_to_add, replace_first = self._move_out_var_from_yield(
first_yield, indentation
)
if replace_first is None:
return None

if second_yield.is_assign_or_expr():
# just move it
Expand All @@ -523,6 +525,8 @@ def _create_replacement_for_yield_nodes(
second_assign_lines, replace_second = self._move_out_var_from_yield(
second_yield, indentation
)
if replace_second is None:
return None
lines_to_add += second_assign_lines
lines_for_second_yield = replace_second.lines_to_add

Expand All @@ -540,7 +544,7 @@ def _create_replacement_for_yield_nodes(

def _move_out_var_from_yield(
self, yield_info: YieldInfo, indentation: int
) -> Tuple[List[str], Replacement]:
) -> Tuple[List[str], Optional[Replacement]]:
"""Helper for splitting up a yield node and moving it to an earlier place.

For example, it will help turn:
Expand Down Expand Up @@ -608,7 +612,9 @@ def generate_varname_from_node(self, node: ast.AST) -> str:
def is_available(name: str) -> bool:
if name in self.used_varnames:
return False
value = self.visitor.scopes.get(name, node=None, state=None)
value = self.visitor.scopes.get(
name, node=None, state=VisitorState.check_names
)
return value is UNINITIALIZED_VALUE

varname = VarnameGenerator(is_available).get(node)
Expand Down