@@ -222,6 +222,7 @@ def scan(self) -> ScanResult:
222222 css_classes : dict [str , list [tuple [str , int ]]] = {} # class -> [(file, line)]
223223 used_css_classes : set [str ] = set ()
224224 used_css_module_classes : dict [Path , set [str ]] = {}
225+ unresolved_css_module_classes : set [str ] = set ()
225226 components : dict [str , str ] = {} # ComponentName -> file
226227 routes : list [tuple [str , str ]] = [] # (route_path, file)
227228 star_reexports : list [tuple [str , str ]] = [] # (barrel_file, module_spec)
@@ -257,7 +258,9 @@ def scan(self) -> ScanResult:
257258 # CSS modules can also be consumed by plain TypeScript/JavaScript
258259 # files, for example when exporting class-name maps to components.
259260 if not self ._is_css_file (rel_path ):
260- self ._parse_css_module_usage (content , rel_path , file_set , used_css_module_classes )
261+ self ._parse_css_module_usage (
262+ content , rel_path , file_set , used_css_module_classes , unresolved_css_module_classes
263+ )
261264
262265 # Parse components
263266 if rel_path .endswith ((".tsx" , ".jsx" )):
@@ -286,7 +289,9 @@ def scan(self) -> ScanResult:
286289 self ._find_dead_routes (routes , all_files , result )
287290
288291 # 2c. Orphaned CSS
289- self ._find_orphaned_css (css_classes , used_css_classes , used_css_module_classes , result )
292+ self ._find_orphaned_css (
293+ css_classes , used_css_classes , used_css_module_classes , unresolved_css_module_classes , result
294+ )
290295
291296 # 2d. Unreferenced components
292297 # Collect all names that are imported somewhere (i.e., actually used)
@@ -514,6 +519,7 @@ def _parse_css_module_usage(
514519 rel_path : str ,
515520 file_set : set [str ],
516521 used_css_module_classes : dict [Path , set [str ]],
522+ unresolved_css_module_classes : set [str ],
517523 ) -> None :
518524 """Extract CSS-module class names consumed via object accessors.
519525
@@ -526,22 +532,30 @@ def _parse_css_module_usage(
526532 so that ``styles.card`` protects ``card`` only in that stylesheet.
527533 Bracket access ``styles['card-hover']`` is also captured. Canonical
528534 paths keep internal stylesheet symlinks and their targets consistent.
535+ Classes used through unresolved imports remain uncertain so alias or
536+ bundler-specific resolution cannot make live module classes removable.
529537 Without this, every CSS-module class is falsely reported as orphaned
530538 and marked removable=True, risking deletion of live styles.
531539 """
532540 # Map the local binding (e.g. ``styles``) to a CSS-module import so
533541 # only module accessors are treated as class names.
534- module_bindings : dict [str , Path ] = {}
542+ module_bindings : dict [str , Path | None ] = {}
535543 for match in _CSS_MODULE_IMPORT_PATTERN .finditer (content ):
536544 stylesheet = self ._resolve_relative_module (rel_path , match .group (2 ), file_set )
537545 if stylesheet is not None and stylesheet .endswith (".module.css" ):
538546 module_bindings [match .group (1 )] = (self .project_dir / stylesheet ).resolve ()
547+ else :
548+ module_bindings [match .group (1 )] = None
539549 if not module_bindings :
540550 return
541551 for m in _CSS_MODULE_USAGE_PATTERN .finditer (content ):
542552 binding , dot_name , _ , bracket_name = m .groups ()
543553 if binding in module_bindings :
544- used_css_module_classes .setdefault (module_bindings [binding ], set ()).add (dot_name or bracket_name )
554+ stylesheet_path = module_bindings [binding ]
555+ if stylesheet_path is None :
556+ unresolved_css_module_classes .add (dot_name or bracket_name )
557+ else :
558+ used_css_module_classes .setdefault (stylesheet_path , set ()).add (dot_name or bracket_name )
545559
546560 def _parse_components (self , content : str , rel_path : str , components : dict [str , str ]) -> None :
547561 """Extract React component definitions."""
@@ -669,6 +683,7 @@ def _find_orphaned_css(
669683 css_classes : dict [str , list [tuple [str , int ]]],
670684 used_css_classes : set [str ],
671685 used_css_module_classes : dict [Path , set [str ]],
686+ unresolved_css_module_classes : set [str ],
672687 result : ScanResult ,
673688 ) -> None :
674689 """Find CSS classes defined but never used in JSX."""
@@ -699,14 +714,19 @@ def _find_orphaned_css(
699714 continue
700715 elif cls in used_css_classes :
701716 continue
717+ uncertain = file .endswith (".module.css" ) and cls in unresolved_css_module_classes
702718 result .findings .append (
703719 Finding (
704720 file = file ,
705721 line = line ,
706722 name = cls ,
707723 category = "orphaned_css" ,
708- detail = f"CSS class '.{ cls } ' is not used in any JSX className" ,
709- removable = True ,
724+ detail = (
725+ f"CSS module class '.{ cls } ' may be used through an unresolved stylesheet import"
726+ if uncertain
727+ else f"CSS class '.{ cls } ' is not used in any JSX className"
728+ ),
729+ removable = not uncertain ,
710730 )
711731 )
712732
0 commit comments