@@ -360,6 +360,182 @@ def test_collect_unused_import_lines_handles_dotted_imports(self) -> None:
360360 "import os.path muss als entfernbar markiert werden wenn 'os' ungenutzt ist" ,
361361 )
362362
363+ def test_collect_unused_import_lines_keeps_future_imports (self ) -> None :
364+ """Regression (Bug D): from __future__ import annotations darf nicht entfernt
365+ werden, auch wenn 'annotations' nicht explizit als Name genutzt wird."""
366+ import ast as _ast
367+ sys .path .insert (0 , str (PROJECT_ROOT ))
368+ from MethodenAnalyser3 import _collect_unused_import_lines
369+
370+ code = "from __future__ import annotations\n import os\n x = 1\n "
371+ tree = _ast .parse (code )
372+ lines_to_remove = _collect_unused_import_lines (tree , {"annotations" , "os" })
373+
374+ self .assertNotIn (1 , lines_to_remove , "__future__-Import darf nicht entfernt werden" )
375+ self .assertIn (2 , lines_to_remove , "normaler unbenutzter Import muss markiert werden" )
376+
377+
378+ class TestEncodingHandling (unittest .TestCase ):
379+ """Tests für Encoding-Fallback bei nicht-UTF-8-Dateien (Latin-1)."""
380+
381+ def setUp (self ):
382+ sys .path .insert (0 , str (PROJECT_ROOT ))
383+ self .tmpdir = tempfile .mkdtemp ()
384+
385+ def tearDown (self ):
386+ import shutil
387+ shutil .rmtree (self .tmpdir , ignore_errors = True )
388+
389+ def test_analyze_project_latin1_file_not_in_errors (self ) -> None :
390+ """Regression (Bug B): analyze_project() darf latin-1-Dateien nicht in
391+ files_with_errors listen, wenn die Analyse per Encoding-Fallback erfolgreich war."""
392+ from MethodenAnalyser3 import analyze_project
393+
394+ latin1_code = b"# encoding: latin-1\n import os\n x = 'caf\xe9 '\n "
395+ file_path = os .path .join (self .tmpdir , "latin1_file.py" )
396+ with open (file_path , "wb" ) as f :
397+ f .write (latin1_code )
398+
399+ result = analyze_project (self .tmpdir )
400+
401+ error_paths = [e [0 ] for e in result .files_with_errors ]
402+ self .assertNotIn (
403+ file_path ,
404+ error_paths ,
405+ "latin-1-Datei darf nicht in files_with_errors stehen wenn Analyse erfolgreich war" ,
406+ )
407+ self .assertEqual (result .files_analyzed , 1 )
408+
409+ def _call_auto_fix (self , filepath , result ):
410+ """Setzt Globals, ruft auto_fix_unused_imports mit gemockter GUI auf."""
411+ import unittest .mock
412+ import MethodenAnalyser3 as m3
413+ orig_path = m3 ._last_analysis_path
414+ orig_result = m3 ._last_analysis_result
415+ try :
416+ m3 ._last_analysis_path = filepath
417+ m3 ._last_analysis_result = result
418+ with unittest .mock .patch ("MethodenAnalyser3.messagebox" ) as mb :
419+ mb .askyesno .return_value = True
420+ m3 .auto_fix_unused_imports (unittest .mock .MagicMock ())
421+ finally :
422+ m3 ._last_analysis_path = orig_path
423+ m3 ._last_analysis_result = orig_result
424+
425+ def test_auto_fix_works_on_latin1_file (self ) -> None :
426+ """Regression (Bug A): auto_fix_unused_imports() darf bei latin-1-Dateien
427+ nicht mit UnicodeDecodeError abstuerzen und muss den Import korrekt entfernen."""
428+ import MethodenAnalyser3 as m3
429+
430+ latin1_code = b"import os\n import sys\n x = 'caf\xe9 '\n print(sys.argv)\n "
431+ filepath = os .path .join (self .tmpdir , "latin1_autofix.py" )
432+ with open (filepath , "wb" ) as f :
433+ f .write (latin1_code )
434+
435+ result = m3 .analyze_file (filepath )
436+ self .assertIn ("os" , result .unused_imports )
437+
438+ self ._call_auto_fix (filepath , result )
439+
440+ with open (filepath , "r" , encoding = "latin-1" ) as f :
441+ content = f .read ()
442+ self .assertNotIn ("import os\n " , content )
443+ self .assertIn ("import sys\n " , content )
444+
445+ def test_auto_fix_form_feed_line_alignment (self ) -> None :
446+ """Regression (Fix A): Form-Feed \\ x0c darf AST-Zeilennummern nicht verschieben
447+ — splitlines() wuerde bei \\ x0c extra Zeilen erzeugen, readlines() nicht."""
448+ import MethodenAnalyser3 as m3
449+
450+ # \x0c vor import os: splitlines() wuerde Zeile 1=leer, 2=import os sehen,
451+ # AST sieht aber lineno=1 fuer import os — readlines() bleibt konsistent.
452+ code = b"\x0c import os\n import sys\n print(os.getcwd())\n "
453+ filepath = os .path .join (self .tmpdir , "formfeed_autofix.py" )
454+ with open (filepath , "wb" ) as f :
455+ f .write (code )
456+
457+ result = m3 .analyze_file (filepath )
458+ self .assertIn ("sys" , result .unused_imports )
459+ self .assertNotIn ("os" , result .unused_imports )
460+
461+ self ._call_auto_fix (filepath , result )
462+
463+ with open (filepath , "r" , encoding = "utf-8" ) as f :
464+ content = f .read ()
465+ self .assertNotIn ("import sys" , content )
466+ self .assertIn ("import os" , content )
467+
468+ def test_auto_fix_preserves_latin1_encoding_for_non_ascii_content (self ) -> None :
469+ """Regression (Bug C): auto_fix darf bei latin-1-Dateien das Encoding nicht auf
470+ UTF-8 aendern — wuerde Dateien mit '# coding: latin-1' und nicht-ASCII korrumpieren."""
471+ import MethodenAnalyser3 as m3
472+
473+ # Datei mit latin-1 Nicht-ASCII-Zeichen (café = caf + \xe9)
474+ latin1_code = b"import os\n import sys\n x = 'caf\xe9 '\n print(sys.argv)\n "
475+ filepath = os .path .join (self .tmpdir , "latin1_nonascii.py" )
476+ with open (filepath , "wb" ) as f :
477+ f .write (latin1_code )
478+
479+ result = m3 .analyze_file (filepath )
480+ self .assertIn ("os" , result .unused_imports )
481+
482+ self ._call_auto_fix (filepath , result )
483+
484+ # Datei muss weiterhin als latin-1 lesbar sein (kein UnicodeDecodeError)
485+ with open (filepath , "rb" ) as f :
486+ raw_bytes = f .read ()
487+ # Das nicht-ASCII-Byte \xe9 (é in latin-1) muss erhalten bleiben
488+ self .assertIn (b"\xe9 " , raw_bytes , "latin-1 Byte \\ xe9 darf nach auto_fix nicht fehlen" )
489+ # Darf NICHT als UTF-8-Sequenz \xc3\xa9 codiert worden sein
490+ self .assertNotIn (b"\xc3 \xa9 " , raw_bytes , "Encoding darf nicht von latin-1 auf UTF-8 geaendert worden sein" )
491+
492+
493+ class TestExceptHandlerAndDunders (unittest .TestCase ):
494+ """Tests für Bug E (ExceptHandler-Binding) und Bug F (Module-Dunders)."""
495+
496+ def setUp (self ):
497+ sys .path .insert (0 , str (PROJECT_ROOT ))
498+
499+ def test_except_binding_not_in_missing_imports (self ) -> None :
500+ """Regression (Bug E): 'except Exception as e:' darf 'e' nicht als
501+ missing_import ausweisen — ExceptHandler.name ist ein str, kein ast.Name-Knoten."""
502+ from MethodenAnalyser3 import analyze_source
503+
504+ code = textwrap .dedent ("""
505+ import sys
506+
507+ def run():
508+ try:
509+ pass
510+ except Exception as e:
511+ print(e)
512+ """ ).strip () + "\n "
513+
514+ result = analyze_source (code )
515+ self .assertNotIn (
516+ "e" ,
517+ result .missing_imports ,
518+ "Exception-Binding 'e' darf nicht als missing_import erscheinen" ,
519+ )
520+
521+ def test_module_dunders_not_in_missing_imports (self ) -> None :
522+ """Regression (Bug F): __file__, __name__, __doc__ sind implizit verfuegbar
523+ und duerfen nicht als missing_imports erscheinen."""
524+ from MethodenAnalyser3 import analyze_source
525+
526+ code = textwrap .dedent ("""
527+ def info():
528+ print(__file__, __name__, __doc__)
529+ """ ).strip () + "\n "
530+
531+ result = analyze_source (code )
532+ for dunder in ("__file__" , "__name__" , "__doc__" ):
533+ self .assertNotIn (
534+ dunder ,
535+ result .missing_imports ,
536+ f"{ dunder } ist implizit verfuegbar und darf nicht in missing_imports stehen" ,
537+ )
538+
363539
364540class TranslatorIsGermanTests (unittest .TestCase ):
365541 """Tests für TranslationSystem._is_german()."""
0 commit comments