{
"tool_response": {
"type": "update",
"filePath": "\/var\/www\/tools\/ki-protokoll\/claude-hook\/quality\/post_rules.py",
"content": "#!\/usr\/bin\/env python3\n\"\"\"\nPost-Hook Regeln (WARN) - Beliebig erweiterbar ohne Claude-Neustart.\n\nAlle W-Regeln aus dem Quality Gate Plan.\nOrganisiert in separate Module nach Kategorie.\n\"\"\"\n\nfrom typing import List\nfrom .rules_quality import RULES as QUALITY_RULES\nfrom .rules_security import RULES as SECURITY_RULES\nfrom .rules_style import RULES as STYLE_RULES\n\n\n# =============================================================================\n# ALLE AKTIVEN REGELN\n# =============================================================================\n\nRULES = QUALITY_RULES + SECURITY_RULES + STYLE_RULES\n\n\n# =============================================================================\n# MAIN CHECK FUNCTION\n# =============================================================================\n\ndef check(file_path: str, content: str) -> dict:\n \"\"\"\n Führt alle Post-Hook-Regeln aus.\n Sammelt alle Warnungen.\n\n Args:\n file_path: Pfad zur zu prüfenden Datei\n content: Dateiinhalt\n\n Returns:\n Dict mit warnings-Liste\n \"\"\"\n all_warnings = []\n\n for rule in RULES:\n warnings = rule(file_path, content)\n all_warnings.extend(warnings)\n\n return {\"warnings\": all_warnings}\n",
"structuredPatch": [
{
"oldStart": 3,
"oldLines": 460,
"newStart": 3,
"newLines": 37,
"lines": [
" Post-Hook Regeln (WARN) - Beliebig erweiterbar ohne Claude-Neustart.",
" ",
" Alle W-Regeln aus dem Quality Gate Plan.",
"+Organisiert in separate Module nach Kategorie.",
" \"\"\"",
" ",
"-import re",
"-from pathlib import Path",
"-from collections import Counter",
" from typing import List",
"+from .rules_quality import RULES as QUALITY_RULES",
"+from .rules_security import RULES as SECURITY_RULES",
"+from .rules_style import RULES as STYLE_RULES",
" ",
"-# =============================================================================",
"-# ALLOWLIST",
"-# =============================================================================",
" ",
"-GLOBAL_ALLOWLIST = [",
"- \"\/vendor\/\",",
"- \"\/tests\/\",",
"- \"\/Test\/\",",
"-]",
"-",
"-DTO_ALLOWLIST = [",
"- \"\/Infrastructure\/DTO\/\",",
"- \"\/DTO\/\",",
"-]",
"-",
"-MIGRATION_ALLOWLIST = [",
"- \"\/migrations\/\",",
"- \"\/Migration\/\",",
"-]",
"-",
" # =============================================================================",
"-# COMMON NUMBERS - Erlaubte Magic Numbers",
"+# ALLE AKTIVEN REGELN",
" # =============================================================================",
" ",
"-COMMON_NUMBERS = {",
"- '0', '1', '2',",
"- '10', '100', '1000',",
"- '60', '24', '365',",
"- '30', '31', '28', '29',",
"- '12', '52', '7',",
"- '200', '201', '204',",
"- '301', '302', '304',",
"- '400', '401', '403', '404', '500',",
"-}",
"+RULES = QUALITY_RULES + SECURITY_RULES + STYLE_RULES",
" ",
"-# =============================================================================",
"-# HELPER FUNCTIONS",
"-# =============================================================================",
" ",
"-def is_in_allowlist(file_path: str, allowlist: list) -> bool:",
"- \"\"\"Prüft ob Pfad in Allowlist ist.\"\"\"",
"- return any(allowed in file_path for allowed in allowlist)",
"-",
"-",
"-def count_non_empty_lines(content: str) -> int:",
"- \"\"\"Zählt nicht-leere Zeilen.\"\"\"",
"- return len([line for line in content.split('\\n') if line.strip()])",
"-",
"-",
" # =============================================================================",
"-# PRÜFUNG 1: SRP + KISS (Warnungen)",
"-# =============================================================================",
"-",
"-def w1_1_class_size(file_path: str, content: str) -> List[str]:",
"- \"\"\"W1.1: Klassengröße.\"\"\"",
"- if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):",
"- return []",
"-",
"- loc = count_non_empty_lines(content)",
"- warnings = []",
"-",
"- if loc > 300:",
"- warnings.append(f\"W1.1: Class has {loc} lines (max 300). Consider splitting.\")",
"- elif loc > 200:",
"- warnings.append(f\"W1.1: Class has {loc} lines (approaching limit of 300).\")",
"-",
"- return warnings",
"-",
"-",
"-def w1_2_public_method_count(file_path: str, content: str) -> List[str]:",
"- \"\"\"W1.2: Anzahl public methods.\"\"\"",
"- if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):",
"- return []",
"-",
"- public_methods = re.findall(r\"public\\s+function\\s+\\w+\", content)",
"- count = len(public_methods)",
"-",
"- if count > 10:",
"- return [f\"W1.2: Class has {count} public methods (max 10). Consider splitting.\"]",
"-",
"- return []",
"-",
"-",
"-def w1_3_constructor_params(file_path: str, content: str) -> List[str]:",
"- \"\"\"W1.3: Constructor-Parameter.\"\"\"",
"- if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):",
"- return []",
"-",
"- # Finde Constructor",
"- constructor_match = re.search(r\"function\\s+__construct\\s*\\(([^)]*)\\)\", content, re.DOTALL)",
"- if not constructor_match:",
"- return []",
"-",
"- params = constructor_match.group(1)",
"- # Zähle Parameter (durch Komma getrennt, aber nicht in Klammern)",
"- param_count = len([p for p in params.split(',') if p.strip()])",
"-",
"- if param_count > 5:",
"- return [f\"W1.3: Constructor has {param_count} parameters (max 5). Consider refactoring.\"]",
"-",
"- return []",
"-",
"-",
"-def w1_4_dependency_count(file_path: str, content: str) -> List[str]:",
"- \"\"\"W1.4: Anzahl use-Statements.\"\"\"",
"- if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):",
"- return []",
"-",
"- use_statements = re.findall(r\"^use\\s+\", content, re.MULTILINE)",
"- count = len(use_statements)",
"-",
"- if count > 10:",
"- return [f\"W1.4: Class has {count} dependencies (max 10). Consider reducing coupling.\"]",
"-",
"- return []",
"-",
"-",
"-def w1_5_suspicious_names(file_path: str, content: str) -> List[str]:",
"- \"\"\"W1.5: Verdächtige Namen (Hint).\"\"\"",
"- if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):",
"- return []",
"-",
"- filename = Path(file_path).stem",
"- hints = []",
"-",
"- if \"Manager\" in filename:",
"- hints.append(f\"W1.5: Hint - '{filename}' contains 'Manager'. Verify single responsibility.\")",
"-",
"- # \"And\" als separates Wort im CamelCase",
"- if re.search(r\"[a-z]And[A-Z]\", filename):",
"- hints.append(f\"W1.5: Hint - '{filename}' contains 'And'. May indicate multiple responsibilities.\")",
"-",
"- return hints",
"-",
"-",
"-# =============================================================================",
"-# PRÜFUNG 2: MVC + CRUD (Warnungen)",
"-# =============================================================================",
"-",
"-def w2_1_business_keywords_in_controller(file_path: str, content: str) -> List[str]:",
"- \"\"\"W2.1: Business-Keywords in Controller.\"\"\"",
"- if \"\/Controller\/\" not in file_path:",
"- return []",
"- if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):",
"- return []",
"-",
"- keywords = [",
"- (r\"\\bcalculate\\w*\\s*\\(\", \"calculate\"),",
"- (r\"\\bcompute\\w*\\s*\\(\", \"compute\"),",
"- (r\"\\bvalidate\\w*\\s*\\(\", \"validate\"),",
"- (r\"\\bprocess\\w*\\s*\\(\", \"process\"),",
"- (r\"\\btransform\\w*\\s*\\(\", \"transform\"),",
"- (r\"\\bconvert\\w*\\s*\\(\", \"convert\"),",
"- ]",
"-",
"- found = []",
"- for pattern, name in keywords:",
"- if re.search(pattern, content, re.IGNORECASE):",
"- found.append(name)",
"-",
"- if found:",
"- return [f\"W2.1: Business keywords in Controller: {', '.join(found)}. Consider moving to Domain\/UseCase.\"]",
"-",
"- return []",
"-",
"-",
"-def w2_2_private_methods_in_controller(file_path: str, content: str) -> List[str]:",
"- \"\"\"W2.2: Viele private Methoden in Controller.\"\"\"",
"- if \"\/Controller\/\" not in file_path:",
"- return []",
"- if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):",
"- return []",
"-",
"- private_methods = re.findall(r\"private\\s+function\\s+\\w+\", content)",
"- count = len(private_methods)",
"-",
"- if count > 5:",
"- return [f\"W2.2: Controller has {count} private methods (max 5). Extract to Service.\"]",
"-",
"- return []",
"-",
"-",
"-# =============================================================================",
"-# PRÜFUNG 3: PSR + Types (Warnungen)",
"-# =============================================================================",
"-",
"-def w3_1_potential_untyped_params(file_path: str, content: str) -> List[str]:",
"- \"\"\"W3.1: Potentiell untypisierte Parameter.\"\"\"",
"- if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):",
"- return []",
"-",
"- # Heuristik: Suche nach $param direkt nach ( oder ,",
"- potential = re.findall(r\"function\\s+\\w+\\s*\\([^)]*[(,]\\s*\\$\", content)",
"-",
"- if potential:",
"- return [\"W3.1: Potential untyped parameters detected. Run PHPStan for verification.\"]",
"-",
"- return []",
"-",
"-",
"-def w3_3_mixed_type(file_path: str, content: str) -> List[str]:",
"- \"\"\"W3.3: mixed Type verwendet.\"\"\"",
"- if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):",
"- return []",
"-",
"- if re.search(r\":\\s*mixed\\b\", content):",
"- return [\"W3.3: 'mixed' type used. Consider more specific type.\"]",
"-",
"- return []",
"-",
"-",
"-# =============================================================================",
"-# PRÜFUNG 4: OOP (Warnungen)",
"-# =============================================================================",
"-",
"-def w4_1_anemic_model(file_path: str, content: str) -> List[str]:",
"- \"\"\"W4.1: Anämisches Model (hohe Accessor-Ratio).\"\"\"",
"- if is_in_allowlist(file_path, GLOBAL_ALLOWLIST + DTO_ALLOWLIST):",
"- return []",
"-",
"- getters = len(re.findall(r\"public\\s+function\\s+get[A-Z]\\w*\\s*\\(\", content))",
"- setters = len(re.findall(r\"public\\s+function\\s+set[A-Z]\\w*\\s*\\(\", content))",
"- all_public = len(re.findall(r\"public\\s+function\\s+\\w+\", content))",
"-",
"- if all_public > 4:",
"- accessor_ratio = (getters + setters) \/ all_public",
"- if accessor_ratio > 0.7:",
"- return [f\"W4.1: Potential anemic model: {int(accessor_ratio*100)}% accessors. Consider adding behavior.\"]",
"-",
"- return []",
"-",
"-",
"-def w4_2_class_without_behavior(file_path: str, content: str) -> List[str]:",
"- \"\"\"W4.2: Klasse ohne Verhalten.\"\"\"",
"- if is_in_allowlist(file_path, GLOBAL_ALLOWLIST + DTO_ALLOWLIST):",
"- return []",
"-",
"- has_properties = bool(re.search(r\"(?:private|protected|public)\\s+.*\\$\\w+\", content))",
"- methods = re.findall(r\"(?:public|private|protected)\\s+function\\s+(\\w+)\", content)",
"- real_methods = [m for m in methods if not m.startswith((\"get\", \"set\", \"__\"))]",
"-",
"- if has_properties and len(real_methods) == 0:",
"- return [\"W4.2: Class has properties but no behavior methods.\"]",
"-",
"- return []",
"-",
"-",
"-def w4_3_low_encapsulation(file_path: str, content: str) -> List[str]:",
"- \"\"\"W4.3: Niedrige Kapselung.\"\"\"",
"- if is_in_allowlist(file_path, GLOBAL_ALLOWLIST + DTO_ALLOWLIST):",
"- return []",
"-",
"- public_props = len(re.findall(r\"public\\s+(?!function|const).*\\$\", content))",
"- protected_props = len(re.findall(r\"protected\\s+(?!function|const).*\\$\", content))",
"- private_props = len(re.findall(r\"private\\s+(?!function|const).*\\$\", content))",
"-",
"- total = public_props + protected_props + private_props",
"- if total > 3:",
"- exposed_ratio = (public_props + protected_props) \/ total",
"- if exposed_ratio > 0.5:",
"- return [f\"W4.3: Low encapsulation: {int(exposed_ratio*100)}% exposed properties.\"]",
"-",
"- return []",
"-",
"-",
"-def w4_4_high_static_ratio(file_path: str, content: str) -> List[str]:",
"- \"\"\"W4.4: Hohe Static-Ratio.\"\"\"",
"- if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):",
"- return []",
"-",
"- static_methods = len(re.findall(r\"public\\s+static\\s+function\", content))",
"- all_methods = len(re.findall(r\"public\\s+function\", content))",
"-",
"- if all_methods > 0 and static_methods > 2:",
"- static_ratio = static_methods \/ all_methods",
"- if static_ratio > 0.5:",
"- return [f\"W4.4: High static method ratio: {static_methods}\/{all_methods}. Consider instance methods.\"]",
"-",
"- return []",
"-",
"-",
"-# =============================================================================",
"-# PRÜFUNG 5: DRY (Warnungen)",
"-# =============================================================================",
"-",
"-def w5_1_repeated_strings(file_path: str, content: str) -> List[str]:",
"- \"\"\"W5.1: Wiederholte String-Literale.\"\"\"",
"- if is_in_allowlist(file_path, GLOBAL_ALLOWLIST + MIGRATION_ALLOWLIST):",
"- return []",
"-",
"- # Finde Strings > 20 Zeichen",
"- string_literals = re.findall(r'\"([^\"]{20,})\"', content)",
"- counts = Counter(string_literals)",
"-",
"- warnings = []",
"- for s, count in counts.items():",
"- if count > 3:",
"- truncated = s[:40] + \"...\" if len(s) > 40 else s",
"- warnings.append(f\"W5.1: String repeated {count}x: '{truncated}' - consider constant.\")",
"-",
"- return warnings",
"-",
"-",
"-def w5_2_magic_numbers(file_path: str, content: str) -> List[str]:",
"- \"\"\"W5.2: Wiederholte Magic Numbers.\"\"\"",
"- if is_in_allowlist(file_path, GLOBAL_ALLOWLIST + MIGRATION_ALLOWLIST):",
"- return []",
"-",
"- # Finde Zahlen (2+ Ziffern, nicht in Konstanten)",
"- numbers = re.findall(r'(?<![A-Z_])(\\d{2,})(?![A-Z_])', content)",
"-",
"- warnings = []",
"- for num in set(numbers):",
"- if num not in COMMON_NUMBERS and numbers.count(num) > 2:",
"- warnings.append(f\"W5.2: Magic number {num} repeated {numbers.count(num)}x - consider constant.\")",
"-",
"- return warnings",
"-",
"-",
"-# =============================================================================",
"-# PRÜFUNG 6: SOLID + DIP (Warnungen)",
"-# =============================================================================",
"-",
"-def w6_1_interface_too_large(file_path: str, content: str) -> List[str]:",
"- \"\"\"W6.1: Interface mit zu vielen Methoden.\"\"\"",
"- if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):",
"- return []",
"-",
"- interface_match = re.search(r\"interface\\s+(\\w+)[^{]*\\{\", content)",
"- if not interface_match:",
"- return []",
"-",
"- interface_name = interface_match.group(1)",
"-",
"- # Zähle function-Deklarationen im Interface",
"- # Einfache Heuristik: Zähle alle \"public function\" nach dem Interface-Match",
"- interface_start = interface_match.end()",
"- interface_content = content[interface_start:]",
"-",
"- # Finde das Ende des Interface (erste } auf Level 0)",
"- brace_count = 1",
"- end_pos = 0",
"- for i, char in enumerate(interface_content):",
"- if char == '{':",
"- brace_count += 1",
"- elif char == '}':",
"- brace_count -= 1",
"- if brace_count == 0:",
"- end_pos = i",
"- break",
"-",
"- interface_body = interface_content[:end_pos]",
"- method_count = len(re.findall(r\"function\\s+\\w+\", interface_body))",
"-",
"- if method_count > 7:",
"- return [f\"W6.1: Interface '{interface_name}' has {method_count} methods (max 7). Consider splitting (ISP).\"]",
"-",
"- return []",
"-",
"-",
"-def w6_2_too_many_interfaces(file_path: str, content: str) -> List[str]:",
"- \"\"\"W6.2: Klasse implementiert zu viele Interfaces.\"\"\"",
"- if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):",
"- return []",
"-",
"- implements_match = re.search(r\"implements\\s+([^{]+)\", content)",
"- if not implements_match:",
"- return []",
"-",
"- interfaces = [i.strip() for i in implements_match.group(1).split(',') if i.strip()]",
"- count = len(interfaces)",
"-",
"- if count > 5:",
"- return [f\"W6.2: Class implements {count} interfaces (max 5). Potential role confusion.\"]",
"-",
"- return []",
"-",
"-",
"-# =============================================================================",
"-# PRÜFUNG 7: DI + AUTOWIRING",
"-# =============================================================================",
"-",
"-def w7_1_usecase_interface_dependency(file_path: str, content: str) -> List[str]:",
"- \"\"\"W7.1: UseCase mit Interface-Dependency - Hinweis zur Registrierung.\"\"\"",
"- if \"\/UseCases\/\" not in file_path:",
"- return []",
"- if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):",
"- return []",
"-",
"- # Finde Constructor-Parameter die auf Interface enden",
"- constructor_match = re.search(r\"function\\s+__construct\\s*\\(([^)]*)\\)\", content, re.DOTALL)",
"- if not constructor_match:",
"- return []",
"-",
"- params = constructor_match.group(1)",
"- interface_deps = re.findall(r\"(\\w+Interface)\\s+\\$\", params)",
"-",
"- if interface_deps:",
"- return [f\"W7.1: Hint - UseCase uses interfaces: {', '.join(interface_deps)}. Ensure registered in services.php.\"]",
"-",
"- return []",
"-",
"-",
"-# =============================================================================",
" # MAIN CHECK FUNCTION",
" # =============================================================================",
" ",
"-# Alle aktiven Regeln",
"-RULES = [",
"- # Prüfung 1: SRP + KISS",
"- w1_1_class_size,",
"- w1_2_public_method_count,",
"- w1_3_constructor_params,",
"- w1_4_dependency_count,",
"- w1_5_suspicious_names,",
"-",
"- # Prüfung 2: MVC + CRUD",
"- w2_1_business_keywords_in_controller,",
"- w2_2_private_methods_in_controller,",
"-",
"- # Prüfung 3: PSR + Types",
"- w3_1_potential_untyped_params,",
"- w3_3_mixed_type,",
"-",
"- # Prüfung 4: OOP",
"- w4_1_anemic_model,",
"- w4_2_class_without_behavior,",
"- w4_3_low_encapsulation,",
"- w4_4_high_static_ratio,",
"-",
"- # Prüfung 5: DRY",
"- w5_1_repeated_strings,",
"- w5_2_magic_numbers,",
"-",
"- # Prüfung 6: SOLID + DIP",
"- w6_1_interface_too_large,",
"- w6_2_too_many_interfaces,",
"-",
"- # Prüfung 7: DI + Autowiring",
"- w7_1_usecase_interface_dependency,",
"-]",
"-",
"-",
" def check(file_path: str, content: str) -> dict:",
" \"\"\"",
" Führt alle Post-Hook-Regeln aus.",
" Sammelt alle Warnungen.",
"+",
"+ Args:",
"+ file_path: Pfad zur zu prüfenden Datei",
"+ content: Dateiinhalt",
"+",
"+ Returns:",
"+ Dict mit warnings-Liste",
" \"\"\"",
" all_warnings = []",
" "
]
}
],
"originalFile": "#!\/usr\/bin\/env python3\n\"\"\"\nPost-Hook Regeln (WARN) - Beliebig erweiterbar ohne Claude-Neustart.\n\nAlle W-Regeln aus dem Quality Gate Plan.\n\"\"\"\n\nimport re\nfrom pathlib import Path\nfrom collections import Counter\nfrom typing import List\n\n# =============================================================================\n# ALLOWLIST\n# =============================================================================\n\nGLOBAL_ALLOWLIST = [\n \"\/vendor\/\",\n \"\/tests\/\",\n \"\/Test\/\",\n]\n\nDTO_ALLOWLIST = [\n \"\/Infrastructure\/DTO\/\",\n \"\/DTO\/\",\n]\n\nMIGRATION_ALLOWLIST = [\n \"\/migrations\/\",\n \"\/Migration\/\",\n]\n\n# =============================================================================\n# COMMON NUMBERS - Erlaubte Magic Numbers\n# =============================================================================\n\nCOMMON_NUMBERS = {\n '0', '1', '2',\n '10', '100', '1000',\n '60', '24', '365',\n '30', '31', '28', '29',\n '12', '52', '7',\n '200', '201', '204',\n '301', '302', '304',\n '400', '401', '403', '404', '500',\n}\n\n# =============================================================================\n# HELPER FUNCTIONS\n# =============================================================================\n\ndef is_in_allowlist(file_path: str, allowlist: list) -> bool:\n \"\"\"Prüft ob Pfad in Allowlist ist.\"\"\"\n return any(allowed in file_path for allowed in allowlist)\n\n\ndef count_non_empty_lines(content: str) -> int:\n \"\"\"Zählt nicht-leere Zeilen.\"\"\"\n return len([line for line in content.split('\\n') if line.strip()])\n\n\n# =============================================================================\n# PRÜFUNG 1: SRP + KISS (Warnungen)\n# =============================================================================\n\ndef w1_1_class_size(file_path: str, content: str) -> List[str]:\n \"\"\"W1.1: Klassengröße.\"\"\"\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):\n return []\n\n loc = count_non_empty_lines(content)\n warnings = []\n\n if loc > 300:\n warnings.append(f\"W1.1: Class has {loc} lines (max 300). Consider splitting.\")\n elif loc > 200:\n warnings.append(f\"W1.1: Class has {loc} lines (approaching limit of 300).\")\n\n return warnings\n\n\ndef w1_2_public_method_count(file_path: str, content: str) -> List[str]:\n \"\"\"W1.2: Anzahl public methods.\"\"\"\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):\n return []\n\n public_methods = re.findall(r\"public\\s+function\\s+\\w+\", content)\n count = len(public_methods)\n\n if count > 10:\n return [f\"W1.2: Class has {count} public methods (max 10). Consider splitting.\"]\n\n return []\n\n\ndef w1_3_constructor_params(file_path: str, content: str) -> List[str]:\n \"\"\"W1.3: Constructor-Parameter.\"\"\"\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):\n return []\n\n # Finde Constructor\n constructor_match = re.search(r\"function\\s+__construct\\s*\\(([^)]*)\\)\", content, re.DOTALL)\n if not constructor_match:\n return []\n\n params = constructor_match.group(1)\n # Zähle Parameter (durch Komma getrennt, aber nicht in Klammern)\n param_count = len([p for p in params.split(',') if p.strip()])\n\n if param_count > 5:\n return [f\"W1.3: Constructor has {param_count} parameters (max 5). Consider refactoring.\"]\n\n return []\n\n\ndef w1_4_dependency_count(file_path: str, content: str) -> List[str]:\n \"\"\"W1.4: Anzahl use-Statements.\"\"\"\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):\n return []\n\n use_statements = re.findall(r\"^use\\s+\", content, re.MULTILINE)\n count = len(use_statements)\n\n if count > 10:\n return [f\"W1.4: Class has {count} dependencies (max 10). Consider reducing coupling.\"]\n\n return []\n\n\ndef w1_5_suspicious_names(file_path: str, content: str) -> List[str]:\n \"\"\"W1.5: Verdächtige Namen (Hint).\"\"\"\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):\n return []\n\n filename = Path(file_path).stem\n hints = []\n\n if \"Manager\" in filename:\n hints.append(f\"W1.5: Hint - '{filename}' contains 'Manager'. Verify single responsibility.\")\n\n # \"And\" als separates Wort im CamelCase\n if re.search(r\"[a-z]And[A-Z]\", filename):\n hints.append(f\"W1.5: Hint - '{filename}' contains 'And'. May indicate multiple responsibilities.\")\n\n return hints\n\n\n# =============================================================================\n# PRÜFUNG 2: MVC + CRUD (Warnungen)\n# =============================================================================\n\ndef w2_1_business_keywords_in_controller(file_path: str, content: str) -> List[str]:\n \"\"\"W2.1: Business-Keywords in Controller.\"\"\"\n if \"\/Controller\/\" not in file_path:\n return []\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):\n return []\n\n keywords = [\n (r\"\\bcalculate\\w*\\s*\\(\", \"calculate\"),\n (r\"\\bcompute\\w*\\s*\\(\", \"compute\"),\n (r\"\\bvalidate\\w*\\s*\\(\", \"validate\"),\n (r\"\\bprocess\\w*\\s*\\(\", \"process\"),\n (r\"\\btransform\\w*\\s*\\(\", \"transform\"),\n (r\"\\bconvert\\w*\\s*\\(\", \"convert\"),\n ]\n\n found = []\n for pattern, name in keywords:\n if re.search(pattern, content, re.IGNORECASE):\n found.append(name)\n\n if found:\n return [f\"W2.1: Business keywords in Controller: {', '.join(found)}. Consider moving to Domain\/UseCase.\"]\n\n return []\n\n\ndef w2_2_private_methods_in_controller(file_path: str, content: str) -> List[str]:\n \"\"\"W2.2: Viele private Methoden in Controller.\"\"\"\n if \"\/Controller\/\" not in file_path:\n return []\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):\n return []\n\n private_methods = re.findall(r\"private\\s+function\\s+\\w+\", content)\n count = len(private_methods)\n\n if count > 5:\n return [f\"W2.2: Controller has {count} private methods (max 5). Extract to Service.\"]\n\n return []\n\n\n# =============================================================================\n# PRÜFUNG 3: PSR + Types (Warnungen)\n# =============================================================================\n\ndef w3_1_potential_untyped_params(file_path: str, content: str) -> List[str]:\n \"\"\"W3.1: Potentiell untypisierte Parameter.\"\"\"\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):\n return []\n\n # Heuristik: Suche nach $param direkt nach ( oder ,\n potential = re.findall(r\"function\\s+\\w+\\s*\\([^)]*[(,]\\s*\\$\", content)\n\n if potential:\n return [\"W3.1: Potential untyped parameters detected. Run PHPStan for verification.\"]\n\n return []\n\n\ndef w3_3_mixed_type(file_path: str, content: str) -> List[str]:\n \"\"\"W3.3: mixed Type verwendet.\"\"\"\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):\n return []\n\n if re.search(r\":\\s*mixed\\b\", content):\n return [\"W3.3: 'mixed' type used. Consider more specific type.\"]\n\n return []\n\n\n# =============================================================================\n# PRÜFUNG 4: OOP (Warnungen)\n# =============================================================================\n\ndef w4_1_anemic_model(file_path: str, content: str) -> List[str]:\n \"\"\"W4.1: Anämisches Model (hohe Accessor-Ratio).\"\"\"\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST + DTO_ALLOWLIST):\n return []\n\n getters = len(re.findall(r\"public\\s+function\\s+get[A-Z]\\w*\\s*\\(\", content))\n setters = len(re.findall(r\"public\\s+function\\s+set[A-Z]\\w*\\s*\\(\", content))\n all_public = len(re.findall(r\"public\\s+function\\s+\\w+\", content))\n\n if all_public > 4:\n accessor_ratio = (getters + setters) \/ all_public\n if accessor_ratio > 0.7:\n return [f\"W4.1: Potential anemic model: {int(accessor_ratio*100)}% accessors. Consider adding behavior.\"]\n\n return []\n\n\ndef w4_2_class_without_behavior(file_path: str, content: str) -> List[str]:\n \"\"\"W4.2: Klasse ohne Verhalten.\"\"\"\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST + DTO_ALLOWLIST):\n return []\n\n has_properties = bool(re.search(r\"(?:private|protected|public)\\s+.*\\$\\w+\", content))\n methods = re.findall(r\"(?:public|private|protected)\\s+function\\s+(\\w+)\", content)\n real_methods = [m for m in methods if not m.startswith((\"get\", \"set\", \"__\"))]\n\n if has_properties and len(real_methods) == 0:\n return [\"W4.2: Class has properties but no behavior methods.\"]\n\n return []\n\n\ndef w4_3_low_encapsulation(file_path: str, content: str) -> List[str]:\n \"\"\"W4.3: Niedrige Kapselung.\"\"\"\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST + DTO_ALLOWLIST):\n return []\n\n public_props = len(re.findall(r\"public\\s+(?!function|const).*\\$\", content))\n protected_props = len(re.findall(r\"protected\\s+(?!function|const).*\\$\", content))\n private_props = len(re.findall(r\"private\\s+(?!function|const).*\\$\", content))\n\n total = public_props + protected_props + private_props\n if total > 3:\n exposed_ratio = (public_props + protected_props) \/ total\n if exposed_ratio > 0.5:\n return [f\"W4.3: Low encapsulation: {int(exposed_ratio*100)}% exposed properties.\"]\n\n return []\n\n\ndef w4_4_high_static_ratio(file_path: str, content: str) -> List[str]:\n \"\"\"W4.4: Hohe Static-Ratio.\"\"\"\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST):\n return []\n\n static_methods = len(re.findall(r\"public\\s+static\\s+function\", content))\n all_methods = len(re.findall(r\"public\\s+function\", content))\n\n if all_methods > 0 and static_methods > 2:\n static_ratio = static_methods \/ all_methods\n if static_ratio > 0.5:\n return [f\"W4.4: High static method ratio: {static_methods}\/{all_methods}. Consider instance methods.\"]\n\n return []\n\n\n# =============================================================================\n# PRÜFUNG 5: DRY (Warnungen)\n# =============================================================================\n\ndef w5_1_repeated_strings(file_path: str, content: str) -> List[str]:\n \"\"\"W5.1: Wiederholte String-Literale.\"\"\"\n if is_in_allowlist(file_path, GLOBAL_ALLOWLIST + MIGRATION_ALLOWLIST):\n return []\n\n # Finde Strings > 20 Zeichen\n ... [TRUNCATED-b7b8062923829b73]"
}
}