Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions detectores/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,13 @@
"magic_numbers": magic_numbers.detect,
"deep_nesting": deep_nesting.detect,
"dead_code": dead_code.detect,
}

# Limiares default por detector parametrizável (R1/R2/R4). Usado por
# ferramentas que varrem thresholds (p.ex. o estudo de calibração no
# repo do dataset). `magic_numbers` e `dead_code` não são parametrizados.
DETECTOR_DEFAULT_PARAMS = {
"long_method": long_method.DEFAULT_PARAMS,
"long_param_list": long_param_list.DEFAULT_PARAMS,
"deep_nesting": deep_nesting.DEFAULT_PARAMS,
}
23 changes: 13 additions & 10 deletions detectores/deep_nesting.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,28 @@
from .data_structs import FunctionInfo
from .base import DetectionResult

THRESHOLD = 3 # profundidade > 3 = smell
DEFAULT_PARAMS = {
"max_depth": 3, # profundidade > 3 = smell
}

NESTING_NODES = (ast.If, ast.For, ast.While, ast.With, ast.Try,
ast.AsyncFor, ast.AsyncWith)

def _max_depth(node, current=0) -> int:

if isinstance(node, NESTING_NODES):
current += 1

max_d = current

for child in ast.iter_child_nodes(node):
max_d = max(max_d, _max_depth(child, current))

return max_d

def detect(fn: FunctionInfo) -> DetectionResult:

def detect(fn: FunctionInfo, params: dict | None = None) -> DetectionResult:
p = {**DEFAULT_PARAMS, **(params or {})}

try:
tree = ast.parse(fn.source)
except SyntaxError:
Expand All @@ -36,7 +39,7 @@ def detect(fn: FunctionInfo) -> DetectionResult:

return DetectionResult(
smell="deep_nesting",
detected=depth > THRESHOLD,
detected=depth > p["max_depth"],
confidence=1.0,
evidence={"max_depth": depth, "threshold": THRESHOLD},
)
evidence={"max_depth": depth, "threshold": p["max_depth"]},
)
62 changes: 47 additions & 15 deletions detectores/long_method.py
Original file line number Diff line number Diff line change
@@ -1,44 +1,76 @@
import ast
import lizard
from .data_structs import FunctionInfo
from .base import DetectionResult

LINE_THRESHOLD = 30 # linhas lógicas
STMT_THRESHOLD = 15 # statements (Pylint R0915 usa 50, mas 15 é mais restrito)
CCN_THRESHOLD = 10 # complexidade ciclomática (McCabe)
# Limiares default. Mantêm o comportamento histórico do detector:
# `stmt_threshold=None` desativa a checagem por número de statements, de modo
# que `detect(fn)` (sem params) é idêntico à versão pré-parametrização. Quem
# quiser varrer o limiar de statements (p.ex. o estudo de calibração) passa um
# inteiro explícito em `params`.
DEFAULT_PARAMS = {
"line_threshold": 30, # linhas lógicas (lizard nloc)
"stmt_threshold": None, # statements (Pylint R0915 usa 50); None = desativado
"ccn_threshold": 10, # complexidade ciclomática (McCabe)
}


def _count_statements(source: str) -> int:
"""Conta os statements do corpo da função (proxy do Pylint R0915).

Lizard não expõe contagem de statements, então usamos o `ast`: contamos os
nós-statement alcançáveis a partir da `FunctionDef` (inclui aninhados) e
descontamos o próprio `def`. Retorna 0 se o source não parsear.
"""
try:
tree = ast.parse(source)
except SyntaxError:
return 0
fn = next((n for n in ast.walk(tree)
if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))), None)
if fn is None:
return 0
return sum(1 for n in ast.walk(fn) if isinstance(n, ast.stmt)) - 1


def detect(fn: FunctionInfo, params: dict | None = None) -> DetectionResult:
p = {**DEFAULT_PARAMS, **(params or {})}

def detect(fn: FunctionInfo) -> DetectionResult:

# Lizard analisa a string de source diretamente

result = lizard.analyze_file.analyze_source_code(
fn.name + ".py", fn.source
)

if not result.function_list:
# fallback: contar linhas brutas
loc = fn.end_lineno - fn.lineno + 1

return DetectionResult(
smell="long_method",
detected=loc > LINE_THRESHOLD,
detected=loc > p["line_threshold"],
confidence=1.0,
evidence={"lines_fallback": loc, "threshold": LINE_THRESHOLD},
evidence={"lines_fallback": loc, "threshold": p["line_threshold"], "params": p},
)

func = result.function_list[0]
n_stmts = _count_statements(fn.source)
detected = (
func.nloc > LINE_THRESHOLD or
func.cyclomatic_complexity > CCN_THRESHOLD
func.nloc > p["line_threshold"]
or func.cyclomatic_complexity > p["ccn_threshold"]
or (p["stmt_threshold"] is not None and n_stmts > p["stmt_threshold"])
)

return DetectionResult(
smell="long_method",
detected=detected,
confidence=1.0,
evidence={
"lines_of_code": func.nloc,
"complexidade_ciclomatica": func.cyclomatic_complexity,
"line_threshold": LINE_THRESHOLD,
"complexidade_threshold": CCN_THRESHOLD,
"n_statements": n_stmts,
"line_threshold": p["line_threshold"],
"stmt_threshold": p["stmt_threshold"],
"complexidade_threshold": p["ccn_threshold"],
"params": p,
},
)
)
17 changes: 10 additions & 7 deletions detectores/long_param_list.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
from .data_structs import FunctionInfo
from .base import DetectionResult

THRESHOLD = 5 # >5 parâmetros = smell
DEFAULT_PARAMS = {
"max_params": 5, # >5 parâmetros = smell
}

def detect(fn: FunctionInfo, params: dict | None = None) -> DetectionResult:
p = {**DEFAULT_PARAMS, **(params or {})}

def detect(fn: FunctionInfo) -> DetectionResult:

count = len(fn.params)

return DetectionResult(
smell="long_param_list",
detected=count > THRESHOLD,
detected=count > p["max_params"],
confidence=1.0,
evidence={"params": fn.params, "count": count, "threshold": THRESHOLD},
)
evidence={"params": fn.params, "count": count, "threshold": p["max_params"]},
)
Loading