plugadvpl 0.3.2__tar.gz → 0.3.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/PKG-INFO +1 -1
  2. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/plugadvpl/_version.py +2 -2
  3. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/plugadvpl/db.py +2 -2
  4. plugadvpl-0.3.4/plugadvpl/lookups/lint_rules.json +376 -0
  5. plugadvpl-0.3.4/plugadvpl/migrations/003_lint_rules_status.sql +18 -0
  6. plugadvpl-0.3.4/tests/unit/test_lint_catalog_consistency.py +150 -0
  7. plugadvpl-0.3.2/plugadvpl/lookups/lint_rules.json +0 -317
  8. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/.gitignore +0 -0
  9. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/README.md +0 -0
  10. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/plugadvpl/__init__.py +0 -0
  11. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/plugadvpl/__main__.py +0 -0
  12. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/plugadvpl/cli.py +0 -0
  13. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/plugadvpl/ingest.py +0 -0
  14. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/plugadvpl/ingest_sx.py +0 -0
  15. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/plugadvpl/lookups/.gitkeep +0 -0
  16. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/plugadvpl/lookups/funcoes_nativas.json +0 -0
  17. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/plugadvpl/lookups/funcoes_restritas.json +0 -0
  18. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/plugadvpl/lookups/modulos_erp.json +0 -0
  19. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/plugadvpl/lookups/pontos_entrada_padrao.json +0 -0
  20. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/plugadvpl/lookups/sql_macros.json +0 -0
  21. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/plugadvpl/migrations/001_initial.sql +0 -0
  22. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/plugadvpl/migrations/002_universo2_sx.sql +0 -0
  23. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/plugadvpl/output.py +0 -0
  24. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/plugadvpl/parsing/.gitkeep +0 -0
  25. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/plugadvpl/parsing/__init__.py +0 -0
  26. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/plugadvpl/parsing/lint.py +0 -0
  27. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/plugadvpl/parsing/parser.py +0 -0
  28. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/plugadvpl/parsing/stripper.py +0 -0
  29. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/plugadvpl/parsing/sx_csv.py +0 -0
  30. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/plugadvpl/query.py +0 -0
  31. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/plugadvpl/scan.py +0 -0
  32. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/pyproject.toml +0 -0
  33. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/__init__.py +0 -0
  34. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/bench/.gitkeep +0 -0
  35. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/bench/__init__.py +0 -0
  36. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/bench/test_ingest_perf.py +0 -0
  37. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/bench/test_sx_ingest_perf.py +0 -0
  38. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/e2e_local/.gitkeep +0 -0
  39. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/e2e_local/__init__.py +0 -0
  40. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/e2e_local/test_e2e_local_ingest.py +0 -0
  41. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/e2e_local/test_ingest_sx_real.py +0 -0
  42. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/fixtures/expected/.gitkeep +0 -0
  43. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/fixtures/sx_synthetic/six.csv +0 -0
  44. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/fixtures/sx_synthetic/sx1.csv +0 -0
  45. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/fixtures/sx_synthetic/sx2.csv +0 -0
  46. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/fixtures/sx_synthetic/sx3.csv +0 -0
  47. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/fixtures/sx_synthetic/sx5.csv +0 -0
  48. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/fixtures/sx_synthetic/sx6.csv +0 -0
  49. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/fixtures/sx_synthetic/sx7.csv +0 -0
  50. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/fixtures/sx_synthetic/sx9.csv +0 -0
  51. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/fixtures/sx_synthetic/sxa.csv +0 -0
  52. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/fixtures/sx_synthetic/sxb.csv +0 -0
  53. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/fixtures/sx_synthetic/sxg.csv +0 -0
  54. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/fixtures/synthetic/.gitkeep +0 -0
  55. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/fixtures/synthetic/_generate.py +0 -0
  56. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/fixtures/synthetic/classic_browse.prw +0 -0
  57. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/fixtures/synthetic/corrupted.bak +0 -0
  58. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/fixtures/synthetic/empty.prw +0 -0
  59. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/fixtures/synthetic/encoding_cp1252.prw +0 -0
  60. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/fixtures/synthetic/encoding_utf8.prw +0 -0
  61. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/fixtures/synthetic/exec_auto.prw +0 -0
  62. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/fixtures/synthetic/http_outbound.prw +0 -0
  63. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/fixtures/synthetic/huge.prw +0 -0
  64. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/fixtures/synthetic/job_rpc.prw +0 -0
  65. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/fixtures/synthetic/multi_filial.prw +0 -0
  66. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/fixtures/synthetic/mvc_complete.prw +0 -0
  67. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/fixtures/synthetic/mvc_hooks.prw +0 -0
  68. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/fixtures/synthetic/pe_simple.prw +0 -0
  69. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/fixtures/synthetic/pubvars.prw +0 -0
  70. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/fixtures/synthetic/reclock_pattern.prw +0 -0
  71. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/fixtures/synthetic/reclock_unbalanced.prw +0 -0
  72. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/fixtures/synthetic/sql_embedded.prw +0 -0
  73. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/fixtures/synthetic/tlpp_namespace.tlpp +0 -0
  74. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/fixtures/synthetic/ws_rest.tlpp +0 -0
  75. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/fixtures/synthetic/ws_soap.prw +0 -0
  76. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/integration/.gitkeep +0 -0
  77. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/integration/__init__.py +0 -0
  78. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/integration/test_cli.py +0 -0
  79. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/integration/test_ingest.py +0 -0
  80. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/integration/test_ingest_sx.py +0 -0
  81. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/unit/.gitkeep +0 -0
  82. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/unit/__snapshots__/test_parser_snapshots.ambr +0 -0
  83. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/unit/test_db.py +0 -0
  84. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/unit/test_lint.py +0 -0
  85. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/unit/test_output.py +0 -0
  86. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/unit/test_parser.py +0 -0
  87. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/unit/test_parser_snapshots.py +0 -0
  88. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/unit/test_query.py +0 -0
  89. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/unit/test_scan.py +0 -0
  90. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/tests/unit/test_stripper.py +0 -0
  91. {plugadvpl-0.3.2 → plugadvpl-0.3.4}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plugadvpl
3
- Version: 0.3.2
3
+ Version: 0.3.4
4
4
  Summary: CLI que indexa fontes ADVPL/Protheus em SQLite com FTS5 para análise por LLM (companheiro do plugin Claude Code plugadvpl)
5
5
  Project-URL: Homepage, https://github.com/JoniPraia/plugadvpl
6
6
  Project-URL: Issues, https://github.com/JoniPraia/plugadvpl/issues
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.3.2'
22
- __version_tuple__ = version_tuple = (0, 3, 2)
21
+ __version__ = version = '0.3.4'
22
+ __version_tuple__ = version_tuple = (0, 3, 4)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -11,7 +11,7 @@ from typing import TYPE_CHECKING
11
11
  if TYPE_CHECKING:
12
12
  from pathlib import Path
13
13
 
14
- SCHEMA_VERSION = "2"
14
+ SCHEMA_VERSION = "3"
15
15
 
16
16
 
17
17
  # Mapeamento {filename JSON -> (tabela, colunas em ordem)}.
@@ -34,7 +34,7 @@ _LOOKUP_FILES: dict[str, tuple[str, list[str]]] = {
34
34
  "lint_rules",
35
35
  [
36
36
  "regra_id", "titulo", "severidade", "categoria", "descricao",
37
- "fix_guidance", "detection_kind",
37
+ "fix_guidance", "detection_kind", "status", "impl_function",
38
38
  ],
39
39
  ),
40
40
  "sql_macros.json": (
@@ -0,0 +1,376 @@
1
+ [
2
+ {
3
+ "regra_id": "BP-001",
4
+ "titulo": "RecLock sem MsUnlock pareado no mesmo escopo de função",
5
+ "severidade": "critical",
6
+ "categoria": "best-practice",
7
+ "descricao": "Toda chamada `RecLock` deve ter um `MsUnlock` correspondente no mesmo escopo lógico. Esquecer o unlock mantém o registro travado até a sessão morrer, bloqueando outros usuários e gerando deadlocks. A regra detecta `RecLock` cuja contrapartida `MsUnlock` não aparece no mesmo bloco/função (incluindo branches de erro).",
8
+ "fix_guidance": "Pareie `RecLock` com `MsUnlock` em todos os caminhos (sucesso e erro). Em fluxos com risco, use `Begin Transaction`/`End Transaction` (rollback automático faz unlock).",
9
+ "detection_kind": "regex",
10
+ "status": "active",
11
+ "impl_function": "_check_bp001_reclock_unbalanced"
12
+ },
13
+ {
14
+ "regra_id": "BP-002",
15
+ "titulo": "BEGIN TRANSACTION sem END TRANSACTION pareado dentro do mesmo escopo",
16
+ "severidade": "critical",
17
+ "categoria": "best-practice",
18
+ "descricao": "Conta opens vs closes de `BEGIN TRANSACTION` por função; se opens > closes, reporta os opens extras. Sem `End Transaction`, em caso de exceção fica estado inconsistente (master sem detail, etc.); atomicidade quebrada.",
19
+ "fix_guidance": "Pareie `Begin Transaction` com `End Transaction`. Use `DisarmTransaction()` antes do `Break` em `Recover`. NUNCA misture funções de manutenção AdvPL básicas com Framework dentro do mesmo bloco.",
20
+ "detection_kind": "regex",
21
+ "status": "active",
22
+ "impl_function": "_check_bp002_transaction_unbalanced"
23
+ },
24
+ {
25
+ "regra_id": "BP-002b",
26
+ "titulo": "Variáveis declaradas como Private ou Public em vez de Local",
27
+ "severidade": "warning",
28
+ "categoria": "best-practice",
29
+ "descricao": "Variáveis devem ser declaradas como `Local` exceto quando há necessidade explícita de escopo mais amplo. `Private` e `Public` poluem a call stack e podem ser sobrescritas acidentalmente por funções chamadas.",
30
+ "fix_guidance": "Substitua `Private`/`Public` por `Local` quando possível. Usos legítimos de `Private`: passar variáveis para models MVC ou frameworks legados que esperam por elas (`MV_PAR*`).",
31
+ "detection_kind": "regex",
32
+ "status": "planned"
33
+ },
34
+ {
35
+ "regra_id": "BP-003",
36
+ "titulo": "MsExecAuto sem checar lMsErroAuto nas linhas seguintes",
37
+ "severidade": "error",
38
+ "categoria": "best-practice",
39
+ "descricao": "Após chamar `MsExecAuto`, é obrigatório checar `lMsErroAuto` nas próximas N linhas. Sem isso, erro silencioso: rotina executou com falha, fluxo segue como se OK. Bug em massa em ETLs e jobs de inclusão automática.",
40
+ "fix_guidance": "Declare `Private lMsErroAuto := .F.` antes da chamada. Logo após `MsExecAuto(...)`, faça `If lMsErroAuto; MostraErro() ...; Return .F.; EndIf` ou capture via `GetAutoGRLog()`.",
41
+ "detection_kind": "regex",
42
+ "status": "active",
43
+ "impl_function": "_check_bp003_msexecauto_no_check"
44
+ },
45
+ {
46
+ "regra_id": "BP-004",
47
+ "titulo": "Pergunte(...) sem uso subsequente de MV_PAR*",
48
+ "severidade": "warning",
49
+ "categoria": "best-practice",
50
+ "descricao": "Detecta `Pergunte(\"GRUPO\", .F./.T.)` sem nenhum uso de `MV_PAR01..MV_PARxx` nas linhas seguintes. Sintoma: chamou Pergunte mas não usou as respostas, ou usou as antigas em memória. Frequentemente é cópia-cola esquecida.",
51
+ "fix_guidance": "Use `MV_PAR01..MV_PARxx` após o `Pergunte`, OU remova a chamada se não for necessária. Para fluxos novos, prefira `ParamBox()` (Code Analysis aprova; SX1 está deprecated). Em Pergunte aninhado, salve/restaure o estado das MV_PAR.",
52
+ "detection_kind": "regex",
53
+ "status": "active",
54
+ "impl_function": "_check_bp004_pergunte_no_check"
55
+ },
56
+ {
57
+ "regra_id": "BP-005",
58
+ "titulo": "Função declarada com mais de 6 parâmetros",
59
+ "severidade": "warning",
60
+ "categoria": "best-practice",
61
+ "descricao": "Code smell universal: funções com mais de 6 parâmetros indicam acoplamento excessivo, dificultam testes e são propensas a bug por ordem de args trocada.",
62
+ "fix_guidance": "Refatore: agrupe params relacionados em objeto/array, divida em funções menores com responsabilidades específicas, ou use named params via JSON.",
63
+ "detection_kind": "regex",
64
+ "status": "active",
65
+ "impl_function": "_check_bp005_too_many_params"
66
+ },
67
+ {
68
+ "regra_id": "BP-006",
69
+ "titulo": "Mistura RecLock + dbAppend()/DbRLock raw na mesma função",
70
+ "severidade": "error",
71
+ "categoria": "best-practice",
72
+ "descricao": "TOTVS proíbe: APIs básicas de manutenção (`dbAppend`, `dbDelete`, `dbRecall`) NÃO podem ser misturadas com Framework (`RecLock`, `MsUnlock`) — semântica de lock conflita, integridade transacional quebra.",
73
+ "fix_guidance": "Use APENAS Framework (RecLock+MsUnlock) dentro de fluxo ERP Protheus. APIs raw (dbAppend etc.) só fora de ambiente Protheus (utilitários offline, scripts de migração).",
74
+ "detection_kind": "regex",
75
+ "status": "active",
76
+ "impl_function": "_check_bp006_mixed_reclock_rawapi"
77
+ },
78
+ {
79
+ "regra_id": "BP-007",
80
+ "titulo": "Função sem header Protheus.doc",
81
+ "severidade": "info",
82
+ "categoria": "best-practice",
83
+ "descricao": "Funções (User Function, Static Function, Method) deveriam ter header Protheus.doc com `@type`, `@author`, `@since`, `@param`, `@return`. Falta de doc dificulta manutenção e impede geração automática de documentação.",
84
+ "fix_guidance": "Adicione bloco `/*/{Protheus.doc} <NOME>` ... `/*/` antes da função, com pelo menos `@type function`, `@author`, `@since`.",
85
+ "detection_kind": "regex",
86
+ "status": "planned"
87
+ },
88
+ {
89
+ "regra_id": "BP-008",
90
+ "titulo": "Shadowing de variável reservada (cFilAnt, cEmpAnt, PARAMIXB, etc.)",
91
+ "severidade": "critical",
92
+ "categoria": "best-practice",
93
+ "descricao": "Declarar variável Local/Static/Private com nome de Public reservada do framework (`cFilAnt`, `cEmpAnt`, `cUserName`, `PARAMIXB`, `aRotina`, `lMsErroAuto`, `__cInternet`, `nUsado`, etc.) faz shadowing — sua função enxerga `\"\"`/`Nil` em vez do valor real do framework.",
94
+ "fix_guidance": "Renomeie a variável local pra algo distinto (prefixo cliente). NUNCA crie Local/Private/Public com o mesmo nome de uma reservada framework.",
95
+ "detection_kind": "regex",
96
+ "status": "planned"
97
+ },
98
+ {
99
+ "regra_id": "SEC-001",
100
+ "titulo": "RpcSetEnv dentro de classe que herda de WSRESTFUL",
101
+ "severidade": "critical",
102
+ "categoria": "security",
103
+ "descricao": "Em REST, o framework já entrega o ambiente Protheus via autenticação HTTP + `PrepareIn` configurado no AppServer.ini. `RpcSetEnv` em endpoint REST bypassa o controle de empresa/filial via TenantId, vaza credenciais hardcoded e mata auditoria — não há rastro de quem realmente fez a operação.",
104
+ "fix_guidance": "Remova `RpcSetEnv`/`RpcClearEnv` do endpoint REST. Configure `PrepareIn=99,01` + `Security=1` no `[HTTPURI]` do appserver.ini. Cliente passa empresa/filial no header `TenantId`. Veja `[[advpl-webservice]]`.",
105
+ "detection_kind": "regex",
106
+ "status": "active",
107
+ "impl_function": "_check_sec001_rpcsetenv_in_restful"
108
+ },
109
+ {
110
+ "regra_id": "SEC-002",
111
+ "titulo": "User Function sem prefixo cliente (2-3 letras) ou padrão PE oficial",
112
+ "severidade": "warning",
113
+ "categoria": "security",
114
+ "descricao": "User Functions devem começar com prefixo institucional (ex: `XYZNomeFn`) para evitar colisão com customizações de outros parceiros instaladas no mesmo RPO. Exceção: nomes de PE oficiais TOTVS (`MT100LOK`, `MA440PGN`, etc.) seguem padrão fixado pela TOTVS.",
115
+ "fix_guidance": "Adicione prefixo de 2-3 letras do cliente (`XYZ`, `MGF`, etc.) ao nome. Para PEs, mantenha o nome fixo TOTVS — exceção justificada.",
116
+ "detection_kind": "regex",
117
+ "status": "active",
118
+ "impl_function": "_check_sec002_user_function_no_prefix"
119
+ },
120
+ {
121
+ "regra_id": "SEC-003",
122
+ "titulo": "Dados sensíveis (PII/credenciais) em ConOut ou FwLogMsg",
123
+ "severidade": "warning",
124
+ "categoria": "security",
125
+ "descricao": "Logs com CPF/CNPJ/senha/token vão pro `console.log` do AppServer e ficam visíveis a qualquer um com acesso ao servidor. Vazamento de PII viola LGPD.",
126
+ "fix_guidance": "Mascarar dados sensíveis antes de logar (`***.***.***-XX` para CPF). NUNCA logue senha, token, ou body REST cru com PII. Use `FwLogMsg` com category específica que possa ser desabilitada em produção.",
127
+ "detection_kind": "regex",
128
+ "status": "planned"
129
+ },
130
+ {
131
+ "regra_id": "SEC-004",
132
+ "titulo": "Credenciais hardcoded em código fonte",
133
+ "severidade": "warning",
134
+ "categoria": "security",
135
+ "descricao": "Senhas, tokens, connection strings hardcoded no .prw são vazadas ao versionamento git, expostas a qualquer dev com acesso ao repo. Particularmente crítico em Jobs (`RpcSetEnv(\"01\", \"0101\", \"admin\", \"totvs\")`).",
136
+ "fix_guidance": "Mova credenciais para `MV_*` em SX6 (encrypted via `EncryptPwd`), variável de ambiente, ou cofre dedicado. Em JOB, leia via `SuperGetMV(\"MV_XYZUSR\")` em runtime.",
137
+ "detection_kind": "regex",
138
+ "status": "planned"
139
+ },
140
+ {
141
+ "regra_id": "SEC-005",
142
+ "titulo": "Uso de função TOTVS restrita/interna (lookup funcoes_restritas)",
143
+ "severidade": "critical",
144
+ "categoria": "security",
145
+ "descricao": "TOTVS mantém ~195 funções/classes/variáveis internas que NÃO devem ser usadas em customização: não documentadas, não suportadas, podem ser removidas/alteradas sem aviso. Algumas têm compilação bloqueada desde 12.1.33.",
146
+ "fix_guidance": "Substitua por equivalente público documentado em TDN. Consulte tabela `funcoes_restritas` (cataloga as 195 conhecidas). Use `/plugadvpl:find function <nome>` para checar se uma função é restrita.",
147
+ "detection_kind": "lookup",
148
+ "status": "planned"
149
+ },
150
+ {
151
+ "regra_id": "PERF-001",
152
+ "titulo": "SELECT * em BeginSql ou TCQuery",
153
+ "severidade": "warning",
154
+ "categoria": "performance",
155
+ "descricao": "`SELECT *` traz todas as colunas mesmo quando você precisa de 3 — desperdiça banda BD↔AppServer, força full row scan em alguns planos do otimizador. Bug colateral: em `BeginSql`, `*` como primeiro caractere de linha é interpretado como comentário pelo pré-processador.",
156
+ "fix_guidance": "Liste explicitamente as colunas necessárias: `SELECT F2_DOC, F2_SERIE, F2_EMISSAO FROM ...`. Combine com `TCSetField` para tipagem pós-query.",
157
+ "detection_kind": "regex",
158
+ "status": "active",
159
+ "impl_function": "_check_perf001_select_star"
160
+ },
161
+ {
162
+ "regra_id": "PERF-002",
163
+ "titulo": "SQL contra tabela Protheus sem %notDel% (registros deletados aparecem)",
164
+ "severidade": "error",
165
+ "categoria": "performance",
166
+ "descricao": "Protheus usa soft-delete via coluna `D_E_L_E_T_`. Query sem filtro `%notDel%` traz registros logicamente apagados, gerando bug em totais, contagens e listagens. Em `BeginSql`, a macro expande para `<alias>.D_E_L_E_T_ = ' '` em runtime.",
167
+ "fix_guidance": "Adicione `<alias>.%notDel%` no `WHERE` para CADA tabela ADVPL no `FROM`/`JOIN`. Ex: `AND SA1.%notDel%` + `AND SD1.%notDel%`.",
168
+ "detection_kind": "regex",
169
+ "status": "active",
170
+ "impl_function": "_check_perf002_no_notdel"
171
+ },
172
+ {
173
+ "regra_id": "PERF-003",
174
+ "titulo": "SQL contra tabela Protheus sem %xfilial% (cross-filial data leak)",
175
+ "severidade": "error",
176
+ "categoria": "performance",
177
+ "descricao": "Cross-filial leak: usuário da filial 01 enxerga dados da filial 02 quando query não filtra por filial. Bug grave em ambientes compartilhados (afeta segurança + performance).",
178
+ "fix_guidance": "Adicione `WHERE <alias>.<TBL>_FILIAL = %xfilial:<TBL>%` para cada tabela filializada. Ex: `WHERE SA1.A1_FILIAL = %xfilial:SA1%`. Tabelas com `X2_MODO='C'` (compartilhada) não precisam — `xFilial` retorna vazio nesses casos.",
179
+ "detection_kind": "regex",
180
+ "status": "active",
181
+ "impl_function": "_check_perf003_no_xfilial"
182
+ },
183
+ {
184
+ "regra_id": "PERF-004",
185
+ "titulo": "Concatenação de string com + ou += em loop",
186
+ "severidade": "warning",
187
+ "categoria": "performance",
188
+ "descricao": "Em ADVPL, string é imutável. Cada `cVar += \"x\"` aloca string nova, copia conteúdo antigo + \"x\", descarta o anterior. 1.000 concatenações = 1.000 alocações, ~500.000 chars copiados. Comportamento O(n²).",
189
+ "fix_guidance": "Acumule em array e faça `FwArrayJoin()` (R26+) ou `Array2String()` (legacy) no final. Comportamento vira O(n).",
190
+ "detection_kind": "regex",
191
+ "status": "planned"
192
+ },
193
+ {
194
+ "regra_id": "PERF-005",
195
+ "titulo": "RecCount() > 0 para checar existência (use !Eof())",
196
+ "severidade": "warning",
197
+ "categoria": "performance",
198
+ "descricao": "`RecCount()` força full scan do alias para contar todos os registros. Para apenas verificar se existe pelo menos 1 registro, `!Eof()` após `DbSeek` ou `DbGoTop` é O(1).",
199
+ "fix_guidance": "Substitua `If RecCount() > 0` por `If !Eof()` ou `If !<alias>->(Eof())`. Em SQL, use `SELECT COUNT(*) FROM ... LIMIT 1` ou subquery EXISTS.",
200
+ "detection_kind": "regex",
201
+ "status": "planned"
202
+ },
203
+ {
204
+ "regra_id": "PERF-006",
205
+ "titulo": "Query com WHERE/ORDER BY em coluna sem índice (cross-file SIX)",
206
+ "severidade": "info",
207
+ "categoria": "performance",
208
+ "descricao": "WHERE/ORDER BY em coluna fora dos índices SIX da tabela força full table scan. Particularmente caro em tabelas grandes (SF1, SD2, SE1, SF3).",
209
+ "fix_guidance": "Adicione índice custom no SIX (`INDICE >= 21`) ou refatore query para usar coluna indexada. Use `/plugadvpl:tables <T>` para ver índices existentes.",
210
+ "detection_kind": "cross-file",
211
+ "status": "planned"
212
+ },
213
+ {
214
+ "regra_id": "MOD-001",
215
+ "titulo": "ConOut(...) em vez de FwLogMsg(...) (Code Analysis acusa)",
216
+ "severidade": "warning",
217
+ "categoria": "modernization",
218
+ "descricao": "`ConOut` é flagged no Code Analysis TOTVS — saída sem estrutura, dificulta análise de log centralizado. `FwLogMsg` é estruturado (syslog-style: severity/transactionId/group/category/step/msgId) e pode ir pro servidor de logs centralizado quando habilitado via `FWLOGMSG_DEBUG` no appserver.ini.",
219
+ "fix_guidance": "Substitua `ConOut(\"INFO: msg\")` por `FwLogMsg(\"INFO\", \"TX-001\", \"MGFFAT\", \"REL\", \"Step1\", \"OK\")`. Para debug rápido em desenvolvimento, ConOut ainda é OK.",
220
+ "detection_kind": "regex",
221
+ "status": "active",
222
+ "impl_function": "_check_mod001_conout_instead_fwlogmsg"
223
+ },
224
+ {
225
+ "regra_id": "MOD-002",
226
+ "titulo": "Declaração Public (polui escopo global)",
227
+ "severidade": "warning",
228
+ "categoria": "modernization",
229
+ "descricao": "Variáveis Public poluem o escopo global, dificultam debug (qualquer função pode mexer), causam vazamento de estado entre threads. NUNCA declare Public — só TOTVS framework cria (`cFilAnt`, `cEmpAnt`, etc.).",
230
+ "fix_guidance": "Use `Local` (95% dos casos) ou `Static` (constante por fonte). `Private` só para padrões legados específicos (`MV_PAR*` injetadas por `Pergunte`).",
231
+ "detection_kind": "regex",
232
+ "status": "active",
233
+ "impl_function": "_check_mod002_public_declaration"
234
+ },
235
+ {
236
+ "regra_id": "MOD-003",
237
+ "titulo": "Funções com prefixo comum candidatas a virar classe",
238
+ "severidade": "info",
239
+ "categoria": "modernization",
240
+ "descricao": "Conjuntos de Static Function com mesmo prefixo (`_AppCalc`, `_AppFmt`, `_AppGet`) e operando sobre os mesmos dados são candidatos a refatoração para `Class ... Method`. OOP melhora encapsulamento e testabilidade.",
241
+ "fix_guidance": "Identifique funções com prefixo comum + dados compartilhados. Refatore para `Class TApp` com `Data` (dados) + `Method` (operações). Em TLPP, use `class` com modificadores `public/private/protected`.",
242
+ "detection_kind": "cross-file",
243
+ "status": "planned"
244
+ },
245
+ {
246
+ "regra_id": "MOD-004",
247
+ "titulo": "Uso de AxCadastro/Modelo2/Modelo3 em vez de MVC",
248
+ "severidade": "info",
249
+ "categoria": "modernization",
250
+ "descricao": "`AxCadastro`, `Modelo2`, `Modelo3` são wrappers procedurais legados (R10/R11) que não plugam no framework MVC moderno (eventos, validações cruzadas, FWMVCRotAuto pra testes automatizados). Customizações novas em R12+ devem ser MVC.",
251
+ "fix_guidance": "Refatore para padrão MVC: `MenuDef()` + `ModelDef()` + `ViewDef()` + `FWMBrowse`. Veja `[[advpl-mvc]]` e `[[advpl-refactoring]]` padrão 4.",
252
+ "detection_kind": "regex",
253
+ "status": "planned"
254
+ },
255
+ {
256
+ "regra_id": "SX-001",
257
+ "titulo": "X3_VALID chama User Function (U_xxx) que não existe nos fontes indexados",
258
+ "severidade": "warning",
259
+ "categoria": "best-practice",
260
+ "descricao": "Validação SX3 (`X3_VALID = \"U_XYZVALID()\"`) referencia User Function não encontrada nos fontes do projeto. Validação quebrada em runtime — campo aceita qualquer valor.",
261
+ "fix_guidance": "Crie a User Function referenciada nos fontes ou ajuste o `X3_VALID` para função existente. Requer `/plugadvpl:ingest` (fontes) + `/plugadvpl:ingest-sx` (dicionário) antes.",
262
+ "detection_kind": "cross-file",
263
+ "status": "active",
264
+ "impl_function": "_check_sx001_x3_valid_unknown_func"
265
+ },
266
+ {
267
+ "regra_id": "SX-002",
268
+ "titulo": "SX7 X7_CDOMIN aponta pra campo que não existe em SX3 (campos)",
269
+ "severidade": "error",
270
+ "categoria": "best-practice",
271
+ "descricao": "Gatilho SX7 com destino (`X7_CDOMIN`) referenciando campo que não existe na SX3. Em runtime, gatilho dispara erro ou silenciosamente falha sem atualizar nada.",
272
+ "fix_guidance": "Apague o gatilho ou crie o campo destino na SX3. Verifique também typos no `X7_CDOMIN`.",
273
+ "detection_kind": "cross-file",
274
+ "status": "active",
275
+ "impl_function": "_check_sx002_x7_destino_inexistente"
276
+ },
277
+ {
278
+ "regra_id": "SX-003",
279
+ "titulo": "Parâmetro SX6 (MV_*) declarado mas zero referências em fontes",
280
+ "severidade": "warning",
281
+ "categoria": "modernization",
282
+ "descricao": "Parâmetro custom `MV_*` cadastrado no SX6 mas nenhum fonte do projeto faz `GetMV`/`SuperGetMV` por ele. Parâmetro morto ocupa configurador, confunde admin.",
283
+ "fix_guidance": "Remova o parâmetro do SX6 (script com `PutMV` revertendo) ou use onde foi planejado. Se intencional para uso externo, documente em `X6_DESCRIC`.",
284
+ "detection_kind": "cross-file",
285
+ "status": "active",
286
+ "impl_function": "_check_sx003_mv_param_unused_in_fontes"
287
+ },
288
+ {
289
+ "regra_id": "SX-004",
290
+ "titulo": "Grupo SX1 sem Pergunte() correspondente em nenhum fonte",
291
+ "severidade": "warning",
292
+ "categoria": "modernization",
293
+ "descricao": "Grupo de pergunta (`X1_GRUPO`) cadastrado no SX1 mas nenhum fonte faz `Pergunte(\"GRUPO\", ...)`. Pergunta órfã, ocupa cadastro sem uso.",
294
+ "fix_guidance": "Remova o grupo do SX1 ou implemente o uso. Se intencional para uso futuro, marque com X1_DESCR explicativo.",
295
+ "detection_kind": "cross-file",
296
+ "status": "active",
297
+ "impl_function": "_check_sx004_pergunta_unused_in_fontes"
298
+ },
299
+ {
300
+ "regra_id": "SX-005",
301
+ "titulo": "Campo SX3 custom (X3_PROPRI='U') sem referências em fontes/SX/SX7",
302
+ "severidade": "info",
303
+ "categoria": "modernization",
304
+ "descricao": "Campo custom (`X3_PROPRI = 'U'`) sem nenhuma referência em fontes, validações de outros campos, ou regras de gatilhos. Provável legado, ocupa espaço em cadastro e polui browse.",
305
+ "fix_guidance": "Considere remover do SX3 (script de delete) ou implementar uso pendente. Se intencional para futura customização, documente.",
306
+ "detection_kind": "cross-file",
307
+ "status": "active",
308
+ "impl_function": "_check_sx005_campo_usado_zero_refs"
309
+ },
310
+ {
311
+ "regra_id": "SX-006",
312
+ "titulo": "X3_VALID faz BeginSql/TCQuery (anti-pattern de performance)",
313
+ "severidade": "warning",
314
+ "categoria": "performance",
315
+ "descricao": "Validação X3_VALID com SQL embarcado executa query a CADA Loose Focus do campo. Em telas com muitos campos validados, custa segundos de UI per row.",
316
+ "fix_guidance": "Substitua por `ExistCpo(<alias>, <chave>, <ordem>)` ou `Posicione()` com cache. Se precisa de SQL complexo, mova para User Function que cacheia resultado em `Static`.",
317
+ "detection_kind": "cross-file",
318
+ "status": "active",
319
+ "impl_function": "_check_sx006_perf_sql_in_x3_valid"
320
+ },
321
+ {
322
+ "regra_id": "SX-007",
323
+ "titulo": "X3_VALID chama função listada em funcoes_restritas TOTVS",
324
+ "severidade": "critical",
325
+ "categoria": "security",
326
+ "descricao": "Validação SX3 chama função interna/restrita TOTVS (lookup `funcoes_restritas`). Quebra em release-bump da TOTVS, não suportada, pode ser removida sem aviso.",
327
+ "fix_guidance": "Substitua por equivalente público documentado em TDN. Veja `/plugadvpl:find function <nome>` para confirmar se é restrita.",
328
+ "detection_kind": "cross-file",
329
+ "status": "active",
330
+ "impl_function": "_check_sx007_restricted_func_in_x3_valid"
331
+ },
332
+ {
333
+ "regra_id": "SX-008",
334
+ "titulo": "Tabela compartilhada (X2_MODO='C') usa xFilial em X3_VALID",
335
+ "severidade": "warning",
336
+ "categoria": "best-practice",
337
+ "descricao": "Tabela com `X2_MODO='C'` (compartilhada entre filiais) tem campo SX3 cuja `X3_VALID` usa `xFilial(...)`. Inconsistência: tabela compartilhada não filia, mas validação filtra. Resulta em comportamento imprevisível dependendo da filial ativa.",
338
+ "fix_guidance": "Remova `xFilial` do `X3_VALID` (tabela é compartilhada, valor de filial não importa) OU mude `X2_MODO` para `E` (Exclusiva) se a tabela realmente filia.",
339
+ "detection_kind": "cross-file",
340
+ "status": "active",
341
+ "impl_function": "_check_sx008_xfilial_in_modo_c"
342
+ },
343
+ {
344
+ "regra_id": "SX-009",
345
+ "titulo": "Campo obrigatório (X3_OBRIGAT='X') com X3_INIT vazio/zero",
346
+ "severidade": "warning",
347
+ "categoria": "best-practice",
348
+ "descricao": "Campo marcado como obrigatório mas com inicializador (`X3_INIT`) que retorna vazio (`\"\"`, `Space(N)`, `0`, `Nil`, `.F.`). Usuário sempre vai precisar redigitar. UX ruim.",
349
+ "fix_guidance": "Inicialize com valor sensato (ex: `\"01\"` para campo de filial, `Date()` para data atual, `cFilAnt` para filial). Se realmente não há default, documente em `X3_DESCRIC`.",
350
+ "detection_kind": "cross-file",
351
+ "status": "active",
352
+ "impl_function": "_check_sx009_obrigat_with_empty_init"
353
+ },
354
+ {
355
+ "regra_id": "SX-010",
356
+ "titulo": "Gatilho SX7 X7_TIPO='P' (Pesquisar) sem X7_SEEK='S' válido",
357
+ "severidade": "error",
358
+ "categoria": "best-practice",
359
+ "descricao": "Gatilho do tipo Pesquisar (P) requer `X7_SEEK='S'` para indicar que faz `DbSeek` no alias antes de avaliar a regra. Sem isso, runtime falha ou retorna valor errado silenciosamente.",
360
+ "fix_guidance": "Marque `X7_SEEK='S'` e configure `X7_ALIAS`/`X7_ORDEM`/`X7_CHAVE`. Ou troque `X7_TIPO` para `S` (Secundário) se não precisa de SEEK.",
361
+ "detection_kind": "cross-file",
362
+ "status": "active",
363
+ "impl_function": "_check_sx010_pesquisar_sem_seek"
364
+ },
365
+ {
366
+ "regra_id": "SX-011",
367
+ "titulo": "X3_F3 aponta pra alias SXB (consulta) que não existe",
368
+ "severidade": "error",
369
+ "categoria": "best-practice",
370
+ "descricao": "Campo SX3 com `X3_F3 = \"SA1XYZ\"` referenciando consulta SXB inexistente. F3 (lupa) quebra em runtime quando usuário pressiona F3 no campo.",
371
+ "fix_guidance": "Crie a consulta na SXB (4 tipos de registro: 1/2/3/4) ou remova o `X3_F3`. Verifique typo no alias.",
372
+ "detection_kind": "cross-file",
373
+ "status": "active",
374
+ "impl_function": "_check_sx011_x3_f3_consulta_inexistente"
375
+ }
376
+ ]
@@ -0,0 +1,18 @@
1
+ -- Migration 003 — adiciona colunas status e impl_function em lint_rules.
2
+ --
3
+ -- Contexto: lookups/lint_rules.json passou a documentar (a) quais regras estão
4
+ -- realmente implementadas em parsing/lint.py vs. quais estão catalogadas-mas-não-
5
+ -- detectadas, e (b) qual função _check_<id>_<tópico> implementa cada regra ativa.
6
+ -- Antes desta migration o catálogo e a impl divergiam (issue #1) — após esta
7
+ -- migration o teste de regressão tests/integration/test_lint_catalog_consistency.py
8
+ -- protege contra novo drift.
9
+ --
10
+ -- ALTER TABLE ADD COLUMN é não-destrutivo em SQLite (registros existentes recebem
11
+ -- o DEFAULT). Re-executar não falha pois o block é envolvido em IF NOT EXISTS-
12
+ -- semantic via PRAGMA table_info check no orquestrador da migration.
13
+
14
+ ALTER TABLE lint_rules ADD COLUMN status TEXT DEFAULT 'active';
15
+ ALTER TABLE lint_rules ADD COLUMN impl_function TEXT DEFAULT '';
16
+
17
+ -- Index opcional pra queries do tipo "liste todas as planned"
18
+ CREATE INDEX IF NOT EXISTS idx_lint_rules_status ON lint_rules(status);
@@ -0,0 +1,150 @@
1
+ """Garante que lookups/lint_rules.json e parsing/lint.py não voltem a divergir.
2
+
3
+ Issue #1 documentou drift histórico (10 severidades + 15 títulos diferentes
4
+ entre catálogo e implementação) corrigido em v0.3.4. Este teste é guardião:
5
+ qualquer commit futuro que adicione/remova/renomeie uma regra precisa atualizar
6
+ ambas as fontes de verdade ou o teste falha.
7
+
8
+ Estratégia:
9
+
10
+ 1. Lê o catálogo lookups/lint_rules.json (35 regras esperadas, 24 active + 11 planned).
11
+ 2. Extrai os docstrings das funções _check_* em parsing/lint.py via regex —
12
+ procura o padrão `def _check_<id>_<topico>(...):` seguido de docstring iniciando
13
+ com `<ID>-<n> (<severity>):` (ver constante DEF_RE abaixo).
14
+ 3. Para cada regra_id que aparece em ambas as fontes:
15
+
16
+ - severidade do catálogo deve bater com severidade do docstring impl
17
+ - catálogo deve marcar a regra como status='active'
18
+ - catálogo deve apontar impl_function para o nome real da função
19
+
20
+ 4. Para cada regra status='active' no catálogo, deve existir a _check_* correspondente.
21
+ 5. Para cada regra status='planned' no catálogo, NÃO deve existir _check_*
22
+ (caso contrário a regra foi implementada e o status precisa ser atualizado).
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import json
27
+ import re
28
+ from importlib import resources as ir
29
+ from pathlib import Path
30
+
31
+ import pytest
32
+
33
+ # Mesma regex usada no audit script tmp/audit_lint_drift.py — extrai (fn_name, id, severity, summary).
34
+ DEF_RE = re.compile(
35
+ r'^def\s+(_check_[a-z0-9_]+)\s*\([^)]*\)[^:]*:\s*\n\s+"""'
36
+ r'([A-Z]+-[0-9]+[a-z]?)\s+\(([a-z]+)\):\s*([^\n]+)',
37
+ re.MULTILINE,
38
+ )
39
+
40
+
41
+ @pytest.fixture(scope="module")
42
+ def catalog() -> dict[str, dict]:
43
+ """Carrega lookups/lint_rules.json indexado por regra_id."""
44
+ text = ir.files("plugadvpl").joinpath("lookups/lint_rules.json").read_text(
45
+ encoding="utf-8"
46
+ )
47
+ rules = json.loads(text)
48
+ return {r["regra_id"]: r for r in rules}
49
+
50
+
51
+ @pytest.fixture(scope="module")
52
+ def impl() -> dict[str, dict[str, str]]:
53
+ """Extrai dict[regra_id] = {fn, severidade, summary} dos docstrings de lint.py."""
54
+ text = ir.files("plugadvpl").joinpath("parsing/lint.py").read_text(encoding="utf-8")
55
+ out: dict[str, dict[str, str]] = {}
56
+ for m in DEF_RE.finditer(text):
57
+ fn_name, regra_id, severity, summary = m.groups()
58
+ out[regra_id] = {
59
+ "fn": fn_name,
60
+ "severidade": severity.lower(),
61
+ "summary": summary.strip().rstrip(".").strip(),
62
+ }
63
+ return out
64
+
65
+
66
+ def test_catalog_has_expected_total(catalog: dict) -> None:
67
+ """Sanity check: número total de regras catalogadas (24 active + 11 planned = 35)."""
68
+ assert len(catalog) == 35, (
69
+ f"esperava 35 regras catalogadas, encontrou {len(catalog)}. "
70
+ "Se adicionou/removeu regra intencional, atualize este teste."
71
+ )
72
+
73
+
74
+ def test_active_count_matches_impl(catalog: dict, impl: dict) -> None:
75
+ """24 regras active no catálogo devem bater com 24 funções _check_* no impl."""
76
+ n_active = sum(1 for r in catalog.values() if r.get("status") == "active")
77
+ assert n_active == 24, f"esperava 24 active, encontrou {n_active}"
78
+ assert len(impl) == 24, (
79
+ f"esperava 24 funções _check_* em lint.py, encontrou {len(impl)}. "
80
+ "Se adicionou/removeu detector, atualize este teste e o catálogo."
81
+ )
82
+
83
+
84
+ def test_active_rules_have_impl_function(catalog: dict, impl: dict) -> None:
85
+ """Toda regra active no catálogo precisa ter `impl_function` apontando pra fn real."""
86
+ for rid, rule in catalog.items():
87
+ if rule.get("status") != "active":
88
+ continue
89
+ assert rule.get("impl_function"), (
90
+ f"regra {rid} marcada como active mas sem impl_function no catálogo"
91
+ )
92
+ assert rid in impl, (
93
+ f"regra {rid} marcada active no catálogo mas não há _check_* "
94
+ f"correspondente em lint.py"
95
+ )
96
+ assert rule["impl_function"] == impl[rid]["fn"], (
97
+ f"regra {rid}: catálogo aponta impl_function='{rule['impl_function']}' "
98
+ f"mas a função real é '{impl[rid]['fn']}'"
99
+ )
100
+
101
+
102
+ def test_severity_matches_between_catalog_and_impl(catalog: dict, impl: dict) -> None:
103
+ """Severidade do catálogo deve bater com severidade declarada no docstring impl."""
104
+ drifts = []
105
+ for rid, impl_data in impl.items():
106
+ if rid not in catalog:
107
+ drifts.append(f"{rid}: existe em lint.py mas NÃO no catálogo")
108
+ continue
109
+ cat_sev = catalog[rid].get("severidade", "?")
110
+ if cat_sev != impl_data["severidade"]:
111
+ drifts.append(
112
+ f"{rid}: catálogo='{cat_sev}' impl='{impl_data['severidade']}'"
113
+ )
114
+ assert not drifts, "Drift de severidade detectado:\n - " + "\n - ".join(drifts)
115
+
116
+
117
+ def test_planned_rules_have_no_impl(catalog: dict, impl: dict) -> None:
118
+ """Regras planned no catálogo NÃO devem ter _check_* — se tiver, marque como active."""
119
+ leaks = []
120
+ for rid, rule in catalog.items():
121
+ if rule.get("status") == "planned" and rid in impl:
122
+ leaks.append(
123
+ f"{rid}: marcada planned mas existe {impl[rid]['fn']} em lint.py — "
124
+ "mude status para 'active' e adicione impl_function"
125
+ )
126
+ assert not leaks, "Regras planned com impl real:\n - " + "\n - ".join(leaks)
127
+
128
+
129
+ def test_planned_rules_have_no_impl_function(catalog: dict) -> None:
130
+ """Regras planned não devem ter `impl_function` setado (deve ser '' ou ausente)."""
131
+ for rid, rule in catalog.items():
132
+ if rule.get("status") == "planned":
133
+ assert not rule.get("impl_function"), (
134
+ f"regra {rid} marcada planned mas tem impl_function='{rule['impl_function']}' "
135
+ "— remova o campo ou marque como active"
136
+ )
137
+
138
+
139
+ def test_all_rules_have_required_fields(catalog: dict) -> None:
140
+ """Schema mínimo: toda regra precisa de regra_id/titulo/severidade/categoria/descricao/status."""
141
+ required = ["regra_id", "titulo", "severidade", "categoria", "descricao", "status"]
142
+ for rid, rule in catalog.items():
143
+ for field in required:
144
+ assert field in rule, f"regra {rid} sem campo obrigatório '{field}'"
145
+ assert rule["status"] in ("active", "planned"), (
146
+ f"regra {rid}: status='{rule['status']}' inválido (use 'active' ou 'planned')"
147
+ )
148
+ assert rule["severidade"] in ("critical", "error", "warning", "info"), (
149
+ f"regra {rid}: severidade='{rule['severidade']}' inválida"
150
+ )