| """Block/import reachability analysis.""" |
| |
| from __future__ import annotations |
| |
| from mypy.nodes import ( |
| AssertStmt, |
| AssignmentStmt, |
| Block, |
| ClassDef, |
| ExpressionStmt, |
| ForStmt, |
| FuncDef, |
| IfStmt, |
| Import, |
| ImportAll, |
| ImportFrom, |
| MatchStmt, |
| MypyFile, |
| ReturnStmt, |
| ) |
| from mypy.options import Options |
| from mypy.reachability import ( |
| assert_will_always_fail, |
| infer_reachability_of_if_statement, |
| infer_reachability_of_match_statement, |
| ) |
| from mypy.traverser import TraverserVisitor |
| |
| |
| class SemanticAnalyzerPreAnalysis(TraverserVisitor): |
| """Analyze reachability of blocks and imports and other local things. |
| |
| This runs before semantic analysis, so names have not been bound. Imports are |
| also not resolved yet, so we can only access the current module. |
| |
| This determines static reachability of blocks and imports due to version and |
| platform checks, among others. |
| |
| The main entry point is 'visit_file'. |
| |
| Reachability of imports needs to be determined very early in the build since |
| this affects which modules will ultimately be processed. |
| |
| Consider this example: |
| |
| import sys |
| |
| def do_stuff() -> None: |
| if sys.version_info >= (3, 10): |
| import xyz # Only available in Python 3.10+ |
| xyz.whatever() |
| ... |
| |
| The block containing 'import xyz' is unreachable in Python 3 mode. The import |
| shouldn't be processed in Python 3 mode, even if the module happens to exist. |
| """ |
| |
| def visit_file(self, file: MypyFile, fnam: str, mod_id: str, options: Options) -> None: |
| self.platform = options.platform |
| self.cur_mod_id = mod_id |
| self.cur_mod_node = file |
| self.options = options |
| self.is_global_scope = True |
| self.skipped_lines: set[int] = set() |
| |
| for i, defn in enumerate(file.defs): |
| defn.accept(self) |
| if isinstance(defn, AssertStmt) and assert_will_always_fail(defn, options): |
| # We've encountered an assert that's always false, |
| # e.g. assert sys.platform == 'lol'. Truncate the |
| # list of statements. This mutates file.defs too. |
| if i < len(file.defs) - 1: |
| next_def, last = file.defs[i + 1], file.defs[-1] |
| if last.end_line is not None: |
| # We are on a Python version recent enough to support end lines. |
| self.skipped_lines |= set(range(next_def.line, last.end_line + 1)) |
| del file.defs[i + 1 :] |
| break |
| file.skipped_lines = self.skipped_lines |
| |
| def visit_func_def(self, node: FuncDef) -> None: |
| old_global_scope = self.is_global_scope |
| self.is_global_scope = False |
| super().visit_func_def(node) |
| self.is_global_scope = old_global_scope |
| file_node = self.cur_mod_node |
| if ( |
| self.is_global_scope |
| and file_node.is_stub |
| and node.name == "__getattr__" |
| and file_node.is_package_init_file() |
| ): |
| # __init__.pyi with __getattr__ means that any submodules are assumed |
| # to exist, even if there is no stub. Note that we can't verify that the |
| # return type is compatible, since we haven't bound types yet. |
| file_node.is_partial_stub_package = True |
| |
| def visit_class_def(self, node: ClassDef) -> None: |
| old_global_scope = self.is_global_scope |
| self.is_global_scope = False |
| super().visit_class_def(node) |
| self.is_global_scope = old_global_scope |
| |
| def visit_import_from(self, node: ImportFrom) -> None: |
| node.is_top_level = self.is_global_scope |
| super().visit_import_from(node) |
| |
| def visit_import_all(self, node: ImportAll) -> None: |
| node.is_top_level = self.is_global_scope |
| super().visit_import_all(node) |
| |
| def visit_import(self, node: Import) -> None: |
| node.is_top_level = self.is_global_scope |
| super().visit_import(node) |
| |
| def visit_if_stmt(self, s: IfStmt) -> None: |
| infer_reachability_of_if_statement(s, self.options) |
| for expr in s.expr: |
| expr.accept(self) |
| for node in s.body: |
| node.accept(self) |
| if s.else_body: |
| s.else_body.accept(self) |
| |
| def visit_block(self, b: Block) -> None: |
| if b.is_unreachable: |
| if b.end_line is not None: |
| # We are on a Python version recent enough to support end lines. |
| self.skipped_lines |= set(range(b.line, b.end_line + 1)) |
| return |
| super().visit_block(b) |
| |
| def visit_match_stmt(self, s: MatchStmt) -> None: |
| infer_reachability_of_match_statement(s, self.options) |
| for guard in s.guards: |
| if guard is not None: |
| guard.accept(self) |
| for body in s.bodies: |
| body.accept(self) |
| |
| # The remaining methods are an optimization: don't visit nested expressions |
| # of common statements, since they can have no effect. |
| |
| def visit_assignment_stmt(self, s: AssignmentStmt) -> None: |
| pass |
| |
| def visit_expression_stmt(self, s: ExpressionStmt) -> None: |
| pass |
| |
| def visit_return_stmt(self, s: ReturnStmt) -> None: |
| pass |
| |
| def visit_for_stmt(self, s: ForStmt) -> None: |
| s.body.accept(self) |
| if s.else_body is not None: |
| s.else_body.accept(self) |