From 5503ab80857c850a6e494f0e745ccfd7c7e2e748 Mon Sep 17 00:00:00 2001 From: Filipe Terra Date: Sun, 21 Jun 2026 11:59:35 -0300 Subject: [PATCH 1/4] =?UTF-8?q?feat(cli):=20aceita=20filtro=20de=20smell?= =?UTF-8?q?=20por=20ID=20(R1=E2=80=93R5)=20al=C3=A9m=20do=20nome;=20feat(c?= =?UTF-8?q?li):=20adiciona=20--version=20(SemVer=201.0.0);=20feat(cli):=20?= =?UTF-8?q?veredito=20de=20resumo=20mais=20claro=20no=20scan;=20docs(cli):?= =?UTF-8?q?=20documenta=20exit=20codes=20e=20exemplos=20no=20--help;=20tes?= =?UTF-8?q?t(cli):=20cobre=20filtro=20por=20ID,=20--version,=20veredito=20?= =?UTF-8?q?e=20help?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cli.py | 97 +++++++++++++++++++++++++++++++++++++++-------- tests/test_cli.py | 97 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 179 insertions(+), 15 deletions(-) diff --git a/cli.py b/cli.py index ed3040f..2109507 100644 --- a/cli.py +++ b/cli.py @@ -5,8 +5,9 @@ cada função/método e apresenta os smells encontrados (árvore Rich ou JSON). Uso: + python3 cli.py --version python3 cli.py scan caminho/arquivo.py - python3 cli.py scan caminho/projeto/ --smell long_method --smell dead_code + python3 cli.py scan caminho/projeto/ --smell R1 --smell dead_code python3 cli.py scan projeto/ --json > resultado.json python3 cli.py smells """ @@ -27,6 +28,9 @@ from detectores import DETECTORS # noqa: E402 from extracao.mineracao.ast_utils import parse_file # noqa: E402 +# Versão semântica da ferramenta (SemVer). Primeira release pública. +__version__ = "1.0.0" + # Diretórios que nunca contêm código do usuário a analisar _SKIP_DIRS = {".git", "__pycache__", ".venv", "venv", ".tox", "node_modules", ".mypy_cache", ".ruff_cache", "build", "dist", ".eggs"} @@ -40,6 +44,30 @@ "dead_code": ("R5", "Código morto — candidato a remoção"), } +# Mapa ID (R1–R5) → nome canônico do smell, para o usuário filtrar por qualquer um dos dois. +_ID_PARA_SMELL = {rid.upper(): nome for nome, (rid, _) in SMELL_INFO.items()} + + +def _normalizar_smells(ctx, param, valores): + """Aceita ID (R1–R5, case-insensitive) ou nome do smell; normaliza para o nome canônico. + + Mantém a ordem de digitação e remove duplicatas (ex.: `--smell R2 --smell + long_param_list` vira um único `long_param_list`). Erro de usuário claro + quando o valor não corresponde a nenhum smell. + """ + canonicos = [] + for valor in valores: + chave = valor.strip() + if chave in DETECTORS: + canonicos.append(chave) + elif chave.upper() in _ID_PARA_SMELL: + canonicos.append(_ID_PARA_SMELL[chave.upper()]) + else: + ids = ", ".join(f"{rid} ({nome})" for nome, (rid, _) in SMELL_INFO.items()) + raise click.BadParameter( + f"'{valor}' não é um smell conhecido. Use o ID ou o nome: {ids}.") + return tuple(dict.fromkeys(canonicos)) # dedup preservando ordem + def _coletar_arquivos(caminhos: tuple[str, ...]) -> list[Path]: """Expande arquivos/diretórios em uma lista ordenada de arquivos .py.""" @@ -127,21 +155,47 @@ def _analisar(arquivos: list[Path], smells: tuple[str, ...]) -> tuple[list[dict] return resultados, avisos -@click.group() +@click.group( + epilog="\b\n" + "Exemplos:\n" + " cli.py scan src/ analisa um diretório inteiro\n" + " cli.py scan app.py --smell R1 só Long Method (filtro por ID)\n" + " cli.py scan src/ --json > out.json saída estruturada p/ integração\n" + " cli.py smells lista os smells suportados\n") +@click.version_option(__version__, "-V", "--version", "--V", prog_name="PySniff") def cli(): - """Detector de code smells Python (5 smells, R1–R5) por análise estática.""" + """PySniff — detector de code smells em Python por análise estática. + Aponte a ferramenta para arquivos ou diretórios e ela reporta, por + função/método, os 5 smells de manutenção suportados (R1–R5) com a + evidência métrica de cada detecção. Use `smells` para ver a lista e + `scan --help` para os detalhes da análise. + """ -@cli.command() -@click.argument("caminhos", nargs=-1, required=True, type=click.Path(exists=True)) -@click.option("--smell", "smells", multiple=True, - type=click.Choice(sorted(DETECTORS)), help="Restringe aos smells dados (repetível).") -@click.option("--json", "como_json", is_flag=True, help="Saída JSON em vez de árvore Rich.") + +@cli.command( + epilog="\b\n" + "Códigos de saída:\n" + " 0 análise concluída (mesmo com smells, exceto se --fail-on-detect)\n" + " 1 --fail-on-detect ativo e ao menos um smell detectado\n" + " 2 nenhum arquivo .py encontrado nos caminhos informados\n") +@click.argument("caminhos", nargs=-1, required=True, type=click.Path(exists=True), + metavar="CAMINHOS...") +@click.option("--smell", "smells", multiple=True, metavar="SMELL", callback=_normalizar_smells, + help="Restringe a um smell, por ID (R1–R5) ou nome (ex.: long_method). " + "Repetível. Padrão: todos.") +@click.option("--json", "como_json", is_flag=True, + help="Emite JSON estruturado em vez da árvore visual (para integração/scripts).") @click.option("--fail-on-detect", is_flag=True, - help="Sai com código 1 se qualquer smell for detectado (uso em CI).") + help="Retorna código de saída 1 se algum smell for detectado (útil em CI).") def scan(caminhos, smells, como_json, fail_on_detect): - """Analisa arquivos ou diretórios e reporta smells por função.""" + """Analisa CAMINHOS (arquivos .py ou diretórios) e reporta smells por função. + + Diretórios são percorridos recursivamente; pastas como .venv, .git e + __pycache__ são ignoradas automaticamente. + """ console = Console(stderr=False) + filtrado = bool(smells) # o usuário restringiu os smells via --smell? smells = smells or tuple(sorted(DETECTORS, key=lambda s: SMELL_INFO[s][0])) arquivos = _coletar_arquivos(caminhos) if not arquivos: @@ -161,11 +215,10 @@ def scan(caminhos, smells, como_json, fail_on_detect): "resumo": total_por_smell, "avisos": avisos}, ensure_ascii=False, indent=1, default=str)) else: + console.print(f"[dim]Analisando {len(arquivos)} arquivo(s) " + f"com {len(smells)} detector(es)…[/dim]") for aviso in avisos: console.print(f"[yellow]aviso:[/yellow] {aviso}") - if not resultados: - console.print(f"[green]Nenhum smell detectado[/green] " - f"({len(arquivos)} arquivo(s), {len(smells)} detector(es)).") for r in resultados: arvore = Tree(f"[bold]{r['arquivo']}[/bold]") for f in r["funcoes"]: @@ -175,8 +228,22 @@ def scan(caminhos, smells, como_json, fail_on_detect): no_fn.add(f"[red]{rid} {a['smell']}[/red] — " f"{_resumo_evidencia(a['smell'], a['evidence'])}") console.print(arvore) - if resultados: - tabela = Table(title="Resumo") + + total_deteccoes = sum(total_por_smell.values()) + if total_deteccoes == 0: + if filtrado: + quais = ", ".join(f"{SMELL_INFO[s][0]} ({s})" for s in smells) + console.print(f"[bold green]✓ Nenhum smell do tipo {quais} detectado[/bold green] " + f"em {len(arquivos)} arquivo(s).") + else: + console.print(f"[bold green]✓ Nenhum smell detectado[/bold green] " + f"em {len(arquivos)} arquivo(s).") + else: + n_funcoes = sum(len(r["funcoes"]) for r in resultados) + console.print(f"[bold red]✗ {total_deteccoes} smell(s)[/bold red] " + f"em {n_funcoes} função(ões) de {len(resultados)} arquivo(s) " + f"(de {len(arquivos)} analisado(s)).") + tabela = Table(title="Detecções por smell") tabela.add_column("Smell") tabela.add_column("Detecções", justify="right") for s in smells: diff --git a/tests/test_cli.py b/tests/test_cli.py index e4c24b7..9e04ba7 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -121,3 +121,100 @@ def test_comando_smells_lista_os_cinco(runner): for nome in ["long_method", "long_param_list", "magic_numbers", "deep_nesting", "dead_code"]: assert nome in res.output + + +# --------------------------------------------------------------------------- +# Filtro por ID do smell (R1–R5) — além do nome +# --------------------------------------------------------------------------- + +def test_scan_filtro_por_id_do_smell(runner, tmp_path): + """`--smell R2` deve filtrar exatamente como `--smell long_param_list`.""" + arq = _escrever(tmp_path, "smelly.py", CODIGO_COM_SMELLS) + res = runner.invoke(cli, ["scan", str(arq), "--smell", "R2", "--json"]) + assert res.exit_code == 0 + assert set(json.loads(res.output)["resumo"]) == {"long_param_list"} + + +def test_scan_filtro_por_id_case_insensitive(runner, tmp_path): + """O ID é aceito em minúsculas (`r2` == `R2`).""" + arq = _escrever(tmp_path, "smelly.py", CODIGO_COM_SMELLS) + res = runner.invoke(cli, ["scan", str(arq), "--smell", "r2", "--json"]) + assert res.exit_code == 0 + assert set(json.loads(res.output)["resumo"]) == {"long_param_list"} + + +def test_scan_id_e_nome_sao_deduplicados(runner, tmp_path): + """`--smell R2 --smell long_param_list` referem o mesmo smell → uma entrada.""" + arq = _escrever(tmp_path, "smelly.py", CODIGO_COM_SMELLS) + res = runner.invoke( + cli, ["scan", str(arq), "--smell", "R2", "--smell", "long_param_list", "--json"]) + assert res.exit_code == 0 + assert set(json.loads(res.output)["resumo"]) == {"long_param_list"} + + +def test_scan_smell_invalido_falha_com_mensagem(runner, tmp_path): + """Valor que não é ID nem nome → erro de uso (exit 2) com mensagem clara.""" + arq = _escrever(tmp_path, "smelly.py", CODIGO_COM_SMELLS) + res = runner.invoke(cli, ["scan", str(arq), "--smell", "R9"]) + assert res.exit_code == 2 + assert "não é um smell" in res.output + + +# --------------------------------------------------------------------------- +# Versionamento semântico (--version) +# --------------------------------------------------------------------------- + +def test_version_exibe_semver(runner): + """`--version` imprime a versão 1.0.0 e termina com sucesso.""" + res = runner.invoke(cli, ["--version"]) + assert res.exit_code == 0 + assert "1.0.0" in res.output + + +@pytest.mark.parametrize("flag", ["-V", "--version", "--V"]) +def test_version_aceita_todos_os_aliases(runner, flag): + """A versão é exibível por `-V`, `--version` e `--V`.""" + res = runner.invoke(cli, [flag]) + assert res.exit_code == 0 + assert "1.0.0" in res.output + + +# --------------------------------------------------------------------------- +# Linha de resumo / veredito +# --------------------------------------------------------------------------- + +def test_scan_veredito_quando_detecta(runner, tmp_path): + """Com smells, o veredito final informa a contagem total de detecções.""" + arq = _escrever(tmp_path, "smelly.py", CODIGO_COM_SMELLS) + res = runner.invoke(cli, ["scan", str(arq)]) + assert res.exit_code == 0 + assert "smell(s) em" in res.output + assert "função(ões)" in res.output + + +def test_scan_veredito_quando_limpo(runner, tmp_path): + """Sem smells e sem filtro, o veredito final é genérico.""" + arq = _escrever(tmp_path, "limpo.py", CODIGO_LIMPO) + res = runner.invoke(cli, ["scan", str(arq)]) + assert res.exit_code == 0 + assert "Nenhum smell detectado" in res.output + + +def test_scan_veredito_limpo_com_filtro_nomeia_o_smell(runner, tmp_path): + """Limpo com `--smell R2`, o veredito identifica o smell buscado.""" + arq = _escrever(tmp_path, "limpo.py", CODIGO_LIMPO) + res = runner.invoke(cli, ["scan", str(arq), "--smell", "R2"]) + assert res.exit_code == 0 + assert "Nenhum smell do tipo R2 (long_param_list) detectado" in res.output + + +# --------------------------------------------------------------------------- +# Documentação do --help +# --------------------------------------------------------------------------- + +def test_scan_help_documenta_codigos_de_saida(runner): + """O help do `scan` documenta os códigos de saída (0/1/2).""" + res = runner.invoke(cli, ["scan", "--help"]) + assert res.exit_code == 0 + assert "Códigos de saída" in res.output + assert "--fail-on-detect" in res.output From a9eb27330709f8ab37ec9e39dd6a21bcb91ba3ca Mon Sep 17 00:00:00 2001 From: Filipe Terra Date: Sun, 21 Jun 2026 12:37:38 -0300 Subject: [PATCH 2/4] =?UTF-8?q?reformula=C3=A7=C3=A3o=20readme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 165 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 121 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 81e086a..eaab117 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# TP — Mineração de Repositórios: Detecção e Refatoração de Code Smells em Python +# PySniff — Detector de Code Smells em Python por Mineração e Análise Estática -Trabalho Prático da disciplina **Engenharia de Software II** — UFMG. +Ferramenta de linha de comando da disciplina **Engenharia de Software II** — UFMG. --- -## 1. Membros do Grupo +## Membros do Grupo - Gustavo Dias Apolinário - Bernardo Vale dos Santos Bento @@ -12,18 +12,28 @@ Trabalho Prático da disciplina **Engenharia de Software II** — UFMG. --- -## 2. Sobre o Sistema +## Objetivo da Ferramenta -Ferramenta de linha de comando que **identifica 5 code smells em código Python por análise estática**, construída sobre um pipeline de **mineração de repositórios** que extrai pares reais de refatoração (antes→depois) do histórico de commits do GitHub. O projeto tem duas entregas complementares: +**PySniff** é uma ferramenta de linha de comando que ataca um problema de +**manutenção e evolução de software**: a presença de *code smells* — estruturas +de código que funcionam, mas dificultam manutenção, leitura e evolução. A +ferramenta aponta para um arquivo ou diretório Python e reporta, por +função/método, os smells encontrados, com a **evidência métrica** que justifica +cada detecção. -1. **A CLI de detecção** (`cli.py`) — produto do enunciado: aponta-se para um arquivo ou diretório Python e ela reporta, por função/método, os smells encontrados com a evidência métrica de cada um. -2. **O dataset minerado com qualidade medida** (repo privado `tp-es2-dataset`) — pares before→after por smell, validados por juiz LLM (Gemma) e auditados por **sonda de qualidade humana** (2 anotadores, amostra estratificada, precisão reponderada por taxa-base, Cohen κ) — insumo para o fine-tuning futuro de um modelo de refatoração (alvo: Stable Code Instruct 3B + QLoRA). +O detector é apoiado por um pipeline de **mineração de repositórios** (`extracao/`): +a partir do histórico de commits do GitHub, extraem-se pares reais de refatoração +(antes→depois) que sustentam a calibração dos critérios de detecção e alimentam a +trilha futura de fine-tuning de um modelo de refatoração. -> **Escopo real vs. proposta original:** a proposta inicial previa 12 smells e 11 LoRAs. O escopo executado — documentado com justificativas em `docs/DECISOES_PROJETO.md` — é de **5 smells com detecção por regras estáticas** e a trilha de fine-tuning condicionada à qualidade medida do dataset (a sonda de qualidade veio antes do treino, deliberadamente: *garbage in, garbage out*). +> **Escopo executado:** a proposta inicial previa 12 smells e 11 LoRAs. O escopo +> entregue — justificado em `docs/DECISOES_PROJETO.md` — é de **5 smells com +> detecção por regras estáticas** (a CLI deste repositório), com a trilha de +> fine-tuning condicionada à qualidade medida do dataset minerado. ### Os 5 smells cobertos -| ID | Smell | Refatoração-alvo | Detecção | +| ID | Smell | Refatoração-alvo | Critério de detecção | |---|---|---|---| | R1 | Long Method | Extract Method | Lizard (NLOC > 30 ou CCN > 10) | | R2 | Long Parameter List | Parameter Object | contagem AST (> 5 parâmetros) | @@ -33,59 +43,126 @@ Ferramenta de linha de comando que **identifica 5 code smells em código Python Cada detector expõe o contrato `detect(fn: FunctionInfo) -> DetectionResult` (`detectores/`). -## 3. Como usar a CLI +--- + +## Tecnologias Utilizadas + +- **CLI:** [Click](https://click.palletsprojects.com) (definição dos comandos) + + [Rich](https://github.com/Textualize/rich) (árvore e tabelas no terminal). +- **Análise estática:** `ast` (stdlib), [Lizard](https://github.com/terryyin/lizard) + (complexidade ciclomática e LOC). +- **Mineração de repositórios:** [PyDriller](https://github.com/ishepard/pydriller), + GitPython, PyGithub; Seart GHS para seleção de repositórios. +- **Dataset/juiz:** Gemma (juiz LLM local) e proxy de qualidade por padrão AST. +- **Testes:** [pytest](https://github.com/pytest-dev/pytest) — mais de 250 testes + cobrindo detectores, CLI e pipeline de mineração. +- **CI:** GitHub Actions (executa a suíte em Linux, macOS e Windows). +- **Trilha de fine-tuning (em desenvolvimento, `trilha_b/`):** HuggingFace + Transformers + PEFT (QLoRA). + +--- + +## Como Instalar + +Requer **Python 3.11+**. + +```bash +# 1. clone o repositório +git clone https://github.com/FilipeTerra/tp-mineracao-manutencao.git +cd tp-mineracao-manutencao + +# 2. crie e ative um ambiente virtual +python3 -m venv .venv +source .venv/bin/activate # Windows: .venv\Scripts\activate + +# 3. instale as dependências da ferramenta +pip install -r requirements_cli.txt +``` + +> Para usar **apenas a CLI de detecção** (sem o pipeline de mineração), basta o +> conjunto mínimo: `pip install lizard click rich`. + +--- + +## Como Utilizar + +A ferramenta tem dois comandos: `scan` (analisar código) e `smells` (listar o +que é detectado). ```bash -pip install -r detectores/requirements.txt click rich +# versão da ferramenta +python3 cli.py --version -# listar os smells suportados +# listar os 5 smells suportados python3 cli.py smells -# analisar um arquivo ou diretório (árvore Rich por arquivo → função → smell) +# analisar um arquivo ou diretório (árvore por arquivo → função → smell) python3 cli.py scan caminho/do/projeto/ -# restringir smells, saída JSON, ou uso em CI -python3 cli.py scan src/ --smell long_method --smell dead_code +# restringir a smells específicos — por ID (R1–R5) ou nome +python3 cli.py scan src/ --smell R1 --smell dead_code + +# saída JSON estruturada (para integração/scripts) python3 cli.py scan src/ --json > resultado.json -python3 cli.py scan src/ --fail-on-detect # exit 1 se detectar algo + +# uso em CI: retorna código de saída 1 se algum smell for detectado +python3 cli.py scan src/ --fail-on-detect ``` -## 4. Pipeline de mineração e dataset +**Códigos de saída do `scan`:** `0` análise concluída · `1` `--fail-on-detect` +ativo e houve detecção · `2` nenhum arquivo `.py` encontrado. Veja +`python3 cli.py scan --help` para todos os detalhes. + +--- -- `extracao/` minera pares de refatoração de repositórios Git (PyDriller): modos commit, PR, cross-file e rename, com validação por similaridade AST e checagem comportamental heurística. -- **Ancoragem em rule-ID de linter** (achado central do projeto): minerar commits que *removem* um aviso específico de linter (ex.: Pylint R0913, Ruff PLR2004) multiplica o yield de pares válidos em 5–25× vs. heurísticas de mensagem de commit — o yield é proporcional à **objetividade do critério** do smell. Detalhes e números em `docs/DECISOES_PROJETO.md`. -- 5.072 candidatos minerados → 616 pares aprovados pelo juiz LLM (Gemma) → **sonda de qualidade humana de 50 pares** (estratificada, cega ao proxy AST, precisão populacional reponderada por taxa-base, κ) + re-auditoria assistida por LLM. Resultados, codebook de anotação e decisão de curadoria vivem em `tp-es2-dataset` e `tp-es2-anotador/CODEBOOK.md`. -- **Oracle firewall:** PyRef e Sourcery são reservados para avaliação — nunca geram dados de treino; split treino/teste é por repositório. +## Como Executar os Testes Localmente -## 5. Tecnologias utilizadas +```bash +# instale as dependências de teste (além das da ferramenta) +pip install -r requirements_cli.txt -r requirements-dev.txt -- **Mineração:** PyDriller, GitPython, PyGithub (issues/PRs), Seart GHS para seleção de repositórios. -- **Análise estática:** `ast` (stdlib), Lizard, Pylint, Ruff, Vulture. -- **CLI:** Click + Rich. -- **Dataset/juiz:** Gemma (juiz LLM local), proxy de qualidade por padrão AST (`estimate_positive_quality.py`), anotador web próprio (`tp-es2-anotador`). -- **Testes:** pytest (243 testes — pipeline de mineração, detectores e CLI). -- **Trilha de fine-tuning (em desenvolvimento, `trilha_b/`):** HuggingFace Transformers + PEFT (QLoRA), alvo Stable Code Instruct 3B. +# rode a suíte completa a partir da raiz do repositório +python3 -m pytest -## 6. Estrutura do repositório +# com relatório de cobertura +python3 -m pytest --cov +``` -O projeto usa três repositórios, separados por papel: +Os testes também são executados **automaticamente a cada push e pull request** +via GitHub Actions — veja [`.github/workflows/python-app.yaml`](.github/workflows/python-app.yaml). -| Repositório | Papel | Visibilidade | -|---|---|---| -| **`tp-mineracao-manutencao`** (este) | código: mineração, detectores, CLI, trilha de treino | público | -| `tp-es2-anotador` | ferramenta web de anotação + codebook + scorer da sonda | público | -| `tp-es2-dataset` | dados: candidatos minerados, veredictos, anotações, sonda | privado | +--- -Layout do código: +## Pipeline de Mineração e Dataset + +- `extracao/` minera pares de refatoração de repositórios Git (PyDriller): modos + commit, PR, cross-file e rename, com validação por similaridade AST e checagem + comportamental heurística. +- **Ancoragem em rule-ID de linter** (achado central do projeto): minerar commits + que *removem* um aviso específico de linter (ex.: Pylint R0913, Ruff PLR2004) + multiplica o yield de pares válidos em 5–25× vs. heurísticas de mensagem de + commit. Detalhes em `docs/DECISOES_PROJETO.md`. +- **Oracle firewall:** PyRef e Sourcery são reservados para avaliação — nunca + geram dados de treino; o split treino/teste é por repositório. + +--- + +## Estrutura do Repositório + +``` +cli.py CLI de detecção (produto do enunciado: comandos scan e smells) +detectores/ os 5 detectores estáticos (detect(fn) -> DetectionResult) +extracao/ mineração de pares de refatoração via PyDriller +core/ tipos compartilhados, schema e oráculo de avaliação +trilha_b/ configuração e scripts de fine-tuning (QLoRA; não treinado) +docs/ enunciado, decisões de projeto e checkpoints +tests/ suíte pytest (rodar da raiz: python3 -m pytest) +.github/workflows GitHub Actions (CI) +``` -- `cli.py` — CLI de detecção (produto do enunciado). -- `detectores/` — os 5 detectores estáticos (`detect(fn) -> DetectionResult`). -- `extracao/` — mineração de pares de refatoração via PyDriller. -- `core/` — tipos compartilhados, schema e oráculo de avaliação. -- `trilha_b/` — configuração e scripts de fine-tuning (QLoRA; ainda não treinado). -- `gemma_judge_dataset.py`, `estimate_positive_quality.py` — juiz LLM e proxy de qualidade do dataset. -- `docs/` — enunciado, decisões de projeto datadas e checkpoints. -- `tests/` — suíte pytest (rodar da raiz: `python3 -m pytest`). +O projeto usa repositórios auxiliares separados por papel: `tp-es2-anotador` +(ferramenta web de anotação + codebook, público) e `tp-es2-dataset` (dados +minerados e anotações, privado). --- From 957261619b44f13af25da2b76d1debca0430b15b Mon Sep 17 00:00:00 2001 From: Filipe Terra Date: Sun, 21 Jun 2026 12:51:45 -0300 Subject: [PATCH 3/4] =?UTF-8?q?alter=C3=A7=C3=A3o=20de=20readme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 138 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 85 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index eaab117..0f3b85c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# PySniff — Detector de Code Smells em Python por Mineração e Análise Estática +# PySniff — Detecção e Refatoração de Code Smells em Python via Mineração de Repositórios -Ferramenta de linha de comando da disciplina **Engenharia de Software II** — UFMG. +Trabalho Prático da disciplina **Engenharia de Software II** — UFMG. --- @@ -17,19 +17,29 @@ Ferramenta de linha de comando da disciplina **Engenharia de Software II** — U **PySniff** é uma ferramenta de linha de comando que ataca um problema de **manutenção e evolução de software**: a presença de *code smells* — estruturas de código que funcionam, mas dificultam manutenção, leitura e evolução. A -ferramenta aponta para um arquivo ou diretório Python e reporta, por -função/método, os smells encontrados, com a **evidência métrica** que justifica -cada detecção. - -O detector é apoiado por um pipeline de **mineração de repositórios** (`extracao/`): -a partir do histórico de commits do GitHub, extraem-se pares reais de refatoração -(antes→depois) que sustentam a calibração dos critérios de detecção e alimentam a -trilha futura de fine-tuning de um modelo de refatoração. - -> **Escopo executado:** a proposta inicial previa 12 smells e 11 LoRAs. O escopo -> entregue — justificado em `docs/DECISOES_PROJETO.md` — é de **5 smells com -> detecção por regras estáticas** (a CLI deste repositório), com a trilha de -> fine-tuning condicionada à qualidade medida do dataset minerado. +ferramenta **identifica 5 code smells em código Python por análise estática**, +e é construída sobre um pipeline de **mineração de repositórios** que extrai +pares reais de refatoração (antes→depois) do histórico de commits do GitHub. + +O projeto tem **duas entregas complementares**: + +1. **A CLI de detecção** (`cli.py`) — produto do enunciado: aponta-se para um + arquivo ou diretório Python e ela reporta, por função/método, os smells + encontrados com a evidência métrica de cada um. +2. **O dataset minerado com qualidade medida** (repo privado `tp-es2-dataset`) — + pares before→after por smell, validados por juiz LLM (Gemma) e auditados por + **sonda de qualidade humana** (2 anotadores, amostra estratificada, precisão + reponderada por taxa-base, Cohen κ) — insumo para o **fine-tuning futuro de um + modelo de refatoração** (Trilha B; alvo: Stable Code Instruct 3B + QLoRA), + que proporá automaticamente as correções dos smells detectados. + +> **Escopo real vs. proposta original:** a proposta inicial previa 12 smells e +> 11 LoRAs. O escopo executado — documentado com justificativas em +> `docs/DECISOES_PROJETO.md` — é de **5 smells com detecção por regras estáticas** +> (a CLI deste repositório) e a trilha de fine-tuning condicionada à qualidade +> medida do dataset (a sonda de qualidade veio antes do treino, deliberadamente: +> *garbage in, garbage out*). A **Trilha B (refatoração) será continuada em +> breve.** ### Os 5 smells cobertos @@ -45,41 +55,24 @@ Cada detector expõe o contrato `detect(fn: FunctionInfo) -> DetectionResult` (` --- -## Tecnologias Utilizadas - -- **CLI:** [Click](https://click.palletsprojects.com) (definição dos comandos) + - [Rich](https://github.com/Textualize/rich) (árvore e tabelas no terminal). -- **Análise estática:** `ast` (stdlib), [Lizard](https://github.com/terryyin/lizard) - (complexidade ciclomática e LOC). -- **Mineração de repositórios:** [PyDriller](https://github.com/ishepard/pydriller), - GitPython, PyGithub; Seart GHS para seleção de repositórios. -- **Dataset/juiz:** Gemma (juiz LLM local) e proxy de qualidade por padrão AST. -- **Testes:** [pytest](https://github.com/pytest-dev/pytest) — mais de 250 testes - cobrindo detectores, CLI e pipeline de mineração. -- **CI:** GitHub Actions (executa a suíte em Linux, macOS e Windows). -- **Trilha de fine-tuning (em desenvolvimento, `trilha_b/`):** HuggingFace - Transformers + PEFT (QLoRA). - ---- - ## Como Instalar Requer **Python 3.11+**. ```bash # 1. clone o repositório -git clone https://github.com/FilipeTerra/tp-mineracao-manutencao.git +git clone https://github.com/Gronoxx/tp-mineracao-manutencao cd tp-mineracao-manutencao # 2. crie e ative um ambiente virtual python3 -m venv .venv -source .venv/bin/activate # Windows: .venv\Scripts\activate +source .venv/bin/activate # 3. instale as dependências da ferramenta pip install -r requirements_cli.txt ``` -> Para usar **apenas a CLI de detecção** (sem o pipeline de mineração), basta o +> Para usar **a CLI de detecção** > conjunto mínimo: `pip install lizard click rich`. --- @@ -96,7 +89,7 @@ python3 cli.py --version # listar os 5 smells suportados python3 cli.py smells -# analisar um arquivo ou diretório (árvore por arquivo → função → smell) +# analisar um arquivo ou diretório (árvore Rich por arquivo → função → smell) python3 cli.py scan caminho/do/projeto/ # restringir a smells específicos — por ID (R1–R5) ou nome @@ -128,8 +121,9 @@ python3 -m pytest python3 -m pytest --cov ``` -Os testes também são executados **automaticamente a cada push e pull request** -via GitHub Actions — veja [`.github/workflows/python-app.yaml`](.github/workflows/python-app.yaml). +A suíte tem **mais de 250 testes** (detectores, CLI e pipeline de mineração) e é +executada **automaticamente a cada push e pull request** via GitHub Actions — +veja [`.github/workflows/python-app.yaml`](.github/workflows/python-app.yaml). --- @@ -141,28 +135,66 @@ via GitHub Actions — veja [`.github/workflows/python-app.yaml`](.github/workfl - **Ancoragem em rule-ID de linter** (achado central do projeto): minerar commits que *removem* um aviso específico de linter (ex.: Pylint R0913, Ruff PLR2004) multiplica o yield de pares válidos em 5–25× vs. heurísticas de mensagem de - commit. Detalhes em `docs/DECISOES_PROJETO.md`. + commit — o yield é proporcional à **objetividade do critério** do smell. + Detalhes e números em `docs/DECISOES_PROJETO.md`. +- 5.072 candidatos minerados → 616 pares aprovados pelo juiz LLM (Gemma) → + **sonda de qualidade humana de 50 pares** (estratificada, cega ao proxy AST, + precisão populacional reponderada por taxa-base, κ) + re-auditoria assistida + por LLM. Resultados, codebook de anotação e decisão de curadoria vivem em + `tp-es2-dataset` e `tp-es2-anotador/CODEBOOK.md`. - **Oracle firewall:** PyRef e Sourcery são reservados para avaliação — nunca geram dados de treino; o split treino/teste é por repositório. +### Trilha B — Fine-tuning de refatoração (em desenvolvimento) + +O dataset minerado e auditado alimenta a **Trilha B**: o treino de um modelo que +proporá automaticamente a correção dos smells detectados. A infraestrutura vive +em `trilha_b/` (configuração e scripts de fine-tuning com QLoRA; alvo +**Stable Code Instruct 3B**). O modelo ainda não foi treinado — esta trilha será +continuada em breve. + --- -## Estrutura do Repositório +## Tecnologias Utilizadas -``` -cli.py CLI de detecção (produto do enunciado: comandos scan e smells) -detectores/ os 5 detectores estáticos (detect(fn) -> DetectionResult) -extracao/ mineração de pares de refatoração via PyDriller -core/ tipos compartilhados, schema e oráculo de avaliação -trilha_b/ configuração e scripts de fine-tuning (QLoRA; não treinado) -docs/ enunciado, decisões de projeto e checkpoints -tests/ suíte pytest (rodar da raiz: python3 -m pytest) -.github/workflows GitHub Actions (CI) -``` +- **CLI:** [Click](https://click.palletsprojects.com) (definição dos comandos) + + [Rich](https://github.com/Textualize/rich) (árvore e tabelas no terminal). +- **Análise estática:** `ast` (stdlib), [Lizard](https://github.com/terryyin/lizard), + Pylint, Ruff, Vulture. +- **Mineração:** PyDriller, GitPython, PyGithub (issues/PRs), Seart GHS para + seleção de repositórios. +- **Dataset/juiz:** Gemma (juiz LLM local), proxy de qualidade por padrão AST + (`estimate_positive_quality.py`), anotador web próprio (`tp-es2-anotador`). +- **Testes:** [pytest](https://github.com/pytest-dev/pytest) — mais de 250 testes + cobrindo detectores, CLI e pipeline de mineração. +- **CI:** GitHub Actions (executa a suíte em Linux, macOS e Windows). +- **Trilha de fine-tuning (em desenvolvimento, `trilha_b/`):** HuggingFace + Transformers + PEFT (QLoRA), alvo Stable Code Instruct 3B. + +--- + +## Estrutura do Repositório -O projeto usa repositórios auxiliares separados por papel: `tp-es2-anotador` -(ferramenta web de anotação + codebook, público) e `tp-es2-dataset` (dados -minerados e anotações, privado). +O projeto usa três repositórios, separados por papel: + +| Repositório | Papel | Visibilidade | +|---|---|---| +| **`tp-mineracao-manutencao`** (este) | código: mineração, detectores, CLI, trilha de treino | público | +| `tp-es2-anotador` | ferramenta web de anotação + codebook + scorer da sonda | público | +| `tp-es2-dataset` | dados: candidatos minerados, veredictos, anotações, sonda | privado | + +Layout do código: + +- `cli.py` — CLI de detecção (produto do enunciado: comandos `scan` e `smells`). +- `detectores/` — os 5 detectores estáticos (`detect(fn) -> DetectionResult`). +- `extracao/` — mineração de pares de refatoração via PyDriller. +- `core/` — tipos compartilhados, schema e oráculo de avaliação. +- `trilha_b/` — configuração e scripts de fine-tuning (QLoRA; ainda não treinado). +- `gemma_judge_dataset.py`, `estimate_positive_quality.py` — juiz LLM e proxy de + qualidade do dataset. +- `docs/` — enunciado, decisões de projeto datadas e checkpoints. +- `tests/` — suíte pytest (rodar da raiz: `python3 -m pytest`). +- `.github/workflows/` — GitHub Actions (CI). --- From 8ec4283124d52da8c8d17acd9303a0b20e4e1376 Mon Sep 17 00:00:00 2001 From: Gronoxx Date: Sun, 21 Jun 2026 13:38:57 -0300 Subject: [PATCH 4/4] =?UTF-8?q?docs(readme):=20corrige=20frase=20truncada?= =?UTF-8?q?=20e=20nota=20de=20vers=C3=A3o=20testada=20no=20CI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - "Para usar apenas a CLI..." estava cortada; completa a frase (conjunto mínimo lizard/click/rich). - Python 3.11+ agora nota que o CI testa 3.13 em Linux/macOS/Windows. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0f3b85c..a1d211b 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ Cada detector expõe o contrato `detect(fn: FunctionInfo) -> DetectionResult` (` ## Como Instalar -Requer **Python 3.11+**. +Requer **Python 3.11+** (testado em CI com 3.13 em Linux, macOS e Windows). ```bash # 1. clone o repositório @@ -72,8 +72,8 @@ source .venv/bin/activate pip install -r requirements_cli.txt ``` -> Para usar **a CLI de detecção** -> conjunto mínimo: `pip install lizard click rich`. +> Para usar **apenas a CLI de detecção** (sem o pipeline de mineração), o +> conjunto mínimo de dependências é: `pip install lizard click rich`. ---