NexusCoder / nexus /tools /code_smells.py
AdminReal's picture
Import NexusCoder from github.com/mhieuhonda/NexusCoder
eca5751 verified
Raw
History Blame Contribute Delete
6.37 kB
"""
Code Smells Detector - Phát hiện code smells trong Python file.
Author: Hieu Louis (2026)
Phát hiện các smells:
- long_function : quá nhiều statements/lines
- too_many_params : > 5 parameters
- deep_nesting : nesting > 4 levels
- long_class : class có quá nhiều methods
- duplicate_string_literal : string literal xuất hiện ≥ 3 lần (len ≥ 5)
Dùng `ast` để walk tree. Read-only (SAFE).
"""
from __future__ import annotations
import ast
import json
from collections import Counter
from typing import Any, Dict, List, Optional
from .base import Tool, ToolResult, ToolContext, ToolCategory, ToolSafety
# Ngưỡng smell // smell thresholds (tunable)
LONG_FUNCTION_STMTS = 50
LONG_FUNCTION_LINES = 50
TOO_MANY_PARAMS = 5
DEEP_NESTING = 4
LONG_CLASS_METHODS = 20
DUP_LIT_MIN_COUNT = 3
DUP_LIT_MIN_LEN = 5
DUP_LIT_MAX_REPORT = 50
class _SmellVisitor(ast.NodeVisitor):
"""Visitor quét AST để phát hiện code smells."""
def __init__(self) -> None:
self.smells: List[Dict[str, Any]] = []
self._str_literals: List[str] = []
def _record(self, kind: str, name: str, line: int, detail: Dict[str, Any]) -> None:
self.smells.append({"kind": kind, "name": name, "line": line, **detail})
def _max_nesting(self, node: ast.AST, depth: int = 0) -> int:
"""Tính độ sâu nesting tối đa trong block."""
max_d = depth
for child in ast.iter_child_nodes(node):
if isinstance(child, (ast.If, ast.For, ast.While, ast.With, ast.Try, ast.ExceptHandler)):
d = self._max_nesting(child, depth + 1)
else:
d = self._max_nesting(child, depth)
if d > max_d:
max_d = d
return max_d
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
self._check_function(node)
self.generic_visit(node)
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
self._check_function(node)
self.generic_visit(node)
def _check_function(self, node: ast.FunctionDef) -> None:
name = node.name
line = node.lineno
end_line = getattr(node, "end_lineno", line)
# Statement count (approximate)
n_stmts = sum(1 for _ in ast.walk(node) if isinstance(_, ast.stmt))
n_lines = max(1, end_line - line + 1)
if n_stmts > LONG_FUNCTION_STMTS or n_lines > LONG_FUNCTION_LINES:
self._record("long_function", name, line, {"statements": n_stmts, "lines": n_lines})
# Parameters count
n_args = (
len(node.args.args)
+ len(node.args.kwonlyargs)
+ len(node.args.posonlyargs)
)
if node.args.vararg:
n_args += 1
if node.args.kwarg:
n_args += 1
if n_args > TOO_MANY_PARAMS:
self._record("too_many_params", name, line, {"params": n_args})
# Nesting depth
nesting = self._max_nesting(node)
if nesting > DEEP_NESTING:
self._record("deep_nesting", name, line, {"depth": nesting})
def visit_ClassDef(self, node: ast.ClassDef) -> None:
n_methods = sum(
1 for m in node.body
if isinstance(m, (ast.FunctionDef, ast.AsyncFunctionDef))
)
if n_methods > LONG_CLASS_METHODS:
self._record("long_class", node.name, node.lineno, {"methods": n_methods})
self.generic_visit(node)
def visit_Constant(self, node: ast.Constant) -> None:
# ast.Str deprecated in 3.8+; use ast.Constant
if isinstance(node.value, str) and len(node.value) >= DUP_LIT_MIN_LEN:
self._str_literals.append(node.value)
self.generic_visit(node)
class CodeSmellsTool(Tool):
"""Phát hiện code smells (long function, too many params, deep nesting, ...)."""
category = ToolCategory.CODE
safety = ToolSafety.SAFE # read-only analysis
@property
def name(self) -> str:
return "code_smells"
@property
def description(self) -> str:
return (
"Phát hiện code smells trong Python file: long function, too many params, "
"deep nesting, long class, duplicate string literals."
)
@property
def parameters(self) -> Dict[str, Any]:
return {
"type": "object",
"properties": {
"path": {"type": "string", "description": "File Python (.py) để phân tích"},
},
"required": ["path"],
}
def validate_args(self, args: Dict[str, Any]) -> Optional[str]:
if not args.get("path"):
return "Missing required arg: path"
return None
def execute(self, args: Dict[str, Any], context: ToolContext) -> ToolResult:
path: str = args["path"]
try:
with open(path, "r", encoding="utf-8") as f:
source = f.read()
except Exception as e:
return ToolResult(success=False, error=f"Không đọc được file: {e}", return_code=1)
try:
tree = ast.parse(source)
except SyntaxError as e:
return ToolResult(
success=False,
error=f"SyntaxError line {e.lineno}: {e.msg}",
return_code=1,
)
visitor = _SmellVisitor()
visitor.visit(tree)
# Duplicate string literals analysis
dup_lits = [
{"literal": lit, "count": cnt}
for lit, cnt in Counter(visitor._str_literals).most_common()
if cnt >= DUP_LIT_MIN_COUNT
][:DUP_LIT_MAX_REPORT]
for d in dup_lits:
visitor.smells.append({
"kind": "duplicate_string_literal",
"name": "<literal>",
"line": 0,
**d,
})
by_kind: Dict[str, int] = {}
for s in visitor.smells:
by_kind[s["kind"]] = by_kind.get(s["kind"], 0) + 1
return ToolResult(
success=True,
output=json.dumps(
{"smells": visitor.smells, "summary": by_kind},
indent=2, ensure_ascii=False,
),
metadata={
"path": path,
"smell_count": len(visitor.smells),
"by_kind": by_kind,
},
)