rules_leastsurprise.py
- Pfad:
/var/www/tools/ki-protokoll/claude-hook/quality/rules_leastsurprise.py - Namespace: claude-hook.quality
- Zeilen: 232 | Größe: 7,701 Bytes
- Geändert: 2025-12-28 12:41:50 | Gescannt: 2025-12-31 10:22:15
Code Hygiene Score: 98
- Dependencies: 100 (25%)
- LOC: 89 (20%)
- Methods: 100 (20%)
- Secrets: 100 (15%)
- Classes: 100 (10%)
- Magic Numbers: 100 (10%)
Keine Issues gefunden.
Dependencies 4
- extends Rule
- use re
- use typing.List
- use rule_base.Rule
Klassen 6
-
W15_1_GetterWithSideEffectclass Zeile 19 -
W15_2_SetterWithReturnclass Zeile 74 -
W15_3_BoolMethodWithoutQuestionclass Zeile 93 -
W15_4_ConstructorWithReturnclass Zeile 114 -
W15_5_VoidMethodWithEchoclass Zeile 151 -
W15_6_MagicMethodOverrideclass Zeile 190
Code
#!/usr/bin/env python3
"""
Post-Hook Least Surprise Regeln (WARN) - Least Surprise Principle.
W15.x Regeln: Warnt bei Code der sich anders verhält als erwartet.
Prinzip: "Code verhält sich so, wie Name und Struktur erwarten lassen. Keine Überraschungen."
"""
import re
from typing import List
from .rule_base import Rule
# =============================================================================
# W15: LEAST SURPRISE PRINCIPLE
# =============================================================================
class W15_1_GetterWithSideEffect(Rule):
"""W15.1: Getter mit Seiteneffekten - unerwartet bei lesenden Methoden."""
# Seiteneffekt-Indikatoren
SIDE_EFFECT_PATTERNS = [
r"\bsave\s*\(",
r"\bupdate\s*\(",
r"\bdelete\s*\(",
r"\binsert\s*\(",
r"\bwrite\s*\(",
r"\bpersist\s*\(",
r"\bflush\s*\(",
r"\bsend\s*\(",
r"\bexecute\s*\(",
r"\$this->\w+\s*=", # Property-Änderung
r"\$this->\w+\s*\[\s*\]\s*=", # Array-Push
r"\$this->\w+\s*\+\+", # Increment
r"\$this->\w+\s*--", # Decrement
]
def check(self, file_path: str, content: str) -> List[str]:
warnings = []
# Finde alle get*-Methoden
getter_pattern = r"function\s+(get[A-Z]\w*)\s*\([^)]*\)\s*(?::\s*[^{]+)?\s*\{"
getter_matches = list(re.finditer(getter_pattern, content))
for match in getter_matches:
method_name = match.group(1)
method_start = match.end()
# Finde das Ende der Methode
brace_count = 1
pos = method_start
while pos < len(content) and brace_count > 0:
if content[pos] == '{':
brace_count += 1
elif content[pos] == '}':
brace_count -= 1
pos += 1
method_body = content[method_start:pos]
# Prüfe auf Seiteneffekte
for side_effect_pattern in self.SIDE_EFFECT_PATTERNS:
if re.search(side_effect_pattern, method_body):
warnings.append(
f"W15.1: Getter '{method_name}' appears to have side effects. "
f"Getters should be pure read operations."
)
break # Nur eine Warnung pro Getter
return warnings
class W15_2_SetterWithReturn(Rule):
"""W15.2: Setter mit Return-Wert - unerwartet bei schreibenden Methoden."""
def check(self, file_path: str, content: str) -> List[str]:
warnings = []
# Finde set*-Methoden mit Return-Type (außer self/static für fluent)
pattern = r"function\s+(set[A-Z]\w*)\s*\([^)]+\)\s*:\s*(?!self|static|void|\$this)[A-Z]\w*"
matches = re.findall(pattern, content)
for method_name in matches:
warnings.append(
f"W15.2: Setter '{method_name}' has unexpected return type. "
f"Setters should return void or self (for fluent interface)."
)
return warnings
class W15_3_BoolMethodWithoutQuestion(Rule):
"""W15.3: Boolean-Methode ohne is/has/can/should Präfix."""
def check(self, file_path: str, content: str) -> List[str]:
warnings = []
# Finde Methoden die bool zurückgeben
pattern = r"function\s+(\w+)\s*\([^)]*\)\s*:\s*bool"
matches = re.findall(pattern, content)
for method_name in matches:
# Erlaubte Präfixe für bool-Methoden
if not re.match(r"^(is|has|can|should|will|was|does|did|are)", method_name):
warnings.append(
f"W15.3: Boolean method '{method_name}' should start with "
f"is/has/can/should for clarity."
)
return warnings
class W15_4_ConstructorWithReturn(Rule):
"""W15.4: Constructor mit explizitem return - unerwartet."""
def check(self, file_path: str, content: str) -> List[str]:
warnings = []
# Finde __construct Methoden
construct_match = re.search(
r"function\s+__construct\s*\([^)]*\)\s*\{",
content
)
if construct_match:
construct_start = construct_match.end()
# Finde das Ende des Constructors
brace_count = 1
pos = construct_start
while pos < len(content) and brace_count > 0:
if content[pos] == '{':
brace_count += 1
elif content[pos] == '}':
brace_count -= 1
pos += 1
construct_body = content[construct_start:pos]
# Prüfe auf return mit Wert (nicht nur 'return;')
if re.search(r"\breturn\s+[^;]+;", construct_body):
warnings.append(
"W15.4: Constructor has return statement with value. "
"Constructors should not return values."
)
return warnings
class W15_5_VoidMethodWithEcho(Rule):
"""W15.5: Void-Methode mit Echo - unerwartete Ausgabe."""
def check(self, file_path: str, content: str) -> List[str]:
# Überspringe Views und Templates
if "/View/" in file_path or "/templates/" in file_path:
return []
warnings = []
# Finde void-Methoden
pattern = r"function\s+(\w+)\s*\([^)]*\)\s*:\s*void\s*\{"
for match in re.finditer(pattern, content):
method_name = match.group(1)
method_start = match.end()
# Finde das Ende der Methode
brace_count = 1
pos = method_start
while pos < len(content) and brace_count > 0:
if content[pos] == '{':
brace_count += 1
elif content[pos] == '}':
brace_count -= 1
pos += 1
method_body = content[method_start:pos]
# Prüfe auf echo/print
if re.search(r"\b(echo|print)\b", method_body):
warnings.append(
f"W15.5: Void method '{method_name}' produces output. "
f"Consider returning a value instead of echoing."
)
return warnings
class W15_6_MagicMethodOverride(Rule):
"""W15.6: Magic Methods mit unerwarteter Semantik."""
def check(self, file_path: str, content: str) -> List[str]:
warnings = []
# __toString sollte keine Exception werfen
tostring_match = re.search(r"function\s+__toString\s*\([^)]*\)\s*:\s*string\s*\{", content)
if tostring_match:
method_start = tostring_match.end()
brace_count = 1
pos = method_start
while pos < len(content) and brace_count > 0:
if content[pos] == '{':
brace_count += 1
elif content[pos] == '}':
brace_count -= 1
pos += 1
method_body = content[method_start:pos]
if re.search(r"\bthrow\s+", method_body):
warnings.append(
"W15.6: __toString() throws exception. This can cause hard-to-debug errors. "
"Return error string instead."
)
return warnings
# =============================================================================
# RULE COLLECTION
# =============================================================================
RULES = [
W15_1_GetterWithSideEffect(),
W15_2_SetterWithReturn(),
W15_3_BoolMethodWithoutQuestion(),
W15_4_ConstructorWithReturn(),
W15_5_VoidMethodWithEcho(),
W15_6_MagicMethodOverride(),
]