i18n(uk): add missing files, translate P4 root docs
- Copy code/image/config files across all modules - Translate brand-voice and code-review templates - Translate CONTRIBUTING, CODE_OF_CONDUCT, SECURITY, STYLE_GUIDE - Copy CHANGELOG as-is (technical log) Ref: luongnv89/claude-howto#63
This commit is contained in:
5
uk/03-skills/.gitignore
vendored
Normal file
5
uk/03-skills/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
# Local skill testing
|
||||
.claude/
|
||||
|
||||
# Blog post outputs
|
||||
blog-posts/
|
||||
14
uk/03-skills/brand-voice/templates/email-template.txt
Normal file
14
uk/03-skills/brand-voice/templates/email-template.txt
Normal file
@@ -0,0 +1,14 @@
|
||||
Тема: [Чітка, орієнтована на вигоду тема]
|
||||
|
||||
Вітаю, [Ім'я]!
|
||||
|
||||
[Вступ: Яка цінність для них]
|
||||
|
||||
[Основна частина: Як це працює / Що вони отримають]
|
||||
|
||||
[Конкретний приклад або вигода]
|
||||
|
||||
[Заклик до дії: Чіткий наступний крок]
|
||||
|
||||
З повагою,
|
||||
[Ім'я]
|
||||
@@ -0,0 +1,4 @@
|
||||
[Хук: Привернути увагу в першому рядку]
|
||||
[2-3 рядки: Цінність або цікавий факт]
|
||||
[Заклик до дії: Посилання, питання або залучення]
|
||||
[Емодзі: 1-2 максимум для візуального інтересу]
|
||||
13
uk/03-skills/brand-voice/tone-examples.md
Normal file
13
uk/03-skills/brand-voice/tone-examples.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Приклади тону бренду
|
||||
|
||||
## Захоплива анонс
|
||||
"Зекономте 8 годин на тиждень на код-рев'ю. Claude переглядає ваші PR автоматично."
|
||||
|
||||
## Емпатична підтримка
|
||||
"Ми знаємо, що деплої можуть бути стресовими. Claude бере тестування на себе, щоб вам не хвилюватися."
|
||||
|
||||
## Впевнена презентація продукту
|
||||
"Claude не просто пропонує код. Він розуміє вашу архітектуру і підтримує консистентність."
|
||||
|
||||
## Освітній блог-пост
|
||||
"Давайте розглянемо, як агенти покращують процеси код-рев'ю. Ось що ми дізналися..."
|
||||
35
uk/03-skills/code-review/scripts/analyze-metrics.py
Normal file
35
uk/03-skills/code-review/scripts/analyze-metrics.py
Normal file
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env python3
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
def analyze_code_metrics(code):
|
||||
"""Analyze code for common metrics."""
|
||||
|
||||
# Count functions
|
||||
functions = len(re.findall(r"^def\s+\w+", code, re.MULTILINE))
|
||||
|
||||
# Count classes
|
||||
classes = len(re.findall(r"^class\s+\w+", code, re.MULTILINE))
|
||||
|
||||
# Average line length
|
||||
lines = code.split("\n")
|
||||
avg_length = sum(len(l) for l in lines) / len(lines) if lines else 0
|
||||
|
||||
# Estimate complexity
|
||||
complexity = len(re.findall(r"\b(if|elif|else|for|while|and|or)\b", code))
|
||||
|
||||
return {
|
||||
"functions": functions,
|
||||
"classes": classes,
|
||||
"avg_line_length": avg_length,
|
||||
"complexity_score": complexity,
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
with open(sys.argv[1]) as f:
|
||||
code = f.read()
|
||||
metrics = analyze_code_metrics(code)
|
||||
for key, value in metrics.items():
|
||||
print(f"{key}: {value:.2f}")
|
||||
174
uk/03-skills/code-review/scripts/compare-complexity.py
Normal file
174
uk/03-skills/code-review/scripts/compare-complexity.py
Normal file
@@ -0,0 +1,174 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Compare cyclomatic complexity of code before and after changes.
|
||||
Helps identify if refactoring actually simplifies code structure.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
class ComplexityAnalyzer:
|
||||
"""Analyze code complexity metrics."""
|
||||
|
||||
def __init__(self, code: str):
|
||||
self.code = code
|
||||
self.lines = code.split("\n")
|
||||
|
||||
def calculate_cyclomatic_complexity(self) -> int:
|
||||
"""
|
||||
Calculate cyclomatic complexity using McCabe's method.
|
||||
Count decision points: if, elif, else, for, while, except, and, or
|
||||
"""
|
||||
complexity = 1 # Base complexity
|
||||
|
||||
# Count decision points
|
||||
decision_patterns = [
|
||||
r"\bif\b",
|
||||
r"\belif\b",
|
||||
r"\bfor\b",
|
||||
r"\bwhile\b",
|
||||
r"\bexcept\b",
|
||||
r"\band\b(?!$)",
|
||||
r"\bor\b(?!$)",
|
||||
]
|
||||
|
||||
for pattern in decision_patterns:
|
||||
matches = re.findall(pattern, self.code)
|
||||
complexity += len(matches)
|
||||
|
||||
return complexity
|
||||
|
||||
def calculate_cognitive_complexity(self) -> int:
|
||||
"""
|
||||
Calculate cognitive complexity - how hard is it to understand?
|
||||
Based on nesting depth and control flow.
|
||||
"""
|
||||
cognitive = 0
|
||||
nesting_depth = 0
|
||||
|
||||
for line in self.lines:
|
||||
# Track nesting depth
|
||||
if re.search(r"^\s*(if|for|while|def|class|try)\b", line):
|
||||
nesting_depth += 1
|
||||
cognitive += nesting_depth
|
||||
elif re.search(r"^\s*(elif|else|except|finally)\b", line):
|
||||
cognitive += nesting_depth
|
||||
|
||||
# Reduce nesting when unindenting
|
||||
if line and not line[0].isspace():
|
||||
nesting_depth = 0
|
||||
|
||||
return cognitive
|
||||
|
||||
def calculate_maintainability_index(self) -> float:
|
||||
"""
|
||||
Maintainability Index ranges from 0-100.
|
||||
> 85: Excellent
|
||||
> 65: Good
|
||||
> 50: Fair
|
||||
< 50: Poor
|
||||
"""
|
||||
lines = len(self.lines)
|
||||
cyclomatic = self.calculate_cyclomatic_complexity()
|
||||
cognitive = self.calculate_cognitive_complexity()
|
||||
|
||||
# Simplified MI calculation
|
||||
mi = (
|
||||
171
|
||||
- 5.2 * (cyclomatic / lines)
|
||||
- 0.23 * (cognitive)
|
||||
- 16.2 * (lines / 1000)
|
||||
)
|
||||
|
||||
return max(0, min(100, mi))
|
||||
|
||||
def get_complexity_report(self) -> dict:
|
||||
"""Generate comprehensive complexity report."""
|
||||
return {
|
||||
"cyclomatic_complexity": self.calculate_cyclomatic_complexity(),
|
||||
"cognitive_complexity": self.calculate_cognitive_complexity(),
|
||||
"maintainability_index": round(self.calculate_maintainability_index(), 2),
|
||||
"lines_of_code": len(self.lines),
|
||||
"avg_line_length": round(
|
||||
sum(len(l) for l in self.lines) / len(self.lines), 2
|
||||
)
|
||||
if self.lines
|
||||
else 0,
|
||||
}
|
||||
|
||||
|
||||
def compare_files(before_file: str, after_file: str) -> None:
|
||||
"""Compare complexity metrics between two code versions."""
|
||||
|
||||
with open(before_file) as f:
|
||||
before_code = f.read()
|
||||
|
||||
with open(after_file) as f:
|
||||
after_code = f.read()
|
||||
|
||||
before_analyzer = ComplexityAnalyzer(before_code)
|
||||
after_analyzer = ComplexityAnalyzer(after_code)
|
||||
|
||||
before_metrics = before_analyzer.get_complexity_report()
|
||||
after_metrics = after_analyzer.get_complexity_report()
|
||||
|
||||
print("=" * 60)
|
||||
print("CODE COMPLEXITY COMPARISON")
|
||||
print("=" * 60)
|
||||
|
||||
print("\nBEFORE:")
|
||||
print(f" Cyclomatic Complexity: {before_metrics['cyclomatic_complexity']}")
|
||||
print(f" Cognitive Complexity: {before_metrics['cognitive_complexity']}")
|
||||
print(f" Maintainability Index: {before_metrics['maintainability_index']}")
|
||||
print(f" Lines of Code: {before_metrics['lines_of_code']}")
|
||||
print(f" Avg Line Length: {before_metrics['avg_line_length']}")
|
||||
|
||||
print("\nAFTER:")
|
||||
print(f" Cyclomatic Complexity: {after_metrics['cyclomatic_complexity']}")
|
||||
print(f" Cognitive Complexity: {after_metrics['cognitive_complexity']}")
|
||||
print(f" Maintainability Index: {after_metrics['maintainability_index']}")
|
||||
print(f" Lines of Code: {after_metrics['lines_of_code']}")
|
||||
print(f" Avg Line Length: {after_metrics['avg_line_length']}")
|
||||
|
||||
print("\nCHANGES:")
|
||||
cyclomatic_change = (
|
||||
after_metrics["cyclomatic_complexity"] - before_metrics["cyclomatic_complexity"]
|
||||
)
|
||||
cognitive_change = (
|
||||
after_metrics["cognitive_complexity"] - before_metrics["cognitive_complexity"]
|
||||
)
|
||||
mi_change = (
|
||||
after_metrics["maintainability_index"] - before_metrics["maintainability_index"]
|
||||
)
|
||||
loc_change = after_metrics["lines_of_code"] - before_metrics["lines_of_code"]
|
||||
|
||||
print(f" Cyclomatic Complexity: {cyclomatic_change:+d}")
|
||||
print(f" Cognitive Complexity: {cognitive_change:+d}")
|
||||
print(f" Maintainability Index: {mi_change:+.2f}")
|
||||
print(f" Lines of Code: {loc_change:+d}")
|
||||
|
||||
print("\nASSESSMENT:")
|
||||
if mi_change > 0:
|
||||
print(" ✅ Code is MORE maintainable")
|
||||
elif mi_change < 0:
|
||||
print(" ⚠️ Code is LESS maintainable")
|
||||
else:
|
||||
print(" ➡️ Maintainability unchanged")
|
||||
|
||||
if cyclomatic_change < 0:
|
||||
print(" ✅ Complexity DECREASED")
|
||||
elif cyclomatic_change > 0:
|
||||
print(" ⚠️ Complexity INCREASED")
|
||||
else:
|
||||
print(" ➡️ Complexity unchanged")
|
||||
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 3:
|
||||
print("Usage: python compare-complexity.py <before_file> <after_file>")
|
||||
sys.exit(1)
|
||||
|
||||
compare_files(sys.argv[1], sys.argv[2])
|
||||
112
uk/03-skills/code-review/templates/finding-template.md
Normal file
112
uk/03-skills/code-review/templates/finding-template.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# Шаблон знахідки код-рев'ю
|
||||
|
||||
Використовуйте цей шаблон для документування кожної знайденої проблеми під час код-рев'ю.
|
||||
|
||||
---
|
||||
|
||||
## Проблема: [НАЗВА]
|
||||
|
||||
### Серйозність
|
||||
- [ ] Критична (блокує деплой)
|
||||
- [ ] Висока (потрібно виправити перед мерджем)
|
||||
- [ ] Середня (потрібно виправити незабаром)
|
||||
- [ ] Низька (бажано виправити)
|
||||
|
||||
### Категорія
|
||||
- [ ] Безпека
|
||||
- [ ] Продуктивність
|
||||
- [ ] Якість коду
|
||||
- [ ] Підтримуваність
|
||||
- [ ] Тестування
|
||||
- [ ] Патерн проєктування
|
||||
- [ ] Документація
|
||||
|
||||
### Розташування
|
||||
**Файл:** `src/components/UserCard.tsx`
|
||||
|
||||
**Рядки:** 45-52
|
||||
|
||||
**Функція/Метод:** `renderUserDetails()`
|
||||
|
||||
### Опис проблеми
|
||||
|
||||
**Що:** Опишіть суть проблеми.
|
||||
|
||||
**Чому це важливо:** Поясніть вплив та чому це потрібно виправити.
|
||||
|
||||
**Поточна поведінка:** Покажіть проблемний код або поведінку.
|
||||
|
||||
**Очікувана поведінка:** Опишіть, що повинно відбуватися замість цього.
|
||||
|
||||
### Приклад коду
|
||||
|
||||
#### Поточний (проблемний)
|
||||
|
||||
```typescript
|
||||
// Shows the N+1 query problem
|
||||
const users = fetchUsers();
|
||||
users.forEach(user => {
|
||||
const posts = fetchUserPosts(user.id); // Query per user!
|
||||
renderUserPosts(posts);
|
||||
});
|
||||
```
|
||||
|
||||
#### Запропоноване виправлення
|
||||
|
||||
```typescript
|
||||
// Optimized with JOIN query
|
||||
const usersWithPosts = fetchUsersWithPosts();
|
||||
usersWithPosts.forEach(({ user, posts }) => {
|
||||
renderUserPosts(posts);
|
||||
});
|
||||
```
|
||||
|
||||
### Аналіз впливу
|
||||
|
||||
| Аспект | Вплив | Серйозність |
|
||||
|--------|-------|-------------|
|
||||
| Продуктивність | 100+ запитів для 20 користувачів | Висока |
|
||||
| Досвід користувача | Повільне завантаження сторінки | Висока |
|
||||
| Масштабованість | Ламається при масштабуванні | Критична |
|
||||
| Підтримуваність | Складно дебажити | Середня |
|
||||
|
||||
### Пов'язані проблеми
|
||||
|
||||
- Аналогічна проблема в `AdminUserList.tsx` рядок 120
|
||||
- Пов'язаний PR: #456
|
||||
- Пов'язана issue: #789
|
||||
|
||||
### Додаткові ресурси
|
||||
|
||||
- [N+1 Query Problem](https://en.wikipedia.org/wiki/N%2B1_problem)
|
||||
- [Database Join Documentation](https://docs.example.com/joins)
|
||||
|
||||
### Нотатки рецензента
|
||||
|
||||
- Це поширений патерн у цій кодовій базі
|
||||
- Варто додати це до гайду зі стилю коду
|
||||
- Можливо, варто створити допоміжну функцію
|
||||
|
||||
### Відповідь автора (для зворотного зв'язку)
|
||||
|
||||
*Заповнюється автором коду:*
|
||||
|
||||
- [ ] Виправлення реалізовано в коміті: `abc123`
|
||||
- [ ] Статус виправлення: Завершено / В процесі / Потребує обговорення
|
||||
- [ ] Питання або зауваження: (опишіть)
|
||||
|
||||
---
|
||||
|
||||
## Статистика знахідок (для рецензента)
|
||||
|
||||
При рев'ю кількох знахідок, відстежуйте:
|
||||
|
||||
- **Всього знайдено проблем:** X
|
||||
- **Критичних:** X
|
||||
- **Високих:** X
|
||||
- **Середніх:** X
|
||||
- **Низьких:** X
|
||||
|
||||
**Рекомендація:** ✅ Затвердити / ⚠️ Запросити зміни / 🔄 Потребує обговорення
|
||||
|
||||
**Загальна якість коду:** 1-5 зірок
|
||||
47
uk/03-skills/code-review/templates/review-checklist.md
Normal file
47
uk/03-skills/code-review/templates/review-checklist.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# Чеклист код-рев'ю
|
||||
|
||||
## Чеклист безпеки
|
||||
- [ ] Немає захардкоджених облікових даних або секретів
|
||||
- [ ] Валідація введення для всіх даних від користувача
|
||||
- [ ] Захист від SQL-ін'єкцій (параметризовані запити)
|
||||
- [ ] CSRF-захист для операцій зі зміною стану
|
||||
- [ ] Захист від XSS з правильним екрануванням
|
||||
- [ ] Перевірка автентифікації на захищених ендпоінтах
|
||||
- [ ] Перевірка авторизації для ресурсів
|
||||
- [ ] Безпечне хешування паролів (bcrypt, argon2)
|
||||
- [ ] Немає чутливих даних у логах
|
||||
- [ ] HTTPS обов'язковий
|
||||
|
||||
## Чеклист продуктивності
|
||||
- [ ] Немає N+1 запитів
|
||||
- [ ] Правильне використання індексів
|
||||
- [ ] Кешування реалізовано де доцільно
|
||||
- [ ] Немає блокуючих операцій в основному потоці
|
||||
- [ ] Async/await використовується коректно
|
||||
- [ ] Великі набори даних пагіновані
|
||||
- [ ] Пул з'єднань до бази даних
|
||||
- [ ] Регулярні вирази оптимізовані
|
||||
- [ ] Немає зайвого створення об'єктів
|
||||
- [ ] Витоки пам'яті запобігаються
|
||||
|
||||
## Чеклист якості
|
||||
- [ ] Функції < 50 рядків
|
||||
- [ ] Зрозумілі назви змінних
|
||||
- [ ] Немає дублювання коду
|
||||
- [ ] Правильна обробка помилок
|
||||
- [ ] Коментарі пояснюють ЧОМУ, а не ЩО
|
||||
- [ ] Немає console.log у продакшені
|
||||
- [ ] Перевірка типів (TypeScript/JSDoc)
|
||||
- [ ] Принципи SOLID дотримані
|
||||
- [ ] Патерни проєктування застосовані коректно
|
||||
- [ ] Самодокументований код
|
||||
|
||||
## Чеклист тестування
|
||||
- [ ] Юніт-тести написані
|
||||
- [ ] Граничні випадки покриті
|
||||
- [ ] Сценарії помилок протестовані
|
||||
- [ ] Інтеграційні тести є
|
||||
- [ ] Покриття > 80%
|
||||
- [ ] Немає нестабільних тестів
|
||||
- [ ] Зовнішні залежності замоковані
|
||||
- [ ] Зрозумілі назви тестів
|
||||
55
uk/03-skills/doc-generator/generate-docs.py
Normal file
55
uk/03-skills/doc-generator/generate-docs.py
Normal file
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env python3
|
||||
import ast
|
||||
|
||||
|
||||
class APIDocExtractor(ast.NodeVisitor):
|
||||
"""Extract API documentation from Python source code."""
|
||||
|
||||
def __init__(self):
|
||||
self.endpoints = []
|
||||
|
||||
def visit_FunctionDef(self, node):
|
||||
"""Extract function documentation."""
|
||||
if node.name.startswith("get_") or node.name.startswith("post_"):
|
||||
doc = ast.get_docstring(node)
|
||||
endpoint = {
|
||||
"name": node.name,
|
||||
"docstring": doc,
|
||||
"params": [arg.arg for arg in node.args.args],
|
||||
"returns": self._extract_return_type(node),
|
||||
}
|
||||
self.endpoints.append(endpoint)
|
||||
self.generic_visit(node)
|
||||
|
||||
def _extract_return_type(self, node):
|
||||
"""Extract return type from function annotation."""
|
||||
if node.returns:
|
||||
return ast.unparse(node.returns)
|
||||
return "Any"
|
||||
|
||||
|
||||
def generate_markdown_docs(endpoints: list[dict]) -> str:
|
||||
"""Generate markdown documentation from endpoints."""
|
||||
docs = "# API Documentation\n\n"
|
||||
|
||||
for endpoint in endpoints:
|
||||
docs += f"## {endpoint['name']}\n\n"
|
||||
docs += f"{endpoint['docstring']}\n\n"
|
||||
docs += f"**Parameters**: {', '.join(endpoint['params'])}\n\n"
|
||||
docs += f"**Returns**: {endpoint['returns']}\n\n"
|
||||
docs += "---\n\n"
|
||||
|
||||
return docs
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
with open(sys.argv[1]) as f:
|
||||
tree = ast.parse(f.read())
|
||||
|
||||
extractor = APIDocExtractor()
|
||||
extractor.visit(tree)
|
||||
|
||||
markdown = generate_markdown_docs(extractor.endpoints)
|
||||
print(markdown)
|
||||
545
uk/03-skills/refactor/scripts/analyze-complexity.py
Normal file
545
uk/03-skills/refactor/scripts/analyze-complexity.py
Normal file
@@ -0,0 +1,545 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Code Complexity Analyzer
|
||||
|
||||
Analyzes code complexity metrics for Python, JavaScript, and TypeScript files.
|
||||
Helps measure the impact of refactoring by comparing before/after metrics.
|
||||
|
||||
Usage:
|
||||
python analyze-complexity.py <file>
|
||||
python analyze-complexity.py <before_file> <after_file> # Compare mode
|
||||
python analyze-complexity.py --dir <directory> # Analyze directory
|
||||
|
||||
Metrics:
|
||||
- Cyclomatic Complexity: Decision points in code
|
||||
- Cognitive Complexity: How hard is it to understand
|
||||
- Maintainability Index: Overall maintainability score (0-100)
|
||||
- Lines of Code: Total lines
|
||||
- Function Count: Number of functions/methods
|
||||
- Average Function Length: Lines per function
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class FunctionMetrics:
|
||||
"""Metrics for a single function."""
|
||||
name: str
|
||||
start_line: int
|
||||
end_line: int
|
||||
lines: int
|
||||
cyclomatic_complexity: int
|
||||
cognitive_complexity: int
|
||||
parameter_count: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class FileMetrics:
|
||||
"""Metrics for a file."""
|
||||
filename: str
|
||||
lines_of_code: int
|
||||
blank_lines: int
|
||||
comment_lines: int
|
||||
function_count: int
|
||||
class_count: int
|
||||
cyclomatic_complexity: int
|
||||
cognitive_complexity: int
|
||||
maintainability_index: float
|
||||
avg_function_length: float
|
||||
max_function_length: int
|
||||
functions: List[FunctionMetrics]
|
||||
|
||||
|
||||
class ComplexityAnalyzer:
|
||||
"""Analyze code complexity for multiple languages."""
|
||||
|
||||
# Patterns for different languages
|
||||
PATTERNS = {
|
||||
'python': {
|
||||
'function': r'^\s*def\s+(\w+)\s*\(',
|
||||
'class': r'^\s*class\s+(\w+)',
|
||||
'decision': [r'\bif\b', r'\belif\b', r'\bfor\b', r'\bwhile\b',
|
||||
r'\bexcept\b', r'\band\b(?!$)', r'\bor\b(?!$)',
|
||||
r'\bcase\b', r'\btry\b'],
|
||||
'comment': r'^\s*#',
|
||||
'multiline_comment_start': r'^\s*["\'][\"\'][\"\']',
|
||||
'multiline_comment_end': r'["\'][\"\'][\"\']',
|
||||
},
|
||||
'javascript': {
|
||||
'function': r'(?:function\s+(\w+)|(\w+)\s*[=:]\s*(?:async\s+)?(?:function|\([^)]*\)\s*=>))',
|
||||
'class': r'class\s+(\w+)',
|
||||
'decision': [r'\bif\b', r'\belse\s+if\b', r'\bfor\b', r'\bwhile\b',
|
||||
r'\bcatch\b', r'\b\?\b', r'\b&&\b', r'\b\|\|\b',
|
||||
r'\bcase\b', r'\btry\b'],
|
||||
'comment': r'^\s*//',
|
||||
'multiline_comment_start': r'/\*',
|
||||
'multiline_comment_end': r'\*/',
|
||||
},
|
||||
'typescript': {
|
||||
'function': r'(?:function\s+(\w+)|(\w+)\s*[=:]\s*(?:async\s+)?(?:function|\([^)]*\)\s*=>))',
|
||||
'class': r'class\s+(\w+)',
|
||||
'decision': [r'\bif\b', r'\belse\s+if\b', r'\bfor\b', r'\bwhile\b',
|
||||
r'\bcatch\b', r'\b\?\b', r'\b&&\b', r'\b\|\|\b',
|
||||
r'\bcase\b', r'\btry\b'],
|
||||
'comment': r'^\s*//',
|
||||
'multiline_comment_start': r'/\*',
|
||||
'multiline_comment_end': r'\*/',
|
||||
}
|
||||
}
|
||||
|
||||
def __init__(self, filepath: str):
|
||||
self.filepath = filepath
|
||||
self.filename = os.path.basename(filepath)
|
||||
self.language = self._detect_language()
|
||||
self.patterns = self.PATTERNS.get(self.language, self.PATTERNS['python'])
|
||||
|
||||
with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
self.code = f.read()
|
||||
self.lines = self.code.split('\n')
|
||||
|
||||
def _detect_language(self) -> str:
|
||||
"""Detect programming language from file extension."""
|
||||
ext = os.path.splitext(self.filepath)[1].lower()
|
||||
ext_map = {
|
||||
'.py': 'python',
|
||||
'.js': 'javascript',
|
||||
'.jsx': 'javascript',
|
||||
'.ts': 'typescript',
|
||||
'.tsx': 'typescript',
|
||||
}
|
||||
return ext_map.get(ext, 'python')
|
||||
|
||||
def calculate_cyclomatic_complexity(self, code: Optional[str] = None) -> int:
|
||||
"""
|
||||
Calculate cyclomatic complexity using McCabe's method.
|
||||
CC = E - N + 2P where E=edges, N=nodes, P=connected components
|
||||
Simplified: Count decision points + 1
|
||||
"""
|
||||
if code is None:
|
||||
code = self.code
|
||||
|
||||
complexity = 1 # Base complexity
|
||||
|
||||
for pattern in self.patterns['decision']:
|
||||
matches = re.findall(pattern, code)
|
||||
complexity += len(matches)
|
||||
|
||||
return complexity
|
||||
|
||||
def calculate_cognitive_complexity(self, code: Optional[str] = None) -> int:
|
||||
"""
|
||||
Calculate cognitive complexity.
|
||||
Measures how hard it is to understand the code.
|
||||
Accounts for nesting depth and control flow breaks.
|
||||
"""
|
||||
if code is None:
|
||||
code = self.code
|
||||
|
||||
lines = code.split('\n')
|
||||
cognitive = 0
|
||||
nesting_depth = 0
|
||||
in_function = False
|
||||
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
|
||||
# Track function boundaries
|
||||
if re.search(self.patterns['function'], line):
|
||||
in_function = True
|
||||
nesting_depth = 0
|
||||
|
||||
# Increment for control flow structures
|
||||
if re.search(r'\b(if|for|while|switch)\b', stripped):
|
||||
nesting_depth += 1
|
||||
cognitive += nesting_depth # Nested structures cost more
|
||||
|
||||
elif re.search(r'\b(elif|else if|else|catch|finally)\b', stripped):
|
||||
cognitive += nesting_depth # Same level as parent
|
||||
|
||||
# Track nesting through braces/indentation
|
||||
if self.language in ['javascript', 'typescript']:
|
||||
nesting_depth += stripped.count('{') - stripped.count('}')
|
||||
nesting_depth = max(0, nesting_depth)
|
||||
|
||||
# Bonus for breaks in linear flow
|
||||
if re.search(r'\b(break|continue|return|throw)\b', stripped):
|
||||
if nesting_depth > 1:
|
||||
cognitive += 1
|
||||
|
||||
# Bonus for recursion
|
||||
# (simplified: just look for function calling itself)
|
||||
|
||||
return cognitive
|
||||
|
||||
def calculate_maintainability_index(self) -> float:
|
||||
"""
|
||||
Calculate Maintainability Index (0-100).
|
||||
Based on Halstead Volume, Cyclomatic Complexity, and Lines of Code.
|
||||
|
||||
MI = max(0, (171 - 5.2*ln(V) - 0.23*CC - 16.2*ln(LOC)) * 100/171)
|
||||
|
||||
Interpretation:
|
||||
- 85-100: Highly maintainable
|
||||
- 65-84: Moderately maintainable
|
||||
- 50-64: Difficult to maintain
|
||||
- 0-49: Very difficult to maintain
|
||||
"""
|
||||
import math
|
||||
|
||||
loc = len([l for l in self.lines if l.strip()])
|
||||
cc = self.calculate_cyclomatic_complexity()
|
||||
|
||||
# Simplified Halstead Volume approximation
|
||||
# Count unique operators and operands
|
||||
operators = len(re.findall(r'[+\-*/%=<>!&|^~]', self.code))
|
||||
operands = len(re.findall(r'\b\w+\b', self.code))
|
||||
volume = (operators + operands) * math.log2(max(1, operators + operands))
|
||||
|
||||
# Calculate MI
|
||||
mi = 171 - 5.2 * math.log(max(1, volume)) - 0.23 * cc - 16.2 * math.log(max(1, loc))
|
||||
mi = max(0, min(100, mi * 100 / 171))
|
||||
|
||||
return round(mi, 2)
|
||||
|
||||
def count_lines(self) -> Dict[str, int]:
|
||||
"""Count different types of lines."""
|
||||
total = len(self.lines)
|
||||
blank = 0
|
||||
comment = 0
|
||||
in_multiline_comment = False
|
||||
|
||||
for line in self.lines:
|
||||
stripped = line.strip()
|
||||
|
||||
# Check for multiline comments
|
||||
if re.search(self.patterns['multiline_comment_start'], stripped):
|
||||
in_multiline_comment = True
|
||||
if re.search(self.patterns['multiline_comment_end'], stripped):
|
||||
in_multiline_comment = False
|
||||
comment += 1
|
||||
continue
|
||||
|
||||
if in_multiline_comment:
|
||||
comment += 1
|
||||
elif not stripped:
|
||||
blank += 1
|
||||
elif re.match(self.patterns['comment'], stripped):
|
||||
comment += 1
|
||||
|
||||
return {
|
||||
'total': total,
|
||||
'blank': blank,
|
||||
'comment': comment,
|
||||
'code': total - blank - comment
|
||||
}
|
||||
|
||||
def find_functions(self) -> List[FunctionMetrics]:
|
||||
"""Find all functions and calculate their individual metrics."""
|
||||
functions = []
|
||||
current_function = None
|
||||
function_start = 0
|
||||
brace_depth = 0
|
||||
|
||||
for i, line in enumerate(self.lines):
|
||||
# Check for function definition
|
||||
match = re.search(self.patterns['function'], line)
|
||||
if match:
|
||||
# Save previous function if exists
|
||||
if current_function:
|
||||
func_code = '\n'.join(self.lines[function_start:i])
|
||||
functions.append(self._create_function_metrics(
|
||||
current_function, function_start, i - 1, func_code
|
||||
))
|
||||
|
||||
current_function = match.group(1) or match.group(2) if match.lastindex and match.lastindex > 1 else match.group(1)
|
||||
function_start = i
|
||||
brace_depth = 0
|
||||
|
||||
# Track braces for JS/TS
|
||||
if self.language in ['javascript', 'typescript']:
|
||||
brace_depth += line.count('{') - line.count('}')
|
||||
|
||||
# Don't forget the last function
|
||||
if current_function:
|
||||
func_code = '\n'.join(self.lines[function_start:])
|
||||
functions.append(self._create_function_metrics(
|
||||
current_function, function_start, len(self.lines) - 1, func_code
|
||||
))
|
||||
|
||||
return functions
|
||||
|
||||
def _create_function_metrics(self, name: str, start: int, end: int, code: str) -> FunctionMetrics:
|
||||
"""Create metrics for a single function."""
|
||||
lines = end - start + 1
|
||||
|
||||
# Count parameters (simplified)
|
||||
param_match = re.search(r'\(([^)]*)\)', code.split('\n')[0])
|
||||
param_count = 0
|
||||
if param_match and param_match.group(1).strip():
|
||||
param_count = len([p for p in param_match.group(1).split(',') if p.strip()])
|
||||
|
||||
return FunctionMetrics(
|
||||
name=name,
|
||||
start_line=start + 1,
|
||||
end_line=end + 1,
|
||||
lines=lines,
|
||||
cyclomatic_complexity=self.calculate_cyclomatic_complexity(code),
|
||||
cognitive_complexity=self.calculate_cognitive_complexity(code),
|
||||
parameter_count=param_count
|
||||
)
|
||||
|
||||
def analyze(self) -> FileMetrics:
|
||||
"""Perform complete analysis of the file."""
|
||||
line_counts = self.count_lines()
|
||||
functions = self.find_functions()
|
||||
|
||||
# Count classes
|
||||
class_count = len(re.findall(self.patterns['class'], self.code))
|
||||
|
||||
# Calculate averages
|
||||
func_lengths = [f.lines for f in functions] if functions else [0]
|
||||
avg_func_length = sum(func_lengths) / len(func_lengths)
|
||||
max_func_length = max(func_lengths)
|
||||
|
||||
return FileMetrics(
|
||||
filename=self.filename,
|
||||
lines_of_code=line_counts['code'],
|
||||
blank_lines=line_counts['blank'],
|
||||
comment_lines=line_counts['comment'],
|
||||
function_count=len(functions),
|
||||
class_count=class_count,
|
||||
cyclomatic_complexity=self.calculate_cyclomatic_complexity(),
|
||||
cognitive_complexity=self.calculate_cognitive_complexity(),
|
||||
maintainability_index=self.calculate_maintainability_index(),
|
||||
avg_function_length=round(avg_func_length, 1),
|
||||
max_function_length=max_func_length,
|
||||
functions=functions
|
||||
)
|
||||
|
||||
|
||||
def print_metrics(metrics: FileMetrics, verbose: bool = False) -> None:
|
||||
"""Print metrics in a readable format."""
|
||||
print("=" * 60)
|
||||
print(f"CODE COMPLEXITY ANALYSIS: {metrics.filename}")
|
||||
print("=" * 60)
|
||||
|
||||
print("\n📊 OVERVIEW")
|
||||
print("-" * 40)
|
||||
print(f" Lines of Code: {metrics.lines_of_code}")
|
||||
print(f" Blank Lines: {metrics.blank_lines}")
|
||||
print(f" Comment Lines: {metrics.comment_lines}")
|
||||
print(f" Functions/Methods: {metrics.function_count}")
|
||||
print(f" Classes: {metrics.class_count}")
|
||||
|
||||
print("\n📈 COMPLEXITY METRICS")
|
||||
print("-" * 40)
|
||||
print(f" Cyclomatic Complexity: {metrics.cyclomatic_complexity}")
|
||||
print(f" Cognitive Complexity: {metrics.cognitive_complexity}")
|
||||
print(f" Maintainability Index: {metrics.maintainability_index}")
|
||||
|
||||
# Interpret maintainability
|
||||
mi = metrics.maintainability_index
|
||||
if mi >= 85:
|
||||
mi_label = "Highly maintainable ✅"
|
||||
elif mi >= 65:
|
||||
mi_label = "Moderately maintainable 🔶"
|
||||
elif mi >= 50:
|
||||
mi_label = "Difficult to maintain ⚠️"
|
||||
else:
|
||||
mi_label = "Very difficult to maintain ❌"
|
||||
print(f" → {mi_label}")
|
||||
|
||||
print("\n📐 FUNCTION METRICS")
|
||||
print("-" * 40)
|
||||
print(f" Avg Function Length: {metrics.avg_function_length} lines")
|
||||
print(f" Max Function Length: {metrics.max_function_length} lines")
|
||||
|
||||
if verbose and metrics.functions:
|
||||
print("\n📋 FUNCTION DETAILS")
|
||||
print("-" * 40)
|
||||
for f in sorted(metrics.functions, key=lambda x: x.cyclomatic_complexity, reverse=True):
|
||||
flag = " ⚠️" if f.cyclomatic_complexity > 10 or f.lines > 50 else ""
|
||||
print(f" {f.name}() [lines {f.start_line}-{f.end_line}]{flag}")
|
||||
print(f" - Lines: {f.lines}, CC: {f.cyclomatic_complexity}, "
|
||||
f"Cognitive: {f.cognitive_complexity}, Params: {f.parameter_count}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
|
||||
|
||||
def print_comparison(before: FileMetrics, after: FileMetrics) -> None:
|
||||
"""Print comparison between two analyses."""
|
||||
print("=" * 70)
|
||||
print("CODE COMPLEXITY COMPARISON")
|
||||
print("=" * 70)
|
||||
|
||||
print(f"\n{'Metric':<30} {'Before':<15} {'After':<15} {'Change':<10}")
|
||||
print("-" * 70)
|
||||
|
||||
def fmt_change(before_val, after_val, lower_is_better=True):
|
||||
diff = after_val - before_val
|
||||
if lower_is_better:
|
||||
symbol = "✅" if diff < 0 else ("⚠️" if diff > 0 else "➖")
|
||||
else:
|
||||
symbol = "✅" if diff > 0 else ("⚠️" if diff < 0 else "➖")
|
||||
return f"{diff:+.1f} {symbol}" if isinstance(diff, float) else f"{diff:+d} {symbol}"
|
||||
|
||||
metrics = [
|
||||
("Lines of Code", before.lines_of_code, after.lines_of_code, True),
|
||||
("Function Count", before.function_count, after.function_count, False),
|
||||
("Class Count", before.class_count, after.class_count, False),
|
||||
("Cyclomatic Complexity", before.cyclomatic_complexity, after.cyclomatic_complexity, True),
|
||||
("Cognitive Complexity", before.cognitive_complexity, after.cognitive_complexity, True),
|
||||
("Maintainability Index", before.maintainability_index, after.maintainability_index, False),
|
||||
("Avg Function Length", before.avg_function_length, after.avg_function_length, True),
|
||||
("Max Function Length", before.max_function_length, after.max_function_length, True),
|
||||
]
|
||||
|
||||
for name, b_val, a_val, lower_better in metrics:
|
||||
change = fmt_change(b_val, a_val, lower_better)
|
||||
print(f"{name:<30} {b_val:<15} {a_val:<15} {change:<10}")
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
|
||||
# Overall assessment
|
||||
print("\n🎯 ASSESSMENT")
|
||||
print("-" * 40)
|
||||
|
||||
improvements = 0
|
||||
regressions = 0
|
||||
|
||||
if after.maintainability_index > before.maintainability_index:
|
||||
print(" ✅ Maintainability improved")
|
||||
improvements += 1
|
||||
elif after.maintainability_index < before.maintainability_index:
|
||||
print(" ⚠️ Maintainability decreased")
|
||||
regressions += 1
|
||||
|
||||
if after.cyclomatic_complexity < before.cyclomatic_complexity:
|
||||
print(" ✅ Complexity reduced")
|
||||
improvements += 1
|
||||
elif after.cyclomatic_complexity > before.cyclomatic_complexity:
|
||||
print(" ⚠️ Complexity increased")
|
||||
regressions += 1
|
||||
|
||||
if after.avg_function_length < before.avg_function_length:
|
||||
print(" ✅ Functions are smaller on average")
|
||||
improvements += 1
|
||||
elif after.avg_function_length > before.avg_function_length:
|
||||
print(" ⚠️ Functions grew larger on average")
|
||||
regressions += 1
|
||||
|
||||
print(f"\n Summary: {improvements} improvements, {regressions} regressions")
|
||||
print("=" * 70)
|
||||
|
||||
|
||||
def analyze_directory(directory: str, verbose: bool = False) -> None:
|
||||
"""Analyze all supported files in a directory."""
|
||||
supported_extensions = ['.py', '.js', '.jsx', '.ts', '.tsx']
|
||||
files = []
|
||||
|
||||
for root, _, filenames in os.walk(directory):
|
||||
for filename in filenames:
|
||||
if any(filename.endswith(ext) for ext in supported_extensions):
|
||||
files.append(os.path.join(root, filename))
|
||||
|
||||
if not files:
|
||||
print(f"No supported files found in {directory}")
|
||||
return
|
||||
|
||||
print(f"Analyzing {len(files)} files in {directory}...\n")
|
||||
|
||||
total_loc = 0
|
||||
total_cc = 0
|
||||
total_functions = 0
|
||||
all_metrics = []
|
||||
|
||||
for filepath in sorted(files):
|
||||
try:
|
||||
analyzer = ComplexityAnalyzer(filepath)
|
||||
metrics = analyzer.analyze()
|
||||
all_metrics.append(metrics)
|
||||
|
||||
total_loc += metrics.lines_of_code
|
||||
total_cc += metrics.cyclomatic_complexity
|
||||
total_functions += metrics.function_count
|
||||
|
||||
if verbose:
|
||||
print_metrics(metrics, verbose=True)
|
||||
else:
|
||||
flag = " ⚠️" if metrics.maintainability_index < 65 else ""
|
||||
print(f" {metrics.filename}: LOC={metrics.lines_of_code}, "
|
||||
f"CC={metrics.cyclomatic_complexity}, MI={metrics.maintainability_index}{flag}")
|
||||
except Exception as e:
|
||||
print(f" Error analyzing {filepath}: {e}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("SUMMARY")
|
||||
print("=" * 60)
|
||||
print(f" Files analyzed: {len(all_metrics)}")
|
||||
print(f" Total lines of code: {total_loc}")
|
||||
print(f" Total complexity: {total_cc}")
|
||||
print(f" Total functions: {total_functions}")
|
||||
|
||||
if all_metrics:
|
||||
avg_mi = sum(m.maintainability_index for m in all_metrics) / len(all_metrics)
|
||||
print(f" Avg maintainability: {avg_mi:.1f}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Analyze code complexity metrics',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
%(prog)s myfile.py Analyze single file
|
||||
%(prog)s before.py after.py Compare two versions
|
||||
%(prog)s --dir src/ Analyze directory
|
||||
%(prog)s -v myfile.py Verbose output with function details
|
||||
"""
|
||||
)
|
||||
parser.add_argument('files', nargs='*', help='File(s) to analyze')
|
||||
parser.add_argument('--dir', '-d', help='Directory to analyze')
|
||||
parser.add_argument('--verbose', '-v', action='store_true', help='Show detailed function metrics')
|
||||
parser.add_argument('--json', '-j', action='store_true', help='Output as JSON')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.dir:
|
||||
analyze_directory(args.dir, args.verbose)
|
||||
elif len(args.files) == 1:
|
||||
analyzer = ComplexityAnalyzer(args.files[0])
|
||||
metrics = analyzer.analyze()
|
||||
|
||||
if args.json:
|
||||
import json
|
||||
print(json.dumps({
|
||||
'filename': metrics.filename,
|
||||
'lines_of_code': metrics.lines_of_code,
|
||||
'cyclomatic_complexity': metrics.cyclomatic_complexity,
|
||||
'cognitive_complexity': metrics.cognitive_complexity,
|
||||
'maintainability_index': metrics.maintainability_index,
|
||||
'function_count': metrics.function_count,
|
||||
'avg_function_length': metrics.avg_function_length,
|
||||
}, indent=2))
|
||||
else:
|
||||
print_metrics(metrics, args.verbose)
|
||||
elif len(args.files) == 2:
|
||||
before_analyzer = ComplexityAnalyzer(args.files[0])
|
||||
after_analyzer = ComplexityAnalyzer(args.files[1])
|
||||
before_metrics = before_analyzer.analyze()
|
||||
after_metrics = after_analyzer.analyze()
|
||||
print_comparison(before_metrics, after_metrics)
|
||||
else:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
711
uk/03-skills/refactor/scripts/detect-smells.py
Normal file
711
uk/03-skills/refactor/scripts/detect-smells.py
Normal file
@@ -0,0 +1,711 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Code Smell Detector
|
||||
|
||||
Detects common code smells in Python, JavaScript, and TypeScript files.
|
||||
Based on Martin Fowler's catalog of code smells.
|
||||
|
||||
Usage:
|
||||
python detect-smells.py <file>
|
||||
python detect-smells.py --dir <directory>
|
||||
python detect-smells.py -v <file> # Verbose with code snippets
|
||||
|
||||
Detects:
|
||||
- Long Method (>30 lines)
|
||||
- Long Parameter List (>4 params)
|
||||
- Duplicate Code (similar code blocks)
|
||||
- Large Class (>300 lines, >10 methods)
|
||||
- Dead Code (unused variables/functions)
|
||||
- Complex Conditionals (deep nesting, long chains)
|
||||
- Magic Numbers/Strings
|
||||
- Feature Envy (methods using other class data heavily)
|
||||
- Comments explaining what (not why)
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Dict, List, Optional, Set, Tuple
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
class SmellSeverity(Enum):
|
||||
"""Severity levels for code smells."""
|
||||
LOW = "Low"
|
||||
MEDIUM = "Medium"
|
||||
HIGH = "High"
|
||||
CRITICAL = "Critical"
|
||||
|
||||
|
||||
class SmellType(Enum):
|
||||
"""Types of code smells."""
|
||||
LONG_METHOD = "Long Method"
|
||||
LONG_PARAMETER_LIST = "Long Parameter List"
|
||||
DUPLICATE_CODE = "Duplicate Code"
|
||||
LARGE_CLASS = "Large Class"
|
||||
DEAD_CODE = "Dead Code"
|
||||
COMPLEX_CONDITIONAL = "Complex Conditional"
|
||||
MAGIC_NUMBER = "Magic Number/String"
|
||||
FEATURE_ENVY = "Feature Envy"
|
||||
EXCESSIVE_COMMENTS = "Excessive Comments"
|
||||
DEEPLY_NESTED = "Deeply Nested Code"
|
||||
PRIMITIVE_OBSESSION = "Primitive Obsession"
|
||||
DATA_CLUMPS = "Data Clumps"
|
||||
SWITCH_STATEMENT = "Switch Statement"
|
||||
MESSAGE_CHAIN = "Message Chain"
|
||||
|
||||
|
||||
@dataclass
|
||||
class CodeSmell:
|
||||
"""Represents a detected code smell."""
|
||||
smell_type: SmellType
|
||||
severity: SmellSeverity
|
||||
location: str
|
||||
line_start: int
|
||||
line_end: int
|
||||
description: str
|
||||
suggestion: str
|
||||
code_snippet: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class SmellReport:
|
||||
"""Report of all smells found in a file."""
|
||||
filename: str
|
||||
smells: List[CodeSmell] = field(default_factory=list)
|
||||
|
||||
@property
|
||||
def critical_count(self) -> int:
|
||||
return sum(1 for s in self.smells if s.severity == SmellSeverity.CRITICAL)
|
||||
|
||||
@property
|
||||
def high_count(self) -> int:
|
||||
return sum(1 for s in self.smells if s.severity == SmellSeverity.HIGH)
|
||||
|
||||
@property
|
||||
def medium_count(self) -> int:
|
||||
return sum(1 for s in self.smells if s.severity == SmellSeverity.MEDIUM)
|
||||
|
||||
@property
|
||||
def low_count(self) -> int:
|
||||
return sum(1 for s in self.smells if s.severity == SmellSeverity.LOW)
|
||||
|
||||
|
||||
class SmellDetector:
|
||||
"""Detect code smells in source files."""
|
||||
|
||||
# Thresholds (configurable)
|
||||
THRESHOLDS = {
|
||||
'long_method_lines': 30,
|
||||
'very_long_method_lines': 50,
|
||||
'max_parameters': 4,
|
||||
'large_class_lines': 300,
|
||||
'large_class_methods': 10,
|
||||
'max_nesting_depth': 4,
|
||||
'long_chain_length': 3,
|
||||
'duplicate_min_lines': 5,
|
||||
}
|
||||
|
||||
def __init__(self, filepath: str):
|
||||
self.filepath = filepath
|
||||
self.filename = os.path.basename(filepath)
|
||||
self.language = self._detect_language()
|
||||
|
||||
with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
self.code = f.read()
|
||||
self.lines = self.code.split('\n')
|
||||
self.smells: List[CodeSmell] = []
|
||||
|
||||
def _detect_language(self) -> str:
|
||||
"""Detect programming language from file extension."""
|
||||
ext = os.path.splitext(self.filepath)[1].lower()
|
||||
ext_map = {
|
||||
'.py': 'python',
|
||||
'.js': 'javascript',
|
||||
'.jsx': 'javascript',
|
||||
'.ts': 'typescript',
|
||||
'.tsx': 'typescript',
|
||||
}
|
||||
return ext_map.get(ext, 'python')
|
||||
|
||||
def detect_all(self) -> SmellReport:
|
||||
"""Run all smell detectors."""
|
||||
self._detect_long_methods()
|
||||
self._detect_long_parameter_lists()
|
||||
self._detect_large_class()
|
||||
self._detect_complex_conditionals()
|
||||
self._detect_magic_numbers()
|
||||
self._detect_excessive_comments()
|
||||
self._detect_deeply_nested()
|
||||
self._detect_switch_statements()
|
||||
self._detect_message_chains()
|
||||
self._detect_duplicate_code()
|
||||
self._detect_dead_code()
|
||||
|
||||
return SmellReport(filename=self.filename, smells=self.smells)
|
||||
|
||||
def _get_snippet(self, start: int, end: int, context: int = 2) -> str:
|
||||
"""Get code snippet with context."""
|
||||
actual_start = max(0, start - context)
|
||||
actual_end = min(len(self.lines), end + context)
|
||||
snippet_lines = []
|
||||
for i in range(actual_start, actual_end):
|
||||
prefix = "→ " if start <= i < end else " "
|
||||
snippet_lines.append(f"{i+1:4d} {prefix}{self.lines[i]}")
|
||||
return '\n'.join(snippet_lines)
|
||||
|
||||
def _detect_long_methods(self) -> None:
|
||||
"""Detect methods that are too long."""
|
||||
if self.language == 'python':
|
||||
pattern = r'^\s*def\s+(\w+)\s*\([^)]*\):'
|
||||
else:
|
||||
pattern = r'(?:function\s+(\w+)|(\w+)\s*[=:]\s*(?:async\s+)?(?:function|\([^)]*\)\s*=>))'
|
||||
|
||||
current_method = None
|
||||
method_start = 0
|
||||
brace_depth = 0
|
||||
indent_level = 0
|
||||
|
||||
for i, line in enumerate(self.lines):
|
||||
match = re.search(pattern, line)
|
||||
if match:
|
||||
# Check previous method if exists
|
||||
if current_method:
|
||||
method_lines = i - method_start
|
||||
self._check_method_length(current_method, method_start, i - 1, method_lines)
|
||||
|
||||
current_method = match.group(1) or (match.group(2) if match.lastindex and match.lastindex > 1 else None)
|
||||
method_start = i
|
||||
indent_level = len(line) - len(line.lstrip())
|
||||
|
||||
# Track end of Python functions by indentation
|
||||
if self.language == 'python' and current_method:
|
||||
if line.strip() and not line.strip().startswith('#'):
|
||||
current_indent = len(line) - len(line.lstrip())
|
||||
if current_indent <= indent_level and i > method_start:
|
||||
method_lines = i - method_start
|
||||
self._check_method_length(current_method, method_start, i - 1, method_lines)
|
||||
current_method = None
|
||||
|
||||
# Check last method
|
||||
if current_method:
|
||||
method_lines = len(self.lines) - method_start
|
||||
self._check_method_length(current_method, method_start, len(self.lines) - 1, method_lines)
|
||||
|
||||
def _check_method_length(self, name: str, start: int, end: int, lines: int) -> None:
|
||||
"""Check if method is too long and add smell if so."""
|
||||
if lines > self.THRESHOLDS['very_long_method_lines']:
|
||||
severity = SmellSeverity.HIGH
|
||||
desc = f"Method '{name}' is {lines} lines (threshold: {self.THRESHOLDS['long_method_lines']})"
|
||||
elif lines > self.THRESHOLDS['long_method_lines']:
|
||||
severity = SmellSeverity.MEDIUM
|
||||
desc = f"Method '{name}' is {lines} lines (threshold: {self.THRESHOLDS['long_method_lines']})"
|
||||
else:
|
||||
return
|
||||
|
||||
self.smells.append(CodeSmell(
|
||||
smell_type=SmellType.LONG_METHOD,
|
||||
severity=severity,
|
||||
location=f"{self.filename}:{start+1}-{end+1}",
|
||||
line_start=start + 1,
|
||||
line_end=end + 1,
|
||||
description=desc,
|
||||
suggestion="Apply Extract Method to break down into smaller functions",
|
||||
code_snippet=self._get_snippet(start, min(start + 5, end), 0)
|
||||
))
|
||||
|
||||
def _detect_long_parameter_lists(self) -> None:
|
||||
"""Detect functions with too many parameters."""
|
||||
if self.language == 'python':
|
||||
pattern = r'def\s+(\w+)\s*\(([^)]*)\)'
|
||||
else:
|
||||
pattern = r'(?:function\s+(\w+)\s*\(([^)]*)\)|(\w+)\s*[=:]\s*(?:async\s+)?(?:function\s*)?\(([^)]*)\))'
|
||||
|
||||
for i, line in enumerate(self.lines):
|
||||
match = re.search(pattern, line)
|
||||
if match:
|
||||
# Safely extract groups
|
||||
groups = match.groups()
|
||||
func_name = groups[0] or (groups[2] if len(groups) > 2 else None)
|
||||
params_str = groups[1] if len(groups) > 1 else ""
|
||||
if not params_str and len(groups) > 3:
|
||||
params_str = groups[3] or ""
|
||||
|
||||
# Count parameters
|
||||
if params_str.strip():
|
||||
params = [p.strip() for p in params_str.split(',') if p.strip()]
|
||||
# Filter out 'self', 'cls' for Python
|
||||
if self.language == 'python':
|
||||
params = [p for p in params if p not in ('self', 'cls')]
|
||||
param_count = len(params)
|
||||
|
||||
if param_count > self.THRESHOLDS['max_parameters']:
|
||||
severity = SmellSeverity.HIGH if param_count > 6 else SmellSeverity.MEDIUM
|
||||
self.smells.append(CodeSmell(
|
||||
smell_type=SmellType.LONG_PARAMETER_LIST,
|
||||
severity=severity,
|
||||
location=f"{self.filename}:{i+1}",
|
||||
line_start=i + 1,
|
||||
line_end=i + 1,
|
||||
description=f"Function '{func_name}' has {param_count} parameters (max: {self.THRESHOLDS['max_parameters']})",
|
||||
suggestion="Consider Introduce Parameter Object or Preserve Whole Object",
|
||||
code_snippet=self._get_snippet(i, i + 1, 1)
|
||||
))
|
||||
|
||||
def _detect_large_class(self) -> None:
|
||||
"""Detect classes that are too large."""
|
||||
if self.language == 'python':
|
||||
class_pattern = r'^\s*class\s+(\w+)'
|
||||
method_pattern = r'^\s+def\s+\w+'
|
||||
else:
|
||||
class_pattern = r'class\s+(\w+)'
|
||||
method_pattern = r'(?:^\s+\w+\s*\([^)]*\)\s*\{|^\s+(?:async\s+)?\w+\s*=)'
|
||||
|
||||
current_class = None
|
||||
class_start = 0
|
||||
method_count = 0
|
||||
class_indent = 0
|
||||
|
||||
for i, line in enumerate(self.lines):
|
||||
class_match = re.search(class_pattern, line)
|
||||
if class_match:
|
||||
# Check previous class
|
||||
if current_class:
|
||||
self._check_class_size(current_class, class_start, i - 1, method_count)
|
||||
|
||||
current_class = class_match.group(1)
|
||||
class_start = i
|
||||
method_count = 0
|
||||
class_indent = len(line) - len(line.lstrip())
|
||||
|
||||
# Count methods in current class
|
||||
if current_class and re.search(method_pattern, line):
|
||||
method_count += 1
|
||||
|
||||
# Check last class
|
||||
if current_class:
|
||||
self._check_class_size(current_class, class_start, len(self.lines) - 1, method_count)
|
||||
|
||||
def _check_class_size(self, name: str, start: int, end: int, methods: int) -> None:
|
||||
"""Check if class is too large."""
|
||||
lines = end - start + 1
|
||||
|
||||
issues = []
|
||||
severity = SmellSeverity.MEDIUM
|
||||
|
||||
if lines > self.THRESHOLDS['large_class_lines']:
|
||||
issues.append(f"{lines} lines (max: {self.THRESHOLDS['large_class_lines']})")
|
||||
severity = SmellSeverity.HIGH
|
||||
|
||||
if methods > self.THRESHOLDS['large_class_methods']:
|
||||
issues.append(f"{methods} methods (max: {self.THRESHOLDS['large_class_methods']})")
|
||||
if severity != SmellSeverity.HIGH:
|
||||
severity = SmellSeverity.MEDIUM
|
||||
|
||||
if issues:
|
||||
self.smells.append(CodeSmell(
|
||||
smell_type=SmellType.LARGE_CLASS,
|
||||
severity=severity,
|
||||
location=f"{self.filename}:{start+1}-{end+1}",
|
||||
line_start=start + 1,
|
||||
line_end=end + 1,
|
||||
description=f"Class '{name}' is too large: {', '.join(issues)}",
|
||||
suggestion="Apply Extract Class to split responsibilities",
|
||||
code_snippet=self._get_snippet(start, start + 3, 0)
|
||||
))
|
||||
|
||||
def _detect_complex_conditionals(self) -> None:
|
||||
"""Detect complex conditional expressions."""
|
||||
for i, line in enumerate(self.lines):
|
||||
# Count logical operators in line
|
||||
and_or_count = len(re.findall(r'\b(and|or|&&|\|\|)\b', line))
|
||||
|
||||
if and_or_count >= 3:
|
||||
self.smells.append(CodeSmell(
|
||||
smell_type=SmellType.COMPLEX_CONDITIONAL,
|
||||
severity=SmellSeverity.MEDIUM,
|
||||
location=f"{self.filename}:{i+1}",
|
||||
line_start=i + 1,
|
||||
line_end=i + 1,
|
||||
description=f"Complex conditional with {and_or_count} logical operators",
|
||||
suggestion="Apply Decompose Conditional or Consolidate Conditional Expression",
|
||||
code_snippet=self._get_snippet(i, i + 1, 1)
|
||||
))
|
||||
|
||||
def _detect_magic_numbers(self) -> None:
|
||||
"""Detect magic numbers and strings."""
|
||||
# Skip common acceptable values
|
||||
acceptable = {'0', '1', '-1', '2', '100', 'true', 'false', 'null', 'None', '""', "''"}
|
||||
|
||||
for i, line in enumerate(self.lines):
|
||||
# Skip comments and imports
|
||||
stripped = line.strip()
|
||||
if stripped.startswith('#') or stripped.startswith('//') or \
|
||||
stripped.startswith('import') or stripped.startswith('from'):
|
||||
continue
|
||||
|
||||
# Find numeric literals (excluding in variable names)
|
||||
numbers = re.findall(r'(?<![a-zA-Z_])\b(\d+\.?\d*)\b(?![a-zA-Z_])', line)
|
||||
|
||||
for num in numbers:
|
||||
if num not in acceptable and float(num) > 2:
|
||||
# Check if it's likely a magic number (in calculation or comparison)
|
||||
if re.search(rf'[<>=+\-*/]\s*{re.escape(num)}|{re.escape(num)}\s*[<>=+\-*/]', line):
|
||||
self.smells.append(CodeSmell(
|
||||
smell_type=SmellType.MAGIC_NUMBER,
|
||||
severity=SmellSeverity.LOW,
|
||||
location=f"{self.filename}:{i+1}",
|
||||
line_start=i + 1,
|
||||
line_end=i + 1,
|
||||
description=f"Magic number '{num}' - consider using a named constant",
|
||||
suggestion="Replace magic number with named constant",
|
||||
code_snippet=self._get_snippet(i, i + 1, 0)
|
||||
))
|
||||
break # One magic number per line is enough
|
||||
|
||||
def _detect_excessive_comments(self) -> None:
|
||||
"""Detect comments that explain 'what' instead of 'why'."""
|
||||
what_patterns = [
|
||||
r'#\s*(set|get|return|loop|iterate|check|if|increment|decrement)',
|
||||
r'//\s*(set|get|return|loop|iterate|check|if|increment|decrement)',
|
||||
]
|
||||
|
||||
for i, line in enumerate(self.lines):
|
||||
for pattern in what_patterns:
|
||||
if re.search(pattern, line, re.IGNORECASE):
|
||||
self.smells.append(CodeSmell(
|
||||
smell_type=SmellType.EXCESSIVE_COMMENTS,
|
||||
severity=SmellSeverity.LOW,
|
||||
location=f"{self.filename}:{i+1}",
|
||||
line_start=i + 1,
|
||||
line_end=i + 1,
|
||||
description="Comment explains 'what' not 'why' - consider renaming or removing",
|
||||
suggestion="Use Extract Method with descriptive name instead of comment",
|
||||
code_snippet=self._get_snippet(i, i + 1, 0)
|
||||
))
|
||||
break
|
||||
|
||||
def _detect_deeply_nested(self) -> None:
|
||||
"""Detect deeply nested code blocks."""
|
||||
max_depth = 0
|
||||
current_depth = 0
|
||||
depth_start = 0
|
||||
|
||||
for i, line in enumerate(self.lines):
|
||||
if self.language == 'python':
|
||||
# Count by indentation
|
||||
if line.strip():
|
||||
indent = len(line) - len(line.lstrip())
|
||||
depth = indent // 4 # Assume 4-space indent
|
||||
if depth > current_depth:
|
||||
if depth > max_depth:
|
||||
max_depth = depth
|
||||
if depth >= self.THRESHOLDS['max_nesting_depth']:
|
||||
depth_start = i
|
||||
current_depth = depth
|
||||
else:
|
||||
# Count braces
|
||||
current_depth += line.count('{') - line.count('}')
|
||||
if current_depth > max_depth:
|
||||
max_depth = current_depth
|
||||
if current_depth >= self.THRESHOLDS['max_nesting_depth']:
|
||||
depth_start = i
|
||||
|
||||
if max_depth >= self.THRESHOLDS['max_nesting_depth']:
|
||||
self.smells.append(CodeSmell(
|
||||
smell_type=SmellType.DEEPLY_NESTED,
|
||||
severity=SmellSeverity.HIGH if max_depth > 5 else SmellSeverity.MEDIUM,
|
||||
location=f"{self.filename}:{depth_start+1}",
|
||||
line_start=depth_start + 1,
|
||||
line_end=depth_start + 1,
|
||||
description=f"Code nested {max_depth} levels deep (max: {self.THRESHOLDS['max_nesting_depth']})",
|
||||
suggestion="Apply Replace Nested Conditional with Guard Clauses or Extract Method",
|
||||
code_snippet=self._get_snippet(depth_start, depth_start + 5, 0)
|
||||
))
|
||||
|
||||
def _detect_switch_statements(self) -> None:
|
||||
"""Detect switch statements that might need polymorphism."""
|
||||
if self.language == 'python':
|
||||
# Python 3.10+ match statements or if/elif chains
|
||||
pattern = r'^\s*(if|elif).*==.*:'
|
||||
consecutive_conditions = 0
|
||||
chain_start = 0
|
||||
|
||||
for i, line in enumerate(self.lines):
|
||||
if re.search(pattern, line):
|
||||
if consecutive_conditions == 0:
|
||||
chain_start = i
|
||||
consecutive_conditions += 1
|
||||
else:
|
||||
if consecutive_conditions >= 4:
|
||||
self._add_switch_smell(chain_start, i - 1, consecutive_conditions)
|
||||
consecutive_conditions = 0
|
||||
else:
|
||||
# JavaScript/TypeScript switch
|
||||
pattern = r'\bswitch\s*\('
|
||||
for i, line in enumerate(self.lines):
|
||||
if re.search(pattern, line):
|
||||
# Count cases
|
||||
case_count = 0
|
||||
for j in range(i, min(i + 50, len(self.lines))):
|
||||
case_count += len(re.findall(r'\bcase\b', self.lines[j]))
|
||||
if case_count >= 4:
|
||||
self._add_switch_smell(i, i + 1, case_count)
|
||||
|
||||
def _add_switch_smell(self, start: int, end: int, cases: int) -> None:
|
||||
"""Add a switch statement smell."""
|
||||
self.smells.append(CodeSmell(
|
||||
smell_type=SmellType.SWITCH_STATEMENT,
|
||||
severity=SmellSeverity.MEDIUM,
|
||||
location=f"{self.filename}:{start+1}",
|
||||
line_start=start + 1,
|
||||
line_end=end + 1,
|
||||
description=f"Switch/case statement with {cases} cases - consider polymorphism",
|
||||
suggestion="Apply Replace Conditional with Polymorphism",
|
||||
code_snippet=self._get_snippet(start, start + 5, 0)
|
||||
))
|
||||
|
||||
def _detect_message_chains(self) -> None:
|
||||
"""Detect long method chains (train wrecks)."""
|
||||
chain_pattern = r'(\w+(?:\.\w+\([^)]*\)){3,})'
|
||||
|
||||
for i, line in enumerate(self.lines):
|
||||
matches = re.findall(chain_pattern, line)
|
||||
for match in matches:
|
||||
chain_length = match.count('.')
|
||||
if chain_length >= self.THRESHOLDS['long_chain_length']:
|
||||
self.smells.append(CodeSmell(
|
||||
smell_type=SmellType.MESSAGE_CHAIN,
|
||||
severity=SmellSeverity.MEDIUM,
|
||||
location=f"{self.filename}:{i+1}",
|
||||
line_start=i + 1,
|
||||
line_end=i + 1,
|
||||
description=f"Message chain with {chain_length} calls - violates Law of Demeter",
|
||||
suggestion="Apply Hide Delegate to reduce coupling",
|
||||
code_snippet=self._get_snippet(i, i + 1, 0)
|
||||
))
|
||||
|
||||
def _detect_duplicate_code(self) -> None:
|
||||
"""Detect potential duplicate code blocks (simplified)."""
|
||||
# Create line hashes for comparison
|
||||
line_hashes: Dict[str, List[int]] = defaultdict(list)
|
||||
|
||||
for i, line in enumerate(self.lines):
|
||||
normalized = re.sub(r'\s+', ' ', line.strip())
|
||||
if len(normalized) > 20: # Only significant lines
|
||||
line_hashes[normalized].append(i)
|
||||
|
||||
# Find duplicates
|
||||
for normalized, positions in line_hashes.items():
|
||||
if len(positions) >= 3: # At least 3 occurrences
|
||||
self.smells.append(CodeSmell(
|
||||
smell_type=SmellType.DUPLICATE_CODE,
|
||||
severity=SmellSeverity.MEDIUM,
|
||||
location=f"{self.filename}:{positions[0]+1}",
|
||||
line_start=positions[0] + 1,
|
||||
line_end=positions[0] + 1,
|
||||
description=f"Potential duplicate code found {len(positions)} times",
|
||||
suggestion="Apply Extract Method to eliminate duplication",
|
||||
code_snippet=self._get_snippet(positions[0], positions[0] + 1, 0)
|
||||
))
|
||||
|
||||
def _detect_dead_code(self) -> None:
|
||||
"""Detect potentially dead code (simplified)."""
|
||||
# Look for common dead code patterns
|
||||
patterns = [
|
||||
(r'^\s*#.*TODO.*delete', "TODO to delete"),
|
||||
(r'^\s*#.*FIXME.*remove', "FIXME to remove"),
|
||||
(r'^\s*//.*TODO.*delete', "TODO to delete"),
|
||||
(r'^\s*//.*FIXME.*remove', "FIXME to remove"),
|
||||
(r'^\s*if\s+False:', "Code behind 'if False'"),
|
||||
(r'^\s*if\s*\(\s*false\s*\)', "Code behind 'if (false)'"),
|
||||
]
|
||||
|
||||
for i, line in enumerate(self.lines):
|
||||
for pattern, desc in patterns:
|
||||
if re.search(pattern, line, re.IGNORECASE):
|
||||
self.smells.append(CodeSmell(
|
||||
smell_type=SmellType.DEAD_CODE,
|
||||
severity=SmellSeverity.LOW,
|
||||
location=f"{self.filename}:{i+1}",
|
||||
line_start=i + 1,
|
||||
line_end=i + 1,
|
||||
description=f"Potential dead code: {desc}",
|
||||
suggestion="Remove dead code",
|
||||
code_snippet=self._get_snippet(i, i + 1, 0)
|
||||
))
|
||||
|
||||
|
||||
def print_report(report: SmellReport, verbose: bool = False) -> None:
|
||||
"""Print smell report in readable format."""
|
||||
print("=" * 70)
|
||||
print(f"CODE SMELL DETECTION REPORT: {report.filename}")
|
||||
print("=" * 70)
|
||||
|
||||
print(f"\n📊 SUMMARY")
|
||||
print("-" * 40)
|
||||
print(f" Total smells found: {len(report.smells)}")
|
||||
print(f" Critical: {report.critical_count}")
|
||||
print(f" High: {report.high_count}")
|
||||
print(f" Medium: {report.medium_count}")
|
||||
print(f" Low: {report.low_count}")
|
||||
|
||||
if not report.smells:
|
||||
print("\n✅ No code smells detected!")
|
||||
print("=" * 70)
|
||||
return
|
||||
|
||||
# Group by type
|
||||
by_type: Dict[SmellType, List[CodeSmell]] = defaultdict(list)
|
||||
for smell in report.smells:
|
||||
by_type[smell.smell_type].append(smell)
|
||||
|
||||
print(f"\n📋 FINDINGS BY TYPE")
|
||||
print("-" * 40)
|
||||
|
||||
for smell_type, smells in sorted(by_type.items(), key=lambda x: -len(x[1])):
|
||||
print(f"\n### {smell_type.value} ({len(smells)} found)")
|
||||
|
||||
for smell in sorted(smells, key=lambda x: x.severity.value):
|
||||
severity_icon = {
|
||||
SmellSeverity.CRITICAL: "🔴",
|
||||
SmellSeverity.HIGH: "🟠",
|
||||
SmellSeverity.MEDIUM: "🟡",
|
||||
SmellSeverity.LOW: "🟢",
|
||||
}[smell.severity]
|
||||
|
||||
print(f"\n {severity_icon} [{smell.severity.value}] {smell.location}")
|
||||
print(f" {smell.description}")
|
||||
print(f" 💡 {smell.suggestion}")
|
||||
|
||||
if verbose and smell.code_snippet:
|
||||
print(f"\n Code:")
|
||||
for snippet_line in smell.code_snippet.split('\n'):
|
||||
print(f" {snippet_line}")
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print("💡 RECOMMENDED ACTIONS")
|
||||
print("-" * 40)
|
||||
|
||||
if report.critical_count > 0:
|
||||
print(" 1. Address CRITICAL issues immediately")
|
||||
if report.high_count > 0:
|
||||
print(" 2. Plan to fix HIGH severity issues this sprint")
|
||||
if report.medium_count > 0:
|
||||
print(" 3. Schedule MEDIUM issues for upcoming work")
|
||||
if report.low_count > 0:
|
||||
print(" 4. Fix LOW issues opportunistically")
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
|
||||
|
||||
def analyze_directory(directory: str, verbose: bool = False) -> None:
|
||||
"""Analyze all supported files in a directory."""
|
||||
supported_extensions = ['.py', '.js', '.jsx', '.ts', '.tsx']
|
||||
files = []
|
||||
|
||||
for root, _, filenames in os.walk(directory):
|
||||
for filename in filenames:
|
||||
if any(filename.endswith(ext) for ext in supported_extensions):
|
||||
files.append(os.path.join(root, filename))
|
||||
|
||||
if not files:
|
||||
print(f"No supported files found in {directory}")
|
||||
return
|
||||
|
||||
print(f"Scanning {len(files)} files in {directory}...\n")
|
||||
|
||||
total_smells = 0
|
||||
total_critical = 0
|
||||
total_high = 0
|
||||
files_with_smells = 0
|
||||
|
||||
for filepath in sorted(files):
|
||||
try:
|
||||
detector = SmellDetector(filepath)
|
||||
report = detector.detect_all()
|
||||
|
||||
if report.smells:
|
||||
files_with_smells += 1
|
||||
total_smells += len(report.smells)
|
||||
total_critical += report.critical_count
|
||||
total_high += report.high_count
|
||||
|
||||
flag = " 🔴" if report.critical_count else (" 🟠" if report.high_count else " 🟡")
|
||||
print(f" {report.filename}: {len(report.smells)} smells{flag}")
|
||||
|
||||
if verbose:
|
||||
for smell in report.smells:
|
||||
print(f" - [{smell.severity.value}] {smell.smell_type.value}: line {smell.line_start}")
|
||||
else:
|
||||
print(f" {report.filename}: ✅ Clean")
|
||||
|
||||
except Exception as e:
|
||||
print(f" Error analyzing {filepath}: {e}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("SUMMARY")
|
||||
print("=" * 60)
|
||||
print(f" Files analyzed: {len(files)}")
|
||||
print(f" Files with smells: {files_with_smells}")
|
||||
print(f" Total smells found: {total_smells}")
|
||||
print(f" Critical issues: {total_critical}")
|
||||
print(f" High severity issues: {total_high}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Detect code smells in source files',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
%(prog)s myfile.py Analyze single file
|
||||
%(prog)s --dir src/ Analyze directory
|
||||
%(prog)s -v myfile.py Verbose with code snippets
|
||||
"""
|
||||
)
|
||||
parser.add_argument('file', nargs='?', help='File to analyze')
|
||||
parser.add_argument('--dir', '-d', help='Directory to analyze')
|
||||
parser.add_argument('--verbose', '-v', action='store_true', help='Show code snippets')
|
||||
parser.add_argument('--json', '-j', action='store_true', help='Output as JSON')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.dir:
|
||||
analyze_directory(args.dir, args.verbose)
|
||||
elif args.file:
|
||||
detector = SmellDetector(args.file)
|
||||
report = detector.detect_all()
|
||||
|
||||
if args.json:
|
||||
import json
|
||||
smells_data = [{
|
||||
'type': s.smell_type.value,
|
||||
'severity': s.severity.value,
|
||||
'location': s.location,
|
||||
'line_start': s.line_start,
|
||||
'line_end': s.line_end,
|
||||
'description': s.description,
|
||||
'suggestion': s.suggestion,
|
||||
} for s in report.smells]
|
||||
print(json.dumps({
|
||||
'filename': report.filename,
|
||||
'total_smells': len(report.smells),
|
||||
'critical': report.critical_count,
|
||||
'high': report.high_count,
|
||||
'medium': report.medium_count,
|
||||
'low': report.low_count,
|
||||
'smells': smells_data
|
||||
}, indent=2))
|
||||
else:
|
||||
print_report(report, args.verbose)
|
||||
else:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user