Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions src/modules/analyzer/method_analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,20 @@ def _analyze_names(self, tree: ast.AST, result: AnalysisResult):
def _check_common_issues(self, tree: ast.AST, result: AnalysisResult, source: str):
"""Prüft auf häufige Code-Probleme"""

# 1. Doppelte Funktionsdefinitionen auf Modul-Ebene
seen_top_functions: Dict[str, int] = {}
for item in getattr(tree, 'body', []):
if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
if item.name in seen_top_functions:
first_line = seen_top_functions[item.name]
result.warnings.append({
'type': 'DuplicateFunction',
'message': f"Doppelte Funktion '{item.name}' im Modul-Scope (überschreibt Definition in Zeile {first_line})",
'line': item.lineno
})
else:
seen_top_functions[item.name] = item.lineno

for node in ast.walk(tree):
# Bare except
if isinstance(node, ast.ExceptHandler):
Expand Down Expand Up @@ -488,6 +502,89 @@ def _check_common_issues(self, tree: ast.AST, result: AnalysisResult, source: st
'line': node.lineno
})

# Doppelte Methodendefinitionen in Klassen
if isinstance(node, ast.ClassDef):
seen_methods: Dict[str, int] = {}
for item in node.body:
if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
if item.name in seen_methods:
first_line = seen_methods[item.name]
result.warnings.append({
'type': 'DuplicateMethod',
'message': f"Doppelte Methode '{item.name}' in Klasse '{node.name}' (überschreibt Definition in Zeile {first_line})",
'line': item.lineno
})
else:
seen_methods[item.name] = item.lineno

# Assert mit Tupel-Literal (immer wahr)
if isinstance(node, ast.Assert):
if isinstance(node.test, ast.Tuple) and len(node.test.elts) > 0:
result.warnings.append({
'type': 'AssertTuple',
'message': "Assert mit Tupel-Literal ist immer wahr: 'assert (cond, msg)' -> 'assert cond, msg' verwenden",
'line': node.lineno
})

# Vergleiche: None, boolesche Literale und Identitätsvergleiche
if isinstance(node, ast.Compare):
for op, comparator in zip(node.ops, node.comparators):
if isinstance(op, (ast.Eq, ast.NotEq)):
left_none = isinstance(node.left, ast.Constant) and node.left.value is None
right_none = isinstance(comparator, ast.Constant) and comparator.value is None
if left_none or right_none:
result.warnings.append({
'type': 'ComparisonToNone',
'message': "Vergleich mit None sollte 'is None' bzw. 'is not None' verwenden (PEP 8)",
'line': node.lineno
})
left_bool = isinstance(node.left, ast.Constant) and isinstance(node.left.value, bool)
right_bool = isinstance(comparator, ast.Constant) and isinstance(comparator.value, bool)
if left_bool or right_bool:
result.warnings.append({
'type': 'ComparisonToBool',
'message': "Vergleich mit booleschem Literal sollte 'if x:' bzw. 'if not x:' verwenden (PEP 8)",
'line': node.lineno
})
elif isinstance(op, (ast.Is, ast.IsNot)):
left_lit = isinstance(node.left, ast.Constant) and isinstance(node.left.value, (int, float, str, bytes)) and not isinstance(node.left.value, bool)
right_lit = isinstance(comparator, ast.Constant) and isinstance(comparator.value, (int, float, str, bytes)) and not isinstance(comparator.value, bool)
if left_lit or right_lit:
result.warnings.append({
'type': 'LiteralIdentityComparison',
'message': "Identitätsvergleich ('is'/'is not') mit Literal ist implementationsabhängig - '==' bzw. '!=' verwenden",
'line': node.lineno
})

# Raise von Literalen anstelle von Exceptions
if isinstance(node, ast.Raise):
if node.exc and isinstance(node.exc, ast.Constant) and isinstance(node.exc.value, (str, int, float)):
result.warnings.append({
'type': 'RaiseNotAnException',
'message': f"Raise mit Literal ({type(node.exc.value).__name__}) anstelle einer Exception-Klasse",
'line': node.lineno
})

# Nicht erreichbarer Code nach return, raise, break, continue
for attr in ('body', 'orelse', 'finalbody'):
stmts = getattr(node, attr, None)
if isinstance(stmts, list) and len(stmts) > 1:
terminal_found = False
terminal_name = ""
terminal_line = 0
for s in stmts:
if terminal_found:
result.warnings.append({
'type': 'UnreachableCode',
'message': f"Nicht erreichbarer Code nach '{terminal_name}' (Zeile {terminal_line})",
'line': s.lineno
})
break
if isinstance(s, (ast.Return, ast.Raise, ast.Break, ast.Continue)):
terminal_found = True
terminal_name = type(s).__name__.lower()
terminal_line = s.lineno

# TODO-Kommentare finden
pass # Wird über Source-Lines gemacht

Expand Down
27 changes: 27 additions & 0 deletions tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,33 @@ def broken(:
self.assertTrue(len(result.errors) > 0)
self.assertEqual(result.errors[0]['type'], 'SyntaxError')

def test_duplicate_function_and_method_detection(self):
"""Erkennt überschriebene Funktionen und Methoden im selben Scope."""
code = '''
def duplicate():
return 1

def duplicate():
return 2

class Example:
def repeated(self):
return 1

def repeated(self):
return 2
'''

file_path = os.path.join(self.temp_dir, "duplicates.py")
with open(file_path, 'w', encoding='utf-8') as handle:
handle.write(code)

result = self.analyzer.analyze_file(file_path)
warnings_by_type = {warning['type']: warning for warning in result.warnings}

self.assertEqual(warnings_by_type['DuplicateFunction']['line'], 5)
self.assertEqual(warnings_by_type['DuplicateMethod']['line'], 12)

def test_complexity_calculation(self):
"""Testet Komplexitäts-Berechnung"""
code = '''
Expand Down
Loading