From 7830e3e63a1ce27a94190d53e435faeaf1c7eb27 Mon Sep 17 00:00:00 2001 From: Gronoxx Date: Sat, 20 Jun 2026 18:41:36 -0300 Subject: [PATCH] =?UTF-8?q?feat(detectores):=20parametriza=20R1/R2/R4=20po?= =?UTF-8?q?r=20threshold=20(fonte=20=C3=BAnica=20p/=20calibra=C3=A7=C3=A3o?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Permite passar `params` aos detectores long_method, long_param_list e deep_nesting, mantendo `detect(fn)` 100% retrocompatível (defaults idênticos). Elimina a necessidade de uma cópia divergente dos detectores no estudo de calibração (tp-es2-dataset), que agora importa estes canônicos. - long_method: liga o 3o limiar (stmt_threshold) via contagem de statements no AST (proxy do Pylint R0915). Default = None (desativado), então o detector default segue idêntico; o grid de calibração passa valores explícitos para ativá-lo. - __init__: exporta DETECTOR_DEFAULT_PARAMS (R1/R2/R4). - 243 testes passam sem alteração. Co-Authored-By: Claude Opus 4.8 (1M context) --- detectores/__init__.py | 9 +++++ detectores/deep_nesting.py | 23 +++++++------ detectores/long_method.py | 62 ++++++++++++++++++++++++++--------- detectores/long_param_list.py | 17 ++++++---- 4 files changed, 79 insertions(+), 32 deletions(-) diff --git a/detectores/__init__.py b/detectores/__init__.py index 38f8f6d..48ff6aa 100644 --- a/detectores/__init__.py +++ b/detectores/__init__.py @@ -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, } \ No newline at end of file diff --git a/detectores/deep_nesting.py b/detectores/deep_nesting.py index 5235996..b5e9a21 100644 --- a/detectores/deep_nesting.py +++ b/detectores/deep_nesting.py @@ -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: @@ -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}, - ) \ No newline at end of file + evidence={"max_depth": depth, "threshold": p["max_depth"]}, + ) diff --git a/detectores/long_method.py b/detectores/long_method.py index 2baa47d..093665e 100644 --- a/detectores/long_method.py +++ b/detectores/long_method.py @@ -1,15 +1,42 @@ +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 ) @@ -17,20 +44,22 @@ def detect(fn: FunctionInfo) -> DetectionResult: 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, @@ -38,7 +67,10 @@ def detect(fn: FunctionInfo) -> DetectionResult: 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, }, - ) \ No newline at end of file + ) diff --git a/detectores/long_param_list.py b/detectores/long_param_list.py index f368fd7..25e5f47 100644 --- a/detectores/long_param_list.py +++ b/detectores/long_param_list.py @@ -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}, - ) \ No newline at end of file + evidence={"params": fn.params, "count": count, "threshold": p["max_params"]}, + )