4747from  mypy .graph_utils  import  prepare_sccs , strongly_connected_components , topsort 
4848from  mypy .indirection  import  TypeIndirectionVisitor 
4949from  mypy .messages  import  MessageBuilder 
50- from  mypy .nodes  import  Import , ImportAll , ImportBase , ImportFrom , MypyFile , SymbolTable ,  TypeInfo 
50+ from  mypy .nodes  import  Import , ImportAll , ImportBase , ImportFrom , MypyFile , SymbolTable 
5151from  mypy .partially_defined  import  PossiblyUndefinedVariableVisitor 
5252from  mypy .semanal  import  SemanticAnalyzer 
5353from  mypy .semanal_pass1  import  SemanticAnalyzerPreAnalysis 
@@ -335,6 +335,7 @@ class CacheMeta(NamedTuple):
335335    # dep_prios and dep_lines are in parallel with dependencies + suppressed 
336336    dep_prios : list [int ]
337337    dep_lines : list [int ]
338+     dep_hashes : dict [str , str ]
338339    interface_hash : str   # hash representing the public interface 
339340    version_id : str   # mypy version for cache invalidation 
340341    ignore_all : bool   # if errors were ignored 
@@ -373,6 +374,7 @@ def cache_meta_from_dict(meta: dict[str, Any], data_json: str) -> CacheMeta:
373374        meta .get ("options" ),
374375        meta .get ("dep_prios" , []),
375376        meta .get ("dep_lines" , []),
377+         meta .get ("dep_hashes" , {}),
376378        meta .get ("interface_hash" , "" ),
377379        meta .get ("version_id" , sentinel ),
378380        meta .get ("ignore_all" , True ),
@@ -890,8 +892,6 @@ def log(self, *message: str) -> None:
890892            self .stderr .flush ()
891893
892894    def  log_fine_grained (self , * message : str ) ->  None :
893-         import  mypy .build 
894- 
895895        if  self .verbosity () >=  1 :
896896            self .log ("fine-grained:" , * message )
897897        elif  mypy .build .DEBUG_FINE_GRAINED :
@@ -1500,6 +1500,7 @@ def validate_meta(
15001500                "options" : (manager .options .clone_for_module (id ).select_options_affecting_cache ()),
15011501                "dep_prios" : meta .dep_prios ,
15021502                "dep_lines" : meta .dep_lines ,
1503+                 "dep_hashes" : meta .dep_hashes ,
15031504                "interface_hash" : meta .interface_hash ,
15041505                "version_id" : manager .version_id ,
15051506                "ignore_all" : meta .ignore_all ,
@@ -1543,7 +1544,7 @@ def write_cache(
15431544    source_hash : str ,
15441545    ignore_all : bool ,
15451546    manager : BuildManager ,
1546- ) ->  tuple [str , CacheMeta  |  None ]:
1547+ ) ->  tuple [str , tuple [ dict [ str ,  Any ],  str ,  str ]  |  None ]:
15471548    """Write cache files for a module. 
15481549
15491550    Note that this mypy's behavior is still correct when any given 
@@ -1564,9 +1565,9 @@ def write_cache(
15641565      manager: the build manager (for pyversion, log/trace) 
15651566
15661567    Returns: 
1567-       A tuple containing the interface hash and CacheMeta  
1568-       corresponding to the metadata that was written (the latter  may 
1569-       be None  if the cache could not be written). 
1568+       A tuple containing the interface hash and inner tuple with cache meta JSON  
1569+       that should be written and paths to cache files (inner tuple  may be None,  
1570+       if the cache data  could not be written). 
15701571    """ 
15711572    metastore  =  manager .metastore 
15721573    # For Bazel we use relative paths and zero mtimes. 
@@ -1581,6 +1582,8 @@ def write_cache(
15811582    if  bazel :
15821583        tree .path  =  path 
15831584
1585+     plugin_data  =  manager .plugin .report_config_data (ReportConfigContext (id , path , is_check = False ))
1586+ 
15841587    # Serialize data and analyze interface 
15851588    if  manager .options .fixed_format_cache :
15861589        data_io  =  Buffer ()
@@ -1589,9 +1592,7 @@ def write_cache(
15891592    else :
15901593        data  =  tree .serialize ()
15911594        data_bytes  =  json_dumps (data , manager .options .debug_cache )
1592-     interface_hash  =  hash_digest (data_bytes )
1593- 
1594-     plugin_data  =  manager .plugin .report_config_data (ReportConfigContext (id , path , is_check = False ))
1595+     interface_hash  =  hash_digest (data_bytes  +  json_dumps (plugin_data ))
15951596
15961597    # Obtain and set up metadata 
15971598    st  =  manager .get_stat (path )
@@ -1659,16 +1660,22 @@ def write_cache(
16591660        "ignore_all" : ignore_all ,
16601661        "plugin_data" : plugin_data ,
16611662    }
1663+     return  interface_hash , (meta , meta_json , data_json )
16621664
1665+ 
1666+ def  write_cache_meta (
1667+     meta : dict [str , Any ], manager : BuildManager , meta_json : str , data_json : str 
1668+ ) ->  CacheMeta :
16631669    # Write meta cache file 
1670+     metastore  =  manager .metastore 
16641671    meta_str  =  json_dumps (meta , manager .options .debug_cache )
16651672    if  not  metastore .write (meta_json , meta_str ):
16661673        # Most likely the error is the replace() call 
16671674        # (see https://github.com/python/mypy/issues/3215). 
16681675        # The next run will simply find the cache entry out of date. 
16691676        manager .log (f"Error writing meta JSON file { meta_json }  " )
16701677
1671-     return  interface_hash ,  cache_meta_from_dict (meta , data_json )
1678+     return  cache_meta_from_dict (meta , data_json )
16721679
16731680
16741681def  delete_cache (id : str , path : str , manager : BuildManager ) ->  None :
@@ -1758,26 +1765,24 @@ def delete_cache(id: str, path: str, manager: BuildManager) -> None:
17581765
17591766For single nodes, processing is simple.  If the node was cached, we 
17601767deserialize the cache data and fix up cross-references.  Otherwise, we 
1761- do semantic analysis followed by type checking.  We also handle (c) 
1762- above; if a module has valid cache data *but* any of its 
1763- dependencies was processed from source, then the module should be 
1764- processed from source. 
1765- 
1766- A relatively simple optimization (outside SCCs) we might do in the 
1767- future is as follows: if a node's cache data is valid, but one or more 
1768- of its dependencies are out of date so we have to re-parse the node 
1769- from source, once we have fully type-checked the node, we can decide 
1770- whether its symbol table actually changed compared to the cache data 
1771- (by reading the cache data and comparing it to the data we would be 
1772- writing).  If there is no change we can declare the node up to date, 
1773- and any node that depends (and for which we have cached data, and 
1774- whose other dependencies are up to date) on it won't need to be 
1775- re-parsed from source. 
1768+ do semantic analysis followed by type checking.  Once we (re-)processed 
1769+ an SCC we check whether its interface (symbol table) is still fresh 
1770+ (matches previous cached value). If it is not, we consider dependent SCCs 
1771+ stale so that they need to be re-parsed as well. 
1772+ 
1773+ Note on indirect dependencies: normally dependencies are determined from 
1774+ imports, but since our interfaces are "opaque" (i.e. symbol tables can 
1775+ contain cross-references as well as types identified by name), these are not 
1776+ enough.  We *must* also add "indirect" dependencies from symbols and types to 
1777+ their definitions.  For this purpose, we record all accessed symbols during 
1778+ semantic analysis, and after we finished processing a module, we traverse its 
1779+ type map, and for each type we find (transitively) on which named types it 
1780+ depends. 
17761781
17771782Import cycles 
17781783------------- 
17791784
1780- Finally we have to decide how to handle (c ), import cycles.  Here 
1785+ Finally we have to decide how to handle (b ), import cycles.  Here 
17811786we'll need a modified version of the original state machine 
17821787(build.py), but we only need to do this per SCC, and we won't have to 
17831788deal with changes to the list of nodes while we're processing it. 
@@ -1867,6 +1872,9 @@ class State:
18671872    # Map each dependency to the line number where it is first imported 
18681873    dep_line_map : dict [str , int ]
18691874
1875+     # Map from dependency id to its last observed interface hash 
1876+     dep_hashes : dict [str , str ] =  {}
1877+ 
18701878    # Parent package, its parent, etc. 
18711879    ancestors : list [str ] |  None  =  None 
18721880
@@ -1879,9 +1887,6 @@ class State:
18791887    # If caller_state is set, the line number in the caller where the import occurred 
18801888    caller_line  =  0 
18811889
1882-     # If True, indicate that the public interface of this module is unchanged 
1883-     externally_same  =  True 
1884- 
18851890    # Contains a hash of the public interface in incremental mode 
18861891    interface_hash : str  =  "" 
18871892
@@ -1994,6 +1999,7 @@ def __init__(
19941999            self .priorities  =  {id : pri  for  id , pri  in  zip (all_deps , self .meta .dep_prios )}
19952000            assert  len (all_deps ) ==  len (self .meta .dep_lines )
19962001            self .dep_line_map  =  {id : line  for  id , line  in  zip (all_deps , self .meta .dep_lines )}
2002+             self .dep_hashes  =  self .meta .dep_hashes 
19972003            if  temporary :
19982004                self .load_tree (temporary = True )
19992005            if  not  manager .use_fine_grained_cache ():
@@ -2046,26 +2052,17 @@ def is_fresh(self) -> bool:
20462052        """Return whether the cache data for this file is fresh.""" 
20472053        # NOTE: self.dependencies may differ from 
20482054        # self.meta.dependencies when a dependency is dropped due to 
2049-         # suppression by silent mode.  However when a suppressed 
2055+         # suppression by silent mode.  However,  when a suppressed 
20502056        # dependency is added back we find out later in the process. 
2051-         return  (
2052-             self .meta  is  not   None 
2053-             and  self .is_interface_fresh ()
2054-             and  self .dependencies  ==  self .meta .dependencies 
2055-         )
2056- 
2057-     def  is_interface_fresh (self ) ->  bool :
2058-         return  self .externally_same 
2057+         return  self .meta  is  not   None  and  self .dependencies  ==  self .meta .dependencies 
20592058
20602059    def  mark_as_rechecked (self ) ->  None :
20612060        """Marks this module as having been fully re-analyzed by the type-checker.""" 
20622061        self .manager .rechecked_modules .add (self .id )
20632062
2064-     def  mark_interface_stale (self ,  * ,  on_errors :  bool   =   False ) ->  None :
2063+     def  mark_interface_stale (self ) ->  None :
20652064        """Marks this module as having a stale public interface, and discards the cache data.""" 
2066-         self .externally_same  =  False 
2067-         if  not  on_errors :
2068-             self .manager .stale_modules .add (self .id )
2065+         self .manager .stale_modules .add (self .id )
20692066
20702067    def  check_blockers (self ) ->  None :
20712068        """Raise CompileError if a blocking error is detected.""" 
@@ -2410,21 +2407,15 @@ def finish_passes(self) -> None:
24102407
24112408            # We should always patch indirect dependencies, even in full (non-incremental) builds, 
24122409            # because the cache still may be written, and it must be correct. 
2413-             # TODO: find a more robust way to traverse *all* relevant types? 
2414-             all_types  =  list (self .type_map ().values ())
2415-             for  _ , sym , _  in  self .tree .local_definitions ():
2416-                 if  sym .type  is  not   None :
2417-                     all_types .append (sym .type )
2418-                 if  isinstance (sym .node , TypeInfo ):
2419-                     # TypeInfo symbols have some extra relevant types. 
2420-                     all_types .extend (sym .node .bases )
2421-                     if  sym .node .metaclass_type :
2422-                         all_types .append (sym .node .metaclass_type )
2423-                     if  sym .node .typeddict_type :
2424-                         all_types .append (sym .node .typeddict_type )
2425-                     if  sym .node .tuple_type :
2426-                         all_types .append (sym .node .tuple_type )
2427-             self ._patch_indirect_dependencies (self .type_checker ().module_refs , all_types )
2410+             self ._patch_indirect_dependencies (
2411+                 # Two possible sources of indirect dependencies: 
2412+                 # * Symbols not directly imported in this module but accessed via an attribute 
2413+                 #   or via a re-export (vast majority of these recorded in semantic analysis). 
2414+                 # * For each expression type we need to record definitions of type components 
2415+                 #   since "meaning" of the type may be updated when definitions are updated. 
2416+                 self .tree .module_refs  |  self .type_checker ().module_refs ,
2417+                 set (self .type_map ().values ()),
2418+             )
24282419
24292420            if  self .options .dump_inference_stats :
24302421                dump_type_stats (
@@ -2453,7 +2444,7 @@ def free_state(self) -> None:
24532444            self ._type_checker .reset ()
24542445            self ._type_checker  =  None 
24552446
2456-     def  _patch_indirect_dependencies (self , module_refs : set [str ], types : list [Type ]) ->  None :
2447+     def  _patch_indirect_dependencies (self , module_refs : set [str ], types : set [Type ]) ->  None :
24572448        assert  None  not  in   types 
24582449        valid  =  self .valid_references ()
24592450
@@ -2507,7 +2498,7 @@ def valid_references(self) -> set[str]:
25072498
25082499        return  valid_refs 
25092500
2510-     def  write_cache (self ) ->  None :
2501+     def  write_cache (self ) ->  tuple [ dict [ str ,  Any ],  str ,  str ]  |   None :
25112502        assert  self .tree  is  not   None , "Internal error: method must be called on parsed file only" 
25122503        # We don't support writing cache files in fine-grained incremental mode. 
25132504        if  (
@@ -2525,20 +2516,19 @@ def write_cache(self) -> None:
25252516                except  Exception :
25262517                    print (f"Error serializing { self .id }  " , file = self .manager .stdout )
25272518                    raise   # Propagate to display traceback 
2528-             return 
2519+             return   None 
25292520        is_errors  =  self .transitive_error 
25302521        if  is_errors :
25312522            delete_cache (self .id , self .path , self .manager )
25322523            self .meta  =  None 
2533-             self .mark_interface_stale (on_errors = True )
2534-             return 
2524+             return  None 
25352525        dep_prios  =  self .dependency_priorities ()
25362526        dep_lines  =  self .dependency_lines ()
25372527        assert  self .source_hash  is  not   None 
25382528        assert  len (set (self .dependencies )) ==  len (
25392529            self .dependencies 
25402530        ), f"Duplicates in dependencies list for { self .id }   ({ self .dependencies }  )" 
2541-         new_interface_hash , self . meta  =  write_cache (
2531+         new_interface_hash , meta_tuple  =  write_cache (
25422532            self .id ,
25432533            self .path ,
25442534            self .tree ,
@@ -2557,6 +2547,7 @@ def write_cache(self) -> None:
25572547            self .manager .log (f"Cached module { self .id }   has changed interface" )
25582548            self .mark_interface_stale ()
25592549            self .interface_hash  =  new_interface_hash 
2550+         return  meta_tuple 
25602551
25612552    def  verify_dependencies (self , suppressed_only : bool  =  False ) ->  None :
25622553        """Report errors for import targets in modules that don't exist. 
@@ -3287,7 +3278,19 @@ def process_graph(graph: Graph, manager: BuildManager) -> None:
32873278        for  id  in  scc :
32883279            deps .update (graph [id ].dependencies )
32893280        deps  -=  ascc 
3290-         stale_deps  =  {id  for  id  in  deps  if  id  in  graph  and  not  graph [id ].is_interface_fresh ()}
3281+ 
3282+         # Verify that interfaces of dependencies still present in graph are up-to-date (fresh). 
3283+         # Note: if a dependency is not in graph anymore, it should be considered interface-stale. 
3284+         # This is important to trigger any relevant updates from indirect dependencies that were 
3285+         # removed in load_graph(). 
3286+         stale_deps  =  set ()
3287+         for  id  in  ascc :
3288+             for  dep  in  graph [id ].dep_hashes :
3289+                 if  dep  not  in   graph :
3290+                     stale_deps .add (dep )
3291+                     continue 
3292+                 if  graph [dep ].interface_hash  !=  graph [id ].dep_hashes [dep ]:
3293+                     stale_deps .add (dep )
32913294        fresh  =  fresh  and  not  stale_deps 
32923295        undeps  =  set ()
32933296        if  fresh :
@@ -3518,14 +3521,25 @@ def process_stale_scc(graph: Graph, scc: list[str], manager: BuildManager) -> No
35183521    if  any (manager .errors .is_errors_for_file (graph [id ].xpath ) for  id  in  stale ):
35193522        for  id  in  stale :
35203523            graph [id ].transitive_error  =  True 
3524+     meta_tuples  =  {}
35213525    for  id  in  stale :
35223526        if  graph [id ].xpath  not  in   manager .errors .ignored_files :
35233527            errors  =  manager .errors .file_messages (
35243528                graph [id ].xpath , formatter = manager .error_formatter 
35253529            )
35263530            manager .flush_errors (manager .errors .simplify_path (graph [id ].xpath ), errors , False )
3527-         graph [id ].write_cache ()
3531+         meta_tuples [ id ]  =   graph [id ].write_cache ()
35283532        graph [id ].mark_as_rechecked ()
3533+     for  id  in  stale :
3534+         meta_tuple  =  meta_tuples [id ]
3535+         if  meta_tuple  is  None :
3536+             graph [id ].meta  =  None 
3537+             continue 
3538+         meta , meta_json , data_json  =  meta_tuple 
3539+         meta ["dep_hashes" ] =  {
3540+             dep : graph [dep ].interface_hash  for  dep  in  graph [id ].dependencies  if  dep  in  graph 
3541+         }
3542+         graph [id ].meta  =  write_cache_meta (meta , manager , meta_json , data_json )
35293543
35303544
35313545def  sorted_components (
0 commit comments