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:
Evgenij I
2026-04-09 23:59:59 +03:00
parent c0d400b21b
commit 1a567be793
75 changed files with 7039 additions and 13 deletions

5
uk/03-skills/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
# Local skill testing
.claude/
# Blog post outputs
blog-posts/

View File

@@ -0,0 +1,14 @@
Тема: [Чітка, орієнтована на вигоду тема]
Вітаю, [Ім'я]!
[Вступ: Яка цінність для них]
[Основна частина: Як це працює / Що вони отримають]
[Конкретний приклад або вигода]
[Заклик до дії: Чіткий наступний крок]
З повагою,
[Ім'я]

View File

@@ -0,0 +1,4 @@
[Хук: Привернути увагу в першому рядку]
[2-3 рядки: Цінність або цікавий факт]
[Заклик до дії: Посилання, питання або залучення]
[Емодзі: 1-2 максимум для візуального інтересу]

View File

@@ -0,0 +1,13 @@
# Приклади тону бренду
## Захоплива анонс
"Зекономте 8 годин на тиждень на код-рев'ю. Claude переглядає ваші PR автоматично."
## Емпатична підтримка
"Ми знаємо, що деплої можуть бути стресовими. Claude бере тестування на себе, щоб вам не хвилюватися."
## Впевнена презентація продукту
"Claude не просто пропонує код. Він розуміє вашу архітектуру і підтримує консистентність."
## Освітній блог-пост
"Давайте розглянемо, як агенти покращують процеси код-рев'ю. Ось що ми дізналися..."

View 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}")

View 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])

View 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 зірок

View File

@@ -0,0 +1,47 @@
# Чеклист код-рев'ю
## Чеклист безпеки
- [ ] Немає захардкоджених облікових даних або секретів
- [ ] Валідація введення для всіх даних від користувача
- [ ] Захист від SQL-ін'єкцій (параметризовані запити)
- [ ] CSRF-захист для операцій зі зміною стану
- [ ] Захист від XSS з правильним екрануванням
- [ ] Перевірка автентифікації на захищених ендпоінтах
- [ ] Перевірка авторизації для ресурсів
- [ ] Безпечне хешування паролів (bcrypt, argon2)
- [ ] Немає чутливих даних у логах
- [ ] HTTPS обов'язковий
## Чеклист продуктивності
- [ ] Немає N+1 запитів
- [ ] Правильне використання індексів
- [ ] Кешування реалізовано де доцільно
- [ ] Немає блокуючих операцій в основному потоці
- [ ] Async/await використовується коректно
- [ ] Великі набори даних пагіновані
- [ ] Пул з'єднань до бази даних
- [ ] Регулярні вирази оптимізовані
- [ ] Немає зайвого створення об'єктів
- [ ] Витоки пам'яті запобігаються
## Чеклист якості
- [ ] Функції < 50 рядків
- [ ] Зрозумілі назви змінних
- [ ] Немає дублювання коду
- [ ] Правильна обробка помилок
- [ ] Коментарі пояснюють ЧОМУ, а не ЩО
- [ ] Немає console.log у продакшені
- [ ] Перевірка типів (TypeScript/JSDoc)
- [ ] Принципи SOLID дотримані
- [ ] Патерни проєктування застосовані коректно
- [ ] Самодокументований код
## Чеклист тестування
- [ ] Юніт-тести написані
- [ ] Граничні випадки покриті
- [ ] Сценарії помилок протестовані
- [ ] Інтеграційні тести є
- [ ] Покриття > 80%
- [ ] Немає нестабільних тестів
- [ ] Зовнішні залежності замоковані
- [ ] Зрозумілі назви тестів

View 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)

View 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()

View 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()