@@ -895,6 +895,9 @@ def __init__(
895895 # raw parsed trees not analyzed with mypy. We use these to find absolute
896896 # location of a symbol used as a location for an error message.
897897 self .extra_trees : dict [str , MypyFile ] = {}
898+ # Snapshot of import-related options per module. We record these even for
899+ # suppressed imports, since they can affect errors in the callers.
900+ self .import_options : dict [str , dict [str , object ]] = {}
898901 # Cache for transitive dependency check (expensive).
899902 self .transitive_deps_cache : dict [tuple [int , int ], bool ] = {}
900903
@@ -1871,6 +1874,7 @@ def write_cache(
18711874 tree : MypyFile ,
18721875 dependencies : list [str ],
18731876 suppressed : list [str ],
1877+ suppressed_deps_opts : bytes ,
18741878 imports_ignored : dict [int , list [str ]],
18751879 dep_prios : list [int ],
18761880 dep_lines : list [int ],
@@ -1989,6 +1993,7 @@ def write_cache(
19891993 suppressed = suppressed ,
19901994 imports_ignored = imports_ignored ,
19911995 options = options_snapshot (id , manager ),
1996+ suppressed_deps_opts = suppressed_deps_opts ,
19921997 dep_prios = dep_prios ,
19931998 dep_lines = dep_lines ,
19941999 interface_hash = interface_hash ,
@@ -2274,6 +2279,7 @@ def new_state(
22742279 import_context = []
22752280 id = id or "__main__"
22762281 options = manager .options .clone_for_module (id )
2282+ manager .import_options [id ] = options .dep_import_options ()
22772283
22782284 ignore_all = False
22792285 if not path and source is None :
@@ -2571,7 +2577,16 @@ def is_fresh(self) -> bool:
25712577 # self.meta.dependencies when a dependency is dropped due to
25722578 # suppression by silent mode. However, when a suppressed
25732579 # dependency is added back we find out later in the process.
2574- return self .meta is not None and self .dependencies == self .meta .dependencies
2580+ # Additionally, we need to verify that import following options are
2581+ # same for suppressed dependencies, even if the first check is OK.
2582+ return (
2583+ self .meta is not None
2584+ and self .dependencies == self .meta .dependencies
2585+ and (
2586+ self .options .fine_grained_incremental
2587+ or self .meta .suppressed_deps_opts == self .suppressed_deps_opts ()
2588+ )
2589+ )
25752590
25762591 def mark_as_rechecked (self ) -> None :
25772592 """Marks this module as having been fully re-analyzed by the type-checker."""
@@ -3020,6 +3035,15 @@ def update_fine_grained_deps(self, deps: dict[str, set[str]]) -> None:
30203035 merge_dependencies (self .compute_fine_grained_deps (), deps )
30213036 type_state .update_protocol_deps (deps )
30223037
3038+ def suppressed_deps_opts (self ) -> bytes :
3039+ return json_dumps (
3040+ {
3041+ dep : self .manager .import_options [dep ]
3042+ for dep in self .suppressed
3043+ if self .priorities .get (dep ) != PRI_INDIRECT
3044+ }
3045+ )
3046+
30233047 def write_cache (self ) -> tuple [CacheMeta , str ] | None :
30243048 assert self .tree is not None , "Internal error: method must be called on parsed file only"
30253049 # We don't support writing cache files in fine-grained incremental mode.
@@ -3051,6 +3075,7 @@ def write_cache(self) -> tuple[CacheMeta, str] | None:
30513075 self .tree ,
30523076 list (self .dependencies ),
30533077 list (self .suppressed ),
3078+ self .suppressed_deps_opts (),
30543079 self .imports_ignored ,
30553080 dep_prios ,
30563081 dep_lines ,
@@ -3126,10 +3151,8 @@ def generate_unused_ignore_notes(self) -> None:
31263151 self .options .warn_unused_ignores
31273152 or codes .UNUSED_IGNORE in self .options .enabled_error_codes
31283153 ) and codes .UNUSED_IGNORE not in self .options .disabled_error_codes :
3129- # If this file was initially loaded from the cache, it may have suppressed
3130- # dependencies due to imports with ignores on them. We need to generate
3131- # those errors to avoid spuriously flagging them as unused ignores.
3132- if self .meta :
3154+ # We only need this for the daemon, regular incremental does this unconditionally.
3155+ if self .meta and self .options .fine_grained_incremental :
31333156 self .verify_dependencies (suppressed_only = True )
31343157 self .manager .errors .generate_unused_ignore_errors (self .xpath )
31353158
@@ -3710,20 +3733,22 @@ def load_graph(
37103733 # but A's cached *indirect* dependency on C is wrong.
37113734 dependencies = [dep for dep in st .dependencies if st .priorities .get (dep ) != PRI_INDIRECT ]
37123735 if not manager .use_fine_grained_cache ():
3713- # TODO: Ideally we could skip here modules that appeared in st.suppressed
3714- # because they are not in build with `follow-imports=skip`.
3715- # This way we could avoid overhead of cloning options in `State.__init__()`
3716- # below to get the option value. This is quite minor performance loss however.
37173736 added = [dep for dep in st .suppressed if find_module_simple (dep , manager )]
37183737 else :
37193738 # During initial loading we don't care about newly added modules,
37203739 # they will be taken care of during fine-grained update. See also
3721- # comment about this in `State.__init__ ()`.
3740+ # comment about this in `State.new_state ()`.
37223741 added = []
37233742 for dep in st .ancestors + dependencies + st .suppressed :
37243743 ignored = dep in st .suppressed_set and dep not in entry_points
37253744 if ignored and dep not in added :
37263745 manager .missing_modules .add (dep )
3746+ # TODO: for now we skip this in the daemon as a performance optimization.
3747+ # This however creates a correctness issue, see #7777 and State.is_fresh().
3748+ if not manager .use_fine_grained_cache ():
3749+ manager .import_options [dep ] = manager .options .clone_for_module (
3750+ dep
3751+ ).dep_import_options ()
37273752 elif dep not in graph :
37283753 try :
37293754 if dep in st .ancestors :
@@ -4129,6 +4154,9 @@ def process_stale_scc(
41294154 t2 = time .time ()
41304155 stale = scc
41314156 for id in stale :
4157+ # Re-generate import errors in case this module was loaded from the cache.
4158+ if graph [id ].meta :
4159+ graph [id ].verify_dependencies (suppressed_only = True )
41324160 # We may already have parsed the module, or not.
41334161 # If the former, parse_file() is a no-op.
41344162 graph [id ].parse_file ()
0 commit comments