NexusCoder / nexus /tools /code_metrics.py
AdminReal's picture
Import NexusCoder from github.com/mhieuhonda/NexusCoder
eca5751 verified
Raw
History Blame Contribute Delete
5.95 kB
"""
Code Metrics Tool - Tính metrics: LOC, SLOC, comments, blank lines, file count.
Author: Hieu Louis (2026)
Sử dụng stdlib `tokenize` cho Python files để đếm comment chính xác,
fallback heuristic (comment prefix) cho các ngôn ngữ khác.
"""
from __future__ import annotations
import io
import json
import os
import tokenize
from typing import Any, Dict, List, Optional
from .base import Tool, ToolResult, ToolContext, ToolCategory, ToolSafety
DEFAULT_EXTENSIONS = [
".py", ".js", ".ts", ".java", ".c", ".cpp", ".h", ".hpp",
".go", ".rs", ".rb", ".php", ".sql", ".sh", ".yml", ".yaml",
]
# Map extension → comment prefix (cho non-Python files)
COMMENT_PREFIX = {
".js": "//", ".ts": "//", ".java": "//", ".c": "//", ".cpp": "//",
".h": "//", ".hpp": "//", ".go": "//", ".rs": "//", ".php": "//",
".sh": "#", ".yml": "#", ".yaml": "#",
".rb": "#", ".sql": "--",
}
def _metrics_python(source: str) -> Dict[str, int]:
"""Tính metrics cho file Python dùng tokenize (chính xác)."""
lines = source.splitlines()
loc = len(lines)
blank = sum(1 for ln in lines if not ln.strip())
comments = 0
try:
toks = list(tokenize.generate_tokens(io.StringIO(source).readline))
for tok in toks:
if tok.type == tokenize.COMMENT:
comments += 1
except tokenize.TokenError:
# Fallback heuristic
comments = sum(1 for ln in lines if ln.strip().startswith("#"))
# SLOC = non-blank, non-pure-comment lines
sloc = 0
for ln in lines:
s = ln.strip()
if not s:
continue
if s.startswith("#"):
continue
sloc += 1
return {"loc": loc, "sloc": sloc, "comments": comments, "blank": blank}
def _metrics_text(source: str, comment_prefix: str = "#") -> Dict[str, int]:
"""Tính metrics cho file text generic (non-Python) theo prefix."""
lines = source.splitlines()
loc = len(lines)
blank = sum(1 for ln in lines if not ln.strip())
comments = sum(1 for ln in lines if ln.strip().startswith(comment_prefix))
sloc = loc - blank - comments
if sloc < 0:
sloc = 0
return {"loc": loc, "sloc": sloc, "comments": comments, "blank": blank}
def _walk_files(path: str, extensions: Optional[List[str]]) -> List[str]:
"""Walk tất cả files trong path (file hoặc dir), lọc theo extension."""
if os.path.isfile(path):
return [path]
out: List[str] = []
for root, _dirs, names in os.walk(path):
for name in sorted(names):
if extensions:
if not any(name.endswith(ext) for ext in extensions):
continue
out.append(os.path.join(root, name))
return out
class CodeMetricsTool(Tool):
"""Tính LOC/SLOC/comments/blank lines/file count cho file hoặc thư mục."""
category = ToolCategory.CODE
safety = ToolSafety.SAFE # read-only
@property
def name(self) -> str:
return "code_metrics"
@property
def description(self) -> str:
return (
"Tính metrics: LOC, SLOC, comments, blank lines, file count. "
"Hỗ trợ thư mục hoặc single file. Python dùng tokenize, "
"ngôn ngữ khác dùng heuristic theo comment prefix."
)
@property
def parameters(self) -> Dict[str, Any]:
return {
"type": "object",
"properties": {
"path": {"type": "string", "description": "File hoặc thư mục"},
"extensions": {
"type": "array",
"items": {"type": "string"},
"description": (
"Lọc theo extension (vd ['.py', '.js']). "
"Mặc định auto-detect nhiều loại code files."
),
},
},
"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"]
extensions: List[str] = args.get("extensions") or DEFAULT_EXTENSIONS
if not os.path.exists(path):
return ToolResult(
success=False,
error=f"Path không tồn tại: {path}",
return_code=1,
)
files = _walk_files(path, extensions)
if not files:
return ToolResult(
success=True,
output="[]",
metadata={"path": path, "file_count": 0, "totals": {}},
)
per_file: List[Dict[str, Any]] = []
totals: Dict[str, int] = {"loc": 0, "sloc": 0, "comments": 0, "blank": 0}
for fp in files:
try:
with open(fp, "r", encoding="utf-8", errors="replace") as f:
src = f.read()
except Exception:
continue
if fp.endswith(".py"):
m = _metrics_python(src)
else:
# Tìm comment prefix phù hợp
prefix = "#"
for ext, p in COMMENT_PREFIX.items():
if fp.endswith(ext):
prefix = p
break
m = _metrics_text(src, prefix)
m["file"] = fp
per_file.append(m)
for k in totals:
totals[k] += m.get(k, 0)
return ToolResult(
success=True,
output=json.dumps(
{"files": per_file, "totals": totals},
indent=2, ensure_ascii=False,
),
metadata={
"path": path,
"file_count": len(per_file),
"totals": totals,
},
)