@@ -92,7 +92,7 @@ class ModuleDepDict(TypedDict):
9292 line : int
9393
9494
95- class DeadCandidateDict (TypedDict ):
95+ class DeadCandidateDictBase (TypedDict ):
9696 qualname : str
9797 local_name : str
9898 filepath : str
@@ -101,6 +101,10 @@ class DeadCandidateDict(TypedDict):
101101 kind : str
102102
103103
104+ class DeadCandidateDict (DeadCandidateDictBase , total = False ):
105+ suppressed_rules : list [str ]
106+
107+
104108class StructuralFindingOccurrenceDict (TypedDict ):
105109 qualname : str
106110 start : int
@@ -1041,14 +1045,17 @@ def _dead_candidate_dict_from_model(
10411045 candidate : DeadCandidate ,
10421046 filepath : str ,
10431047) -> DeadCandidateDict :
1044- return DeadCandidateDict (
1048+ result = DeadCandidateDict (
10451049 qualname = candidate .qualname ,
10461050 local_name = candidate .local_name ,
10471051 filepath = filepath ,
10481052 start_line = candidate .start_line ,
10491053 end_line = candidate .end_line ,
10501054 kind = candidate .kind ,
10511055 )
1056+ if candidate .suppressed_rules :
1057+ result ["suppressed_rules" ] = sorted (set (candidate .suppressed_rules ))
1058+ return result
10521059
10531060
10541061def _structural_occurrence_dict_from_model (
@@ -1203,14 +1210,32 @@ def _canonicalize_cache_entry(entry: CacheEntry) -> CacheEntry:
12031210 item ["line" ],
12041211 ),
12051212 )
1213+ dead_candidates_normalized : list [DeadCandidateDict ] = []
1214+ for candidate in entry ["dead_candidates" ]:
1215+ suppressed_rules = candidate .get ("suppressed_rules" , [])
1216+ normalized_candidate = DeadCandidateDict (
1217+ qualname = candidate ["qualname" ],
1218+ local_name = candidate ["local_name" ],
1219+ filepath = candidate ["filepath" ],
1220+ start_line = candidate ["start_line" ],
1221+ end_line = candidate ["end_line" ],
1222+ kind = candidate ["kind" ],
1223+ )
1224+ if _is_string_list (suppressed_rules ):
1225+ normalized_rules = sorted (set (suppressed_rules ))
1226+ if normalized_rules :
1227+ normalized_candidate ["suppressed_rules" ] = normalized_rules
1228+ dead_candidates_normalized .append (normalized_candidate )
1229+
12061230 dead_candidates_sorted = sorted (
1207- entry [ "dead_candidates" ] ,
1231+ dead_candidates_normalized ,
12081232 key = lambda item : (
12091233 item ["start_line" ],
12101234 item ["end_line" ],
12111235 item ["qualname" ],
12121236 item ["local_name" ],
12131237 item ["kind" ],
1238+ tuple (item .get ("suppressed_rules" , [])),
12141239 ),
12151240 )
12161241
@@ -1803,13 +1828,19 @@ def _decode_wire_dead_candidate(
18031828 filepath : str ,
18041829) -> DeadCandidateDict | None :
18051830 row = _as_list (value )
1806- if row is None or len (row ) != 5 :
1831+ if row is None or len (row ) not in { 5 , 6 } :
18071832 return None
18081833 qualname = _as_str (row [0 ])
18091834 local_name = _as_str (row [1 ])
18101835 start_line = _as_int (row [2 ])
18111836 end_line = _as_int (row [3 ])
18121837 kind = _as_str (row [4 ])
1838+ suppressed_rules : list [str ] | None = []
1839+ if len (row ) == 6 :
1840+ raw_rules = _as_list (row [5 ])
1841+ if raw_rules is None or not all (isinstance (rule , str ) for rule in raw_rules ):
1842+ return None
1843+ suppressed_rules = sorted ({str (rule ) for rule in raw_rules if str (rule )})
18131844 if (
18141845 qualname is None
18151846 or local_name is None
@@ -1818,14 +1849,17 @@ def _decode_wire_dead_candidate(
18181849 or kind is None
18191850 ):
18201851 return None
1821- return DeadCandidateDict (
1852+ decoded = DeadCandidateDict (
18221853 qualname = qualname ,
18231854 local_name = local_name ,
18241855 filepath = filepath ,
18251856 start_line = start_line ,
18261857 end_line = end_line ,
18271858 kind = kind ,
18281859 )
1860+ if suppressed_rules :
1861+ decoded ["suppressed_rules" ] = suppressed_rules
1862+ return decoded
18291863
18301864
18311865def _encode_wire_file_entry (entry : CacheEntry ) -> dict [str , object ]:
@@ -1980,16 +2014,22 @@ def _encode_wire_file_entry(entry: CacheEntry) -> dict[str, object]:
19802014 if dead_candidates :
19812015 # Dead candidates are stored inside a per-file cache entry, so the
19822016 # filepath is implicit and does not need to be repeated in every row.
1983- wire ["dc" ] = [
1984- [
2017+ encoded_dead_candidates : list [list [object ]] = []
2018+ for candidate in dead_candidates :
2019+ encoded = [
19852020 candidate ["qualname" ],
19862021 candidate ["local_name" ],
19872022 candidate ["start_line" ],
19882023 candidate ["end_line" ],
19892024 candidate ["kind" ],
19902025 ]
1991- for candidate in dead_candidates
1992- ]
2026+ suppressed_rules = candidate .get ("suppressed_rules" , [])
2027+ if _is_string_list (suppressed_rules ):
2028+ normalized_rules = sorted (set (suppressed_rules ))
2029+ if normalized_rules :
2030+ encoded .append (normalized_rules )
2031+ encoded_dead_candidates .append (encoded )
2032+ wire ["dc" ] = encoded_dead_candidates
19932033
19942034 if entry ["referenced_names" ]:
19952035 wire ["rn" ] = sorted (set (entry ["referenced_names" ]))
@@ -2135,11 +2175,16 @@ def _is_module_dep_dict(value: object) -> bool:
21352175def _is_dead_candidate_dict (value : object ) -> bool :
21362176 if not isinstance (value , dict ):
21372177 return False
2138- return _has_typed_fields (
2178+ if not _has_typed_fields (
21392179 value ,
21402180 string_keys = ("qualname" , "local_name" , "filepath" , "kind" ),
21412181 int_keys = ("start_line" , "end_line" ),
2142- )
2182+ ):
2183+ return False
2184+ suppressed_rules = value .get ("suppressed_rules" )
2185+ if suppressed_rules is None :
2186+ return True
2187+ return _is_string_list (suppressed_rules )
21432188
21442189
21452190def _is_string_list (value : object ) -> bool :
0 commit comments