plugadvpl 0.4.2__tar.gz → 0.4.3__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.
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/PKG-INFO +1 -1
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/plugadvpl/_version.py +2 -2
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/plugadvpl/cli.py +21 -2
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/plugadvpl/db.py +1 -1
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/plugadvpl/lookups/execauto_routines.json +50 -0
- plugadvpl-0.4.3/plugadvpl/migrations/008_universo3_funcao_indexes.sql +11 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/plugadvpl/parsing/protheus_doc.py +26 -12
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/plugadvpl/parsing/triggers.py +116 -14
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/plugadvpl/query.py +29 -4
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/integration/test_cli.py +33 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/unit/test_execauto.py +18 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/unit/test_protheus_doc.py +76 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/unit/test_triggers.py +66 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/.gitignore +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/README.md +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/plugadvpl/__init__.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/plugadvpl/__main__.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/plugadvpl/ingest.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/plugadvpl/ingest_sx.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/plugadvpl/lookups/.gitkeep +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/plugadvpl/lookups/funcoes_nativas.json +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/plugadvpl/lookups/funcoes_restritas.json +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/plugadvpl/lookups/lint_rules.json +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/plugadvpl/lookups/modulos_erp.json +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/plugadvpl/lookups/pontos_entrada_padrao.json +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/plugadvpl/lookups/sql_macros.json +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/plugadvpl/migrations/001_initial.sql +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/plugadvpl/migrations/002_universo2_sx.sql +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/plugadvpl/migrations/003_lint_rules_status.sql +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/plugadvpl/migrations/004_consultas_pk_with_tipo.sql +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/plugadvpl/migrations/005_universo3_execution_triggers.sql +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/plugadvpl/migrations/006_universo3_execauto_calls.sql +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/plugadvpl/migrations/007_universo3_protheus_docs.sql +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/plugadvpl/output.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/plugadvpl/parsing/.gitkeep +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/plugadvpl/parsing/__init__.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/plugadvpl/parsing/execauto.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/plugadvpl/parsing/lint.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/plugadvpl/parsing/parser.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/plugadvpl/parsing/stripper.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/plugadvpl/parsing/sx_csv.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/plugadvpl/scan.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/pyproject.toml +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/__init__.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/bench/.gitkeep +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/bench/__init__.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/bench/test_ingest_perf.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/bench/test_sx_ingest_perf.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/e2e_local/.gitkeep +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/e2e_local/__init__.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/e2e_local/test_e2e_local_ingest.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/e2e_local/test_ingest_sx_real.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/fixtures/expected/.gitkeep +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/fixtures/sx_synthetic/six.csv +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/fixtures/sx_synthetic/sx1.csv +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/fixtures/sx_synthetic/sx2.csv +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/fixtures/sx_synthetic/sx3.csv +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/fixtures/sx_synthetic/sx5.csv +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/fixtures/sx_synthetic/sx6.csv +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/fixtures/sx_synthetic/sx7.csv +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/fixtures/sx_synthetic/sx9.csv +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/fixtures/sx_synthetic/sxa.csv +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/fixtures/sx_synthetic/sxb.csv +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/fixtures/sx_synthetic/sxb_with_collisions.csv +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/fixtures/sx_synthetic/sxg.csv +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/fixtures/synthetic/.gitkeep +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/fixtures/synthetic/_generate.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/fixtures/synthetic/classic_browse.prw +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/fixtures/synthetic/corrupted.bak +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/fixtures/synthetic/empty.prw +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/fixtures/synthetic/encoding_cp1252.prw +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/fixtures/synthetic/encoding_utf8.prw +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/fixtures/synthetic/exec_auto.prw +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/fixtures/synthetic/http_outbound.prw +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/fixtures/synthetic/huge.prw +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/fixtures/synthetic/job_rpc.prw +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/fixtures/synthetic/multi_filial.prw +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/fixtures/synthetic/mvc_complete.prw +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/fixtures/synthetic/mvc_hooks.prw +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/fixtures/synthetic/pe_paramixb.prw +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/fixtures/synthetic/pe_simple.prw +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/fixtures/synthetic/pubvars.prw +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/fixtures/synthetic/reclock_alias_dup_trigger.prw +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/fixtures/synthetic/reclock_pattern.prw +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/fixtures/synthetic/reclock_unbalanced.prw +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/fixtures/synthetic/sql_embedded.prw +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/fixtures/synthetic/tlpp_namespace.tlpp +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/fixtures/synthetic/ws_rest.tlpp +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/fixtures/synthetic/ws_restful_classic.prw +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/fixtures/synthetic/ws_soap.prw +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/integration/.gitkeep +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/integration/__init__.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/integration/test_ingest.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/integration/test_ingest_sx.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/unit/.gitkeep +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/unit/__snapshots__/test_parser_snapshots.ambr +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/unit/test_db.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/unit/test_lint.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/unit/test_lint_catalog_consistency.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/unit/test_output.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/unit/test_parser.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/unit/test_parser_snapshots.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/unit/test_query.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/unit/test_scan.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/tests/unit/test_stripper.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.3}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: plugadvpl
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.3
|
|
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.4.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 4,
|
|
21
|
+
__version__ = version = '0.4.3'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 4, 3)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -60,6 +60,7 @@ from plugadvpl.query import (
|
|
|
60
60
|
execauto_calls_query,
|
|
61
61
|
execution_triggers_query,
|
|
62
62
|
find_any,
|
|
63
|
+
protheus_doc_homonyms,
|
|
63
64
|
protheus_doc_show,
|
|
64
65
|
protheus_docs_orphans,
|
|
65
66
|
protheus_docs_query,
|
|
@@ -1305,10 +1306,28 @@ def docs(
|
|
|
1305
1306
|
formatado em Markdown. Use ``--orphans`` pra ver funções sem header.
|
|
1306
1307
|
"""
|
|
1307
1308
|
if show:
|
|
1308
|
-
|
|
1309
|
-
|
|
1309
|
+
# v0.4.3 (I2): com homônimos, --arquivo desambiguar; sem --arquivo,
|
|
1310
|
+
# avisa em stderr e mostra o primeiro alfabeticamente.
|
|
1311
|
+
homonyms = _with_ro_db(ctx, lambda c: protheus_doc_homonyms(c, show))
|
|
1312
|
+
if not homonyms:
|
|
1310
1313
|
typer.echo(f"Nenhum Protheus.doc encontrado pra função '{show}'.", err=True)
|
|
1311
1314
|
raise typer.Exit(code=1)
|
|
1315
|
+
if len(homonyms) > 1 and not arquivo:
|
|
1316
|
+
typer.echo(
|
|
1317
|
+
f"Aviso: '{show}' tem doc em {len(homonyms)} fontes: "
|
|
1318
|
+
f"{', '.join(homonyms)}. Mostrando '{homonyms[0]}'. "
|
|
1319
|
+
f"Use --arquivo <nome> pra escolher.",
|
|
1320
|
+
err=True,
|
|
1321
|
+
)
|
|
1322
|
+
d = _with_ro_db(
|
|
1323
|
+
ctx, lambda c: protheus_doc_show(c, show, arquivo=arquivo)
|
|
1324
|
+
)
|
|
1325
|
+
if d is None:
|
|
1326
|
+
typer.echo(
|
|
1327
|
+
f"Nenhum Protheus.doc encontrado pra '{show}' em '{arquivo}'.",
|
|
1328
|
+
err=True,
|
|
1329
|
+
)
|
|
1330
|
+
raise typer.Exit(code=1)
|
|
1312
1331
|
typer.echo(render_pdoc_markdown(d))
|
|
1313
1332
|
return
|
|
1314
1333
|
|
|
@@ -16,6 +16,16 @@
|
|
|
16
16
|
"source_url": "https://tdn.totvs.com",
|
|
17
17
|
"verified": true
|
|
18
18
|
},
|
|
19
|
+
{
|
|
20
|
+
"routine": "MATA020",
|
|
21
|
+
"module": "SIGACOM",
|
|
22
|
+
"type": "cadastro",
|
|
23
|
+
"label": "Cadastro de Fornecedores",
|
|
24
|
+
"tables_primary": ["SA2"],
|
|
25
|
+
"tables_secondary": [],
|
|
26
|
+
"source_url": "https://tdn.totvs.com",
|
|
27
|
+
"verified": true
|
|
28
|
+
},
|
|
19
29
|
{
|
|
20
30
|
"routine": "MATA030",
|
|
21
31
|
"module": "SIGAFIN",
|
|
@@ -26,6 +36,16 @@
|
|
|
26
36
|
"source_url": "https://tdn.totvs.com",
|
|
27
37
|
"verified": true
|
|
28
38
|
},
|
|
39
|
+
{
|
|
40
|
+
"routine": "MATA040",
|
|
41
|
+
"module": "SIGAFIN",
|
|
42
|
+
"type": "cadastro",
|
|
43
|
+
"label": "Cadastro de Bancos",
|
|
44
|
+
"tables_primary": ["SA6"],
|
|
45
|
+
"tables_secondary": [],
|
|
46
|
+
"source_url": "https://tdn.totvs.com",
|
|
47
|
+
"verified": true
|
|
48
|
+
},
|
|
29
49
|
{
|
|
30
50
|
"routine": "MATA050",
|
|
31
51
|
"module": "SIGAFAT",
|
|
@@ -66,6 +86,16 @@
|
|
|
66
86
|
"source_url": "https://tdn.totvs.com/pages/viewpage.action?pageId=318605213",
|
|
67
87
|
"verified": true
|
|
68
88
|
},
|
|
89
|
+
{
|
|
90
|
+
"routine": "MATA112",
|
|
91
|
+
"module": "SIGAFIN",
|
|
92
|
+
"type": "cadastro",
|
|
93
|
+
"label": "Plano de Pagamento",
|
|
94
|
+
"tables_primary": ["SE4"],
|
|
95
|
+
"tables_secondary": [],
|
|
96
|
+
"source_url": "https://tdn.totvs.com",
|
|
97
|
+
"verified": false
|
|
98
|
+
},
|
|
69
99
|
{
|
|
70
100
|
"routine": "MATA120",
|
|
71
101
|
"module": "SIGACOM",
|
|
@@ -166,6 +196,26 @@
|
|
|
166
196
|
"source_url": "https://tdn.totvs.com/pages/releaseview.action?pageId=6784012",
|
|
167
197
|
"verified": true
|
|
168
198
|
},
|
|
199
|
+
{
|
|
200
|
+
"routine": "FATA010",
|
|
201
|
+
"module": "SIGAFAT",
|
|
202
|
+
"type": "cadastro",
|
|
203
|
+
"label": "Cadastro de Bandeira de Cartao",
|
|
204
|
+
"tables_primary": ["AE1"],
|
|
205
|
+
"tables_secondary": [],
|
|
206
|
+
"source_url": "https://tdn.totvs.com",
|
|
207
|
+
"verified": false
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
"routine": "FATA050",
|
|
211
|
+
"module": "SIGAFAT",
|
|
212
|
+
"type": "movimento",
|
|
213
|
+
"label": "Liberacao de Pedidos de Venda",
|
|
214
|
+
"tables_primary": ["SC9"],
|
|
215
|
+
"tables_secondary": [],
|
|
216
|
+
"source_url": "https://tdn.totvs.com",
|
|
217
|
+
"verified": false
|
|
218
|
+
},
|
|
169
219
|
{
|
|
170
220
|
"routine": "MATA460",
|
|
171
221
|
"module": "SIGAFAT",
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
-- v0.4.3 (I6) — indices em `funcao` nas 3 tabelas Universo 3.
|
|
2
|
+
-- Antes: queries cross-ref ("quais funcoes no fonte X chamam ExecAuto?")
|
|
3
|
+
-- forcavam scan + filter Python. Agora idx cobre `funcao` nas 3 tables.
|
|
4
|
+
|
|
5
|
+
CREATE INDEX IF NOT EXISTS idx_exec_funcao
|
|
6
|
+
ON execution_triggers(funcao);
|
|
7
|
+
|
|
8
|
+
CREATE INDEX IF NOT EXISTS idx_execauto_funcao
|
|
9
|
+
ON execauto_calls(funcao);
|
|
10
|
+
|
|
11
|
+
-- protheus_docs ja tem idx_pdoc_funcao (criado em migration 007), nao precisa.
|
|
@@ -30,8 +30,11 @@ _PDOC_BLOCK_RE = re.compile(
|
|
|
30
30
|
r"[ \t]*" # SEM \s — id, se houver, fica na MESMA linha do opening
|
|
31
31
|
r"(?P<id>[\w:]+)?"
|
|
32
32
|
r"(?P<body>.*?)"
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
# v0.4.3 (C2): fechamento ANCORADO a start-of-line. `/*/` em meio a comentário
|
|
34
|
+
# de @example não fecha bloco (padrão oficial TOTVS — fechamento fica sozinho
|
|
35
|
+
# na própria linha).
|
|
36
|
+
r"^[ \t]*/\*/[ \t]*$",
|
|
37
|
+
re.IGNORECASE | re.DOTALL | re.MULTILINE,
|
|
35
38
|
)
|
|
36
39
|
|
|
37
40
|
# Próxima decl de função/método após o fechamento.
|
|
@@ -95,15 +98,13 @@ def infer_module(arquivo: str, funcao: str | None) -> str | None:
|
|
|
95
98
|
# 1. Exact match (rotina exata no catálogo).
|
|
96
99
|
if funcao_upper in idx:
|
|
97
100
|
return idx[funcao_upper]["module"]
|
|
98
|
-
# 2. Prefix match (4 primeiros chars).
|
|
99
|
-
#
|
|
101
|
+
# 2. Prefix match (4 primeiros chars). v0.4.3 (C5): só aceita se TODOS
|
|
102
|
+
# os matches do prefixo apontam pro MESMO módulo. Ambiguidade → None
|
|
103
|
+
# (não inventar). Antes retornava SIGAEST silenciosamente para MATA*.
|
|
100
104
|
prefix4 = funcao_upper[:4]
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
)
|
|
105
|
-
if matches:
|
|
106
|
-
return matches[0]["module"]
|
|
105
|
+
matched_modules = {e["module"] for k, e in idx.items() if k.startswith(prefix4)}
|
|
106
|
+
if len(matched_modules) == 1:
|
|
107
|
+
return matched_modules.pop()
|
|
107
108
|
return None
|
|
108
109
|
|
|
109
110
|
|
|
@@ -200,15 +201,28 @@ def _line_at(content: str, offset: int) -> int:
|
|
|
200
201
|
return content.count("\n", 0, offset) + 1
|
|
201
202
|
|
|
202
203
|
|
|
204
|
+
_PDOC_ORPHAN_LINE_CAP = 80 # v0.4.3 (C4): cap de proximidade pra associar bloco→decl
|
|
205
|
+
|
|
206
|
+
|
|
203
207
|
def _resolve_next_decl(
|
|
204
208
|
content: str, after_offset: int
|
|
205
209
|
) -> tuple[str | None, int | None]:
|
|
206
|
-
"""Acha próxima decl de função/método após offset. Retorna (nome, linha_1based).
|
|
210
|
+
"""Acha próxima decl de função/método após offset. Retorna (nome, linha_1based).
|
|
211
|
+
|
|
212
|
+
v0.4.3 (C4): cap de ``_PDOC_ORPHAN_LINE_CAP`` linhas entre o offset (fim do
|
|
213
|
+
bloco) e a decl encontrada. Acima disso retorna (None, None) — bloco é
|
|
214
|
+
tratado como órfão (preserva sinal de cobertura BP-007 e impede que função
|
|
215
|
+
distante ganhe doc errada associada).
|
|
216
|
+
"""
|
|
207
217
|
m = _NEXT_DECL_RE.search(content, after_offset)
|
|
208
218
|
if not m:
|
|
209
219
|
return None, None
|
|
220
|
+
block_end_line = _line_at(content, max(0, after_offset - 1))
|
|
221
|
+
decl_line = _line_at(content, m.start())
|
|
222
|
+
if decl_line - block_end_line > _PDOC_ORPHAN_LINE_CAP:
|
|
223
|
+
return None, None
|
|
210
224
|
name = m.group(1) or m.group(2)
|
|
211
|
-
return name,
|
|
225
|
+
return name, decl_line
|
|
212
226
|
|
|
213
227
|
|
|
214
228
|
def _parse_body(body: str) -> tuple[str, list[tuple[str, str]]]:
|
|
@@ -75,14 +75,9 @@ _JOB_MAIN_RE = re.compile(
|
|
|
75
75
|
r"^[ \t]*Main\s+Function\s+(\w+)\s*\(",
|
|
76
76
|
re.IGNORECASE | re.MULTILINE,
|
|
77
77
|
)
|
|
78
|
-
# RpcSetEnv
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
r"(?:['\"]([^'\"]*)['\"]|(\w+))\s*,\s*" # emp
|
|
82
|
-
r"(?:['\"]([^'\"]*)['\"]|(\w+))" # fil
|
|
83
|
-
r"(?:[^)]*?['\"](\w*)['\"])?", # módulo (5º arg, opcional)
|
|
84
|
-
re.IGNORECASE,
|
|
85
|
-
)
|
|
78
|
+
# RpcSetEnv — só localiza o início; args extraídos via _parse_rpcsetenv_args
|
|
79
|
+
# (v0.4.3 C3: regex única era frágil quando os 6 args vinham literais consecutivos).
|
|
80
|
+
_JOB_RPCSETENV_RE = re.compile(r"\bRpcSetEnv\s*\(", re.IGNORECASE)
|
|
86
81
|
# RpcSetType(3) — sem licença.
|
|
87
82
|
_JOB_RPCSETTYPE_RE = re.compile(r"\bRpcSetType\s*\(\s*3\s*\)", re.IGNORECASE)
|
|
88
83
|
# Sleep(N*1000) ou Sleep(N) em ms — extrai segundos.
|
|
@@ -102,6 +97,10 @@ _MAIL_UDC_CONNECT_RE = re.compile(r"^\s*CONNECT\s+SMTP\b", re.IGNORECASE | re.MU
|
|
|
102
97
|
_MAIL_TMAILMANAGER_RE = re.compile(r"\bTMailManager\s*\(", re.IGNORECASE)
|
|
103
98
|
_MAIL_TMAILMESSAGE_RE = re.compile(r"\bTMailMessage\s*\(", re.IGNORECASE)
|
|
104
99
|
_MAIL_SEND_METHOD_RE = re.compile(r":\s*Send\s*\(", re.IGNORECASE)
|
|
100
|
+
# v0.4.3 (I1): TMailManager:SendMail/SmtpConnect — variantes legadas (sem TMailMessage).
|
|
101
|
+
_MAIL_TMM_SEND_METHODS_RE = re.compile(
|
|
102
|
+
r":\s*(?:SendMail|SmtpConnect|Send)\s*\(", re.IGNORECASE,
|
|
103
|
+
)
|
|
105
104
|
# Anexo: ATTACHMENT (UDC) ou :AttachFile(
|
|
106
105
|
_MAIL_ATTACH_RE = re.compile(
|
|
107
106
|
r"\bATTACHMENT\b|:\s*AttachFile\s*\(", re.IGNORECASE,
|
|
@@ -127,6 +126,89 @@ def _snippet_at(content: str, linha: int, max_len: int = 200) -> str:
|
|
|
127
126
|
return ""
|
|
128
127
|
|
|
129
128
|
|
|
129
|
+
# --- Helpers ----------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _split_top_level_commas(s: str) -> list[str]:
|
|
133
|
+
"""Split por vírgulas top-level (ignora dentro de (), {}, []).
|
|
134
|
+
|
|
135
|
+
Usado pra extrair args de chamadas com aridade variável onde regex única
|
|
136
|
+
fica frágil (vide RpcSetEnv com 6 literais consecutivos — C3 v0.4.3).
|
|
137
|
+
Caller espera passar conteúdo já stripado (strings → spaces).
|
|
138
|
+
"""
|
|
139
|
+
parts: list[str] = []
|
|
140
|
+
depth_paren = depth_brace = depth_bracket = 0
|
|
141
|
+
last = 0
|
|
142
|
+
for i, c in enumerate(s):
|
|
143
|
+
if c == "(":
|
|
144
|
+
depth_paren += 1
|
|
145
|
+
elif c == ")":
|
|
146
|
+
depth_paren -= 1
|
|
147
|
+
elif c == "{":
|
|
148
|
+
depth_brace += 1
|
|
149
|
+
elif c == "}":
|
|
150
|
+
depth_brace -= 1
|
|
151
|
+
elif c == "[":
|
|
152
|
+
depth_bracket += 1
|
|
153
|
+
elif c == "]":
|
|
154
|
+
depth_bracket -= 1
|
|
155
|
+
elif (
|
|
156
|
+
c == ","
|
|
157
|
+
and depth_paren == 0
|
|
158
|
+
and depth_brace == 0
|
|
159
|
+
and depth_bracket == 0
|
|
160
|
+
):
|
|
161
|
+
parts.append(s[last:i])
|
|
162
|
+
last = i + 1
|
|
163
|
+
parts.append(s[last:])
|
|
164
|
+
return parts
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _find_balanced_paren(s: str, open_idx: int) -> int:
|
|
168
|
+
"""Dado idx de `(`, retorna idx do `)` casado. -1 se não casar."""
|
|
169
|
+
depth = 0
|
|
170
|
+
for i in range(open_idx, len(s)):
|
|
171
|
+
c = s[i]
|
|
172
|
+
if c == "(":
|
|
173
|
+
depth += 1
|
|
174
|
+
elif c == ")":
|
|
175
|
+
depth -= 1
|
|
176
|
+
if depth == 0:
|
|
177
|
+
return i
|
|
178
|
+
return -1
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _parse_rpcsetenv_args(content: str, original: str, open_paren_offset: int) -> dict[str, str]:
|
|
182
|
+
"""Extrai (empresa, filial, modulo) de uma chamada RpcSetEnv pelos args
|
|
183
|
+
posicionais. v0.4.3 (C3): substitui regex frágil que falhava com 6 args
|
|
184
|
+
literais consecutivos.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
content: source stripado (strings → spaces) onde o match foi encontrado.
|
|
188
|
+
original: source original (não usado aqui — kept simples).
|
|
189
|
+
open_paren_offset: índice do `(` em ``content``.
|
|
190
|
+
"""
|
|
191
|
+
close = _find_balanced_paren(content, open_paren_offset)
|
|
192
|
+
if close == -1:
|
|
193
|
+
return {"empresa": "", "filial": "", "modulo": ""}
|
|
194
|
+
args = _split_top_level_commas(content[open_paren_offset + 1 : close])
|
|
195
|
+
|
|
196
|
+
def _arg(idx: int) -> str:
|
|
197
|
+
if idx >= len(args):
|
|
198
|
+
return ""
|
|
199
|
+
token = args[idx].strip()
|
|
200
|
+
# Remove aspas se literal; caso contrário devolve identificador (variável).
|
|
201
|
+
if len(token) >= 2 and token[0] == token[-1] and token[0] in ("'", '"'):
|
|
202
|
+
return token[1:-1]
|
|
203
|
+
return token
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
"empresa": _arg(0),
|
|
207
|
+
"filial": _arg(1),
|
|
208
|
+
"modulo": _arg(4), # 5º arg (0-indexed)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
|
|
130
212
|
# --- Detectores -------------------------------------------------------------
|
|
131
213
|
|
|
132
214
|
|
|
@@ -134,13 +216,21 @@ def _detect_workflow(content: str, stripped: str) -> list[dict[str, Any]]:
|
|
|
134
216
|
"""Detecta `TWFProcess`, `MsWorkflow`, `WFPrepEnv` + extrai metadata."""
|
|
135
217
|
out: list[dict[str, Any]] = []
|
|
136
218
|
# TWFProcess (moderno) — emite 1 trigger por chamada com process_id.
|
|
137
|
-
|
|
219
|
+
# v0.4.3 (C1): coleta TODAS as posições primeiro pra calcular scope_end como
|
|
220
|
+
# próxima instanciação (vs janela fixa de 5000 chars que misturava callbacks
|
|
221
|
+
# entre TWFProcess vizinhos no mesmo fonte).
|
|
222
|
+
twfprocess_matches = list(_WF_TWFPROCESS_RE.finditer(stripped))
|
|
223
|
+
for i, m in enumerate(twfprocess_matches):
|
|
138
224
|
process_id = m.group(1) or ""
|
|
139
225
|
description = m.group(2) or ""
|
|
140
226
|
linha = _line_at(stripped, m.start())
|
|
141
|
-
# Buscar callbacks no contexto da função (até 50 linhas pra frente).
|
|
142
227
|
scope_start = m.start()
|
|
143
|
-
|
|
228
|
+
if i + 1 < len(twfprocess_matches):
|
|
229
|
+
# Cap pelo próximo TWFProcess (preserva isolamento entre workflows).
|
|
230
|
+
scope_end = twfprocess_matches[i + 1].start()
|
|
231
|
+
else:
|
|
232
|
+
# Último — vai até EOF (mas com cap defensivo de 5000 chars).
|
|
233
|
+
scope_end = min(len(stripped), scope_start + 5000)
|
|
144
234
|
scope = stripped[scope_start:scope_end]
|
|
145
235
|
callbacks: dict[str, str] = {}
|
|
146
236
|
for cm in _WF_CALLBACK_RE.finditer(scope):
|
|
@@ -263,9 +353,13 @@ def _detect_job_standalone(content: str, stripped: str) -> list[dict[str, Any]]:
|
|
|
263
353
|
continue
|
|
264
354
|
empresa = filial = modulo = ""
|
|
265
355
|
if rpc_match:
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
356
|
+
# v0.4.3 (C3): args via paren-balanced split (regex única era frágil
|
|
357
|
+
# quando 6 args vinham literais consecutivos sem vírgulas vazias).
|
|
358
|
+
# rpc_match.end() é offset em `body` (slice de `stripped`).
|
|
359
|
+
parsed_args = _parse_rpcsetenv_args(body, body, rpc_match.end() - 1)
|
|
360
|
+
empresa = parsed_args["empresa"]
|
|
361
|
+
filial = parsed_args["filial"]
|
|
362
|
+
modulo = parsed_args["modulo"]
|
|
269
363
|
# Sleep — extrai intervalo em segundos (assume Sleep(N*1000) = N segundos).
|
|
270
364
|
sleep_seconds = 0
|
|
271
365
|
sm = _JOB_SLEEP_RE.search(body)
|
|
@@ -335,6 +429,14 @@ def _detect_mail_send(content: str, stripped: str) -> list[dict[str, Any]]:
|
|
|
335
429
|
# TMailMessage:Send (preferido — TMailManager sozinho é só conexão).
|
|
336
430
|
for m in _MAIL_TMAILMESSAGE_RE.finditer(stripped):
|
|
337
431
|
_emit(m.start(), "TMailManager")
|
|
432
|
+
# v0.4.3 (I1): TMailManager solo (sem TMailMessage) — legacy. Detecta se há
|
|
433
|
+
# TMailManager + chamada de envio (`:SendMail`/`:Send`) no mesmo fonte e
|
|
434
|
+
# ainda nao temos trigger no fonte.
|
|
435
|
+
if not any(t["target"] == "TMailManager" for t in out):
|
|
436
|
+
tmm_match = _MAIL_TMAILMANAGER_RE.search(stripped)
|
|
437
|
+
send_match = _MAIL_TMM_SEND_METHODS_RE.search(stripped)
|
|
438
|
+
if tmm_match and send_match:
|
|
439
|
+
_emit(tmm_match.start(), "TMailManager")
|
|
338
440
|
return out
|
|
339
441
|
|
|
340
442
|
|
|
@@ -1132,20 +1132,45 @@ def protheus_docs_orphans(conn: sqlite3.Connection) -> list[dict[str, Any]]:
|
|
|
1132
1132
|
|
|
1133
1133
|
|
|
1134
1134
|
def protheus_doc_show(
|
|
1135
|
-
conn: sqlite3.Connection,
|
|
1135
|
+
conn: sqlite3.Connection,
|
|
1136
|
+
funcao: str,
|
|
1137
|
+
*,
|
|
1138
|
+
arquivo: str | None = None,
|
|
1136
1139
|
) -> dict[str, Any] | None:
|
|
1137
1140
|
"""Retorna doc completo de uma função (modo `--show`).
|
|
1138
1141
|
|
|
1139
|
-
|
|
1140
|
-
|
|
1142
|
+
v0.4.3 (I2): aceita ``arquivo`` opcional pra desambiguar quando há
|
|
1143
|
+
homônimos. Caller pode usar :func:`protheus_doc_homonyms` antes pra
|
|
1144
|
+
detectar e listar opções.
|
|
1141
1145
|
"""
|
|
1142
1146
|
sql = f"SELECT {_PDOC_COLUMNS} FROM protheus_docs WHERE funcao = ? COLLATE NOCASE"
|
|
1143
|
-
|
|
1147
|
+
params: list[Any] = [funcao]
|
|
1148
|
+
if arquivo:
|
|
1149
|
+
sql += " AND arquivo = ? COLLATE NOCASE"
|
|
1150
|
+
params.append(arquivo)
|
|
1151
|
+
sql += " ORDER BY arquivo, linha_bloco_inicio LIMIT 1"
|
|
1152
|
+
row = conn.execute(sql, params).fetchone()
|
|
1144
1153
|
if row is None:
|
|
1145
1154
|
return None
|
|
1146
1155
|
return _row_to_pdoc(row)
|
|
1147
1156
|
|
|
1148
1157
|
|
|
1158
|
+
def protheus_doc_homonyms(
|
|
1159
|
+
conn: sqlite3.Connection, funcao: str
|
|
1160
|
+
) -> list[str]:
|
|
1161
|
+
"""v0.4.3 (I2): lista arquivos com Protheus.doc pra ``funcao``.
|
|
1162
|
+
|
|
1163
|
+
Usado por `docs --show` pra avisar quando há ambiguidade. Retorna lista
|
|
1164
|
+
ordenada de basenames.
|
|
1165
|
+
"""
|
|
1166
|
+
rows = conn.execute(
|
|
1167
|
+
"SELECT DISTINCT arquivo FROM protheus_docs "
|
|
1168
|
+
"WHERE funcao = ? COLLATE NOCASE ORDER BY arquivo",
|
|
1169
|
+
(funcao,),
|
|
1170
|
+
).fetchall()
|
|
1171
|
+
return [r[0] for r in rows]
|
|
1172
|
+
|
|
1173
|
+
|
|
1149
1174
|
def render_pdoc_markdown(d: dict[str, Any]) -> str:
|
|
1150
1175
|
"""Renderiza um doc em Markdown estruturado pra modo `--show`."""
|
|
1151
1176
|
lines: list[str] = []
|
|
@@ -1068,6 +1068,39 @@ class TestDocs:
|
|
|
1068
1068
|
funcs = {r["funcao"] for r in rows}
|
|
1069
1069
|
assert "MT460NEW" in funcs
|
|
1070
1070
|
|
|
1071
|
+
def test_docs_show_homonym_warns_and_supports_arquivo(
|
|
1072
|
+
self, tmp_path: Path, runner: CliRunner
|
|
1073
|
+
) -> None:
|
|
1074
|
+
"""v0.4.3 (I2): 2 fontes com mesma funcao -> --show avisa em stderr
|
|
1075
|
+
e --arquivo desambiguar."""
|
|
1076
|
+
src = tmp_path / "src"
|
|
1077
|
+
src.mkdir()
|
|
1078
|
+
(src / "FnA.prw").write_bytes(
|
|
1079
|
+
b'/*/{Protheus.doc} HomFn\nDoc do A.\n@author Anna\n/*/\n'
|
|
1080
|
+
b'User Function HomFn()\nReturn\n'
|
|
1081
|
+
)
|
|
1082
|
+
(src / "FnB.prw").write_bytes(
|
|
1083
|
+
b'/*/{Protheus.doc} HomFn\nDoc do B.\n@author Beto\n/*/\n'
|
|
1084
|
+
b'User Function HomFn()\nReturn\n'
|
|
1085
|
+
)
|
|
1086
|
+
runner.invoke(app, ["--root", str(src), "init"])
|
|
1087
|
+
runner.invoke(app, ["--root", str(src), "ingest"])
|
|
1088
|
+
|
|
1089
|
+
# Sem --arquivo: aviso em stderr + mostra primeiro alfabeticamente
|
|
1090
|
+
result = runner.invoke(
|
|
1091
|
+
app, ["--root", str(src), "docs", "--show", "HomFn"]
|
|
1092
|
+
)
|
|
1093
|
+
assert result.exit_code == 0
|
|
1094
|
+
assert "2 fontes" in result.stderr or "Aviso" in result.stderr
|
|
1095
|
+
assert "Anna" in result.stdout # FnA.prw vem antes alfabeticamente
|
|
1096
|
+
|
|
1097
|
+
# Com --arquivo FnB.prw: mostra o do Beto
|
|
1098
|
+
result2 = runner.invoke(
|
|
1099
|
+
app, ["--root", str(src), "docs", "--show", "HomFn", "--arquivo", "FnB.prw"]
|
|
1100
|
+
)
|
|
1101
|
+
assert result2.exit_code == 0
|
|
1102
|
+
assert "Beto" in result2.stdout
|
|
1103
|
+
|
|
1071
1104
|
def test_docs_persisted_in_db(self, docs_project: Path) -> None:
|
|
1072
1105
|
"""Sanity: tabela protheus_docs existe e tem 2 rows."""
|
|
1073
1106
|
db = docs_project / ".plugadvpl" / "index.db"
|
|
@@ -256,6 +256,24 @@ class TestCatalog:
|
|
|
256
256
|
assert "SC5" in mata410["tables_primary"]
|
|
257
257
|
assert "SC6" in mata410["tables_primary"]
|
|
258
258
|
|
|
259
|
+
def test_catalog_no_duplicate_routines(self) -> None:
|
|
260
|
+
"""v0.4.3 (I5): nome de rotina deve ser unico no catalogo.
|
|
261
|
+
|
|
262
|
+
Permite o lookup determinístico (`_routines_index` faz dict[upper_name]).
|
|
263
|
+
Duplicata silenciosa faria a 2a entrada sobrescrever a 1a sem warning.
|
|
264
|
+
"""
|
|
265
|
+
cat = load_execauto_catalog()
|
|
266
|
+
names = [r["routine"].upper() for r in cat["routines"]]
|
|
267
|
+
dups = [n for n in set(names) if names.count(n) > 1]
|
|
268
|
+
assert not dups, f"Rotinas duplicadas no catalogo: {dups}"
|
|
269
|
+
|
|
270
|
+
def test_catalog_has_v043_additions(self) -> None:
|
|
271
|
+
"""v0.4.3 (I5): novas rotinas comuns adicionadas — 6 entradas extras."""
|
|
272
|
+
cat = load_execauto_catalog()
|
|
273
|
+
names = {r["routine"] for r in cat["routines"]}
|
|
274
|
+
for novo in ("MATA020", "MATA040", "MATA112", "FATA010", "FATA050"):
|
|
275
|
+
assert novo in names, f"Esperado {novo} no catalogo v0.4.3"
|
|
276
|
+
|
|
259
277
|
|
|
260
278
|
# --- arg_count + linha + snippet ------------------------------------------
|
|
261
279
|
|
|
@@ -213,6 +213,19 @@ class TestModuleInference:
|
|
|
213
213
|
"""Path SIGAFIN deve vencer prefixo MATA*."""
|
|
214
214
|
assert infer_module("src/SIGAFIN/MATA410.prw", "MATA410") == "SIGAFIN"
|
|
215
215
|
|
|
216
|
+
def test_module_ambiguous_prefix_returns_none(self) -> None:
|
|
217
|
+
"""v0.4.3 (C5): prefix `MATA` casa rotinas de SIGAEST/SIGAFAT/SIGACOM.
|
|
218
|
+
|
|
219
|
+
Antes: retornava SIGAEST silenciosamente (sort alfabético favorecia).
|
|
220
|
+
Agora: ambiguidade real → None (não inventar). MATA999 não existe no
|
|
221
|
+
catálogo e o prefixo é ambíguo entre 3 módulos.
|
|
222
|
+
"""
|
|
223
|
+
assert infer_module("X.prw", "MATA999") is None
|
|
224
|
+
|
|
225
|
+
def test_module_unambiguous_prefix_still_resolves(self) -> None:
|
|
226
|
+
"""Prefixo `FINA` mapeia 100% pra SIGAFIN — sem ambiguidade, resolve."""
|
|
227
|
+
assert infer_module("X.prw", "FINA999") == "SIGAFIN"
|
|
228
|
+
|
|
216
229
|
|
|
217
230
|
# --- Edge cases -----------------------------------------------------------
|
|
218
231
|
|
|
@@ -236,6 +249,40 @@ class TestEdgeCases:
|
|
|
236
249
|
assert len(docs) == 1
|
|
237
250
|
assert docs[0]["linha_funcao"] is None
|
|
238
251
|
|
|
252
|
+
def test_orphan_block_with_distant_function_treated_as_orphan(self) -> None:
|
|
253
|
+
"""v0.4.3 (C4): bloco órfão NÃO deve "puxar" função muito longe.
|
|
254
|
+
|
|
255
|
+
Cap: max 80 linhas entre /*/ fechamento e próxima decl. Acima disso
|
|
256
|
+
funcao=None, linha_funcao=None (preserva sinal de "órfão" e impede
|
|
257
|
+
que a função seguinte ganhe doc errada).
|
|
258
|
+
"""
|
|
259
|
+
# 100 linhas vazias entre /*/ e a decl
|
|
260
|
+
spacer = "\n".join(["// linha de filler"] * 100)
|
|
261
|
+
src = (
|
|
262
|
+
'/*/{Protheus.doc} Soltinho\nDoc.\n/*/\n'
|
|
263
|
+
f'{spacer}\n'
|
|
264
|
+
'User Function MuitoDepois()\n'
|
|
265
|
+
'Return\n'
|
|
266
|
+
)
|
|
267
|
+
docs = extract_protheus_docs(src)
|
|
268
|
+
assert len(docs) == 1
|
|
269
|
+
d = docs[0]
|
|
270
|
+
assert d["funcao"] is None, (
|
|
271
|
+
"Esperado funcao=None pra bloco com decl 100+ linhas adiante"
|
|
272
|
+
)
|
|
273
|
+
assert d["linha_funcao"] is None
|
|
274
|
+
|
|
275
|
+
def test_block_with_function_within_cap_resolves(self) -> None:
|
|
276
|
+
"""Sanity: decl dentro do cap (5 linhas) ainda resolve."""
|
|
277
|
+
src = (
|
|
278
|
+
'/*/{Protheus.doc} Fn\nDoc.\n/*/\n'
|
|
279
|
+
'// 1\n// 2\n// 3\n'
|
|
280
|
+
'User Function Fn()\nReturn\n'
|
|
281
|
+
)
|
|
282
|
+
d = extract_protheus_docs(src)[0]
|
|
283
|
+
assert d["funcao"] == "Fn"
|
|
284
|
+
assert d["linha_funcao"] is not None
|
|
285
|
+
|
|
239
286
|
def test_summary_stops_at_first_tag(self) -> None:
|
|
240
287
|
"""Summary = linhas até primeira @tag, não inclui tags."""
|
|
241
288
|
src = (
|
|
@@ -278,6 +325,35 @@ class TestEdgeCases:
|
|
|
278
325
|
assert d["history"][0]["user"] == "fvernier"
|
|
279
326
|
assert "Refactor" in d["history"][0]["desc"]
|
|
280
327
|
|
|
328
|
+
def test_example_with_inline_close_marker_does_not_close(self) -> None:
|
|
329
|
+
"""v0.4.3 (C2): `/*/` literal em meio de comentário no @example NÃO
|
|
330
|
+
deve fechar o bloco prematuramente.
|
|
331
|
+
|
|
332
|
+
Antes (bug): regex non-greedy `(?P<body>.*?)/\\*/` casava o primeiro
|
|
333
|
+
`/*/` que aparecesse — mesmo dentro do exemplo. Agora o fechamento
|
|
334
|
+
exige start-of-line (padrão oficial TOTVS — `/*/` fica sozinho na
|
|
335
|
+
própria linha).
|
|
336
|
+
"""
|
|
337
|
+
src = (
|
|
338
|
+
'/*/{Protheus.doc} Fn\n'
|
|
339
|
+
'Doc.\n'
|
|
340
|
+
'@example\n'
|
|
341
|
+
' // /*/ exemplo dentro do comentario\n'
|
|
342
|
+
' Local x := 1\n'
|
|
343
|
+
'/*/\n'
|
|
344
|
+
'User Function Fn()\n'
|
|
345
|
+
'Return\n'
|
|
346
|
+
)
|
|
347
|
+
docs = extract_protheus_docs(src)
|
|
348
|
+
assert len(docs) == 1
|
|
349
|
+
d = docs[0]
|
|
350
|
+
assert d["funcao"] == "Fn"
|
|
351
|
+
# O exemplo deve incluir o código completo (incluindo a linha do `/*/` interno).
|
|
352
|
+
assert d["examples"], "esperado pelo menos 1 example"
|
|
353
|
+
ex = d["examples"][0]
|
|
354
|
+
assert "Local x" in ex
|
|
355
|
+
assert "exemplo dentro" in ex
|
|
356
|
+
|
|
281
357
|
def test_example_with_at_inside_code(self) -> None:
|
|
282
358
|
"""@example com '@' dentro do código não conta como nova tag."""
|
|
283
359
|
src = (
|
|
@@ -65,6 +65,30 @@ class TestWorkflowTrigger:
|
|
|
65
65
|
triggers = extract_execution_triggers(src)
|
|
66
66
|
assert "workflow" not in _kinds(triggers)
|
|
67
67
|
|
|
68
|
+
def test_two_twfprocess_distinct_callbacks(self) -> None:
|
|
69
|
+
"""v0.4.3 (C1): 2 TWFProcess no mesmo arquivo nao devem misturar callbacks.
|
|
70
|
+
|
|
71
|
+
Antes (bug): scope_end fixo de 5000 chars capturava callback do segundo
|
|
72
|
+
TWFProcess e atribuia ao primeiro. Agora scope eh limitado pelo proximo
|
|
73
|
+
TWFProcess (ou EOF se nao houver).
|
|
74
|
+
"""
|
|
75
|
+
src = (
|
|
76
|
+
'User Function W1()\n'
|
|
77
|
+
' oWF1 := TWFProcess():New("P1", "D1")\n'
|
|
78
|
+
' oWF1:bReturn := {|o| Cb1(o)}\n'
|
|
79
|
+
'Return\n'
|
|
80
|
+
'User Function W2()\n'
|
|
81
|
+
' oWF2 := TWFProcess():New("P2", "D2")\n'
|
|
82
|
+
' oWF2:bReturn := {|o| Cb2(o)}\n'
|
|
83
|
+
'Return\n'
|
|
84
|
+
)
|
|
85
|
+
triggers = extract_execution_triggers(src)
|
|
86
|
+
wf = [t for t in triggers if t["kind"] == "workflow" and t["metadata"].get("process_id")]
|
|
87
|
+
assert len(wf) == 2
|
|
88
|
+
by_pid = {t["metadata"]["process_id"]: t for t in wf}
|
|
89
|
+
assert by_pid["P1"]["metadata"]["return_callback"].lower() == "cb1"
|
|
90
|
+
assert by_pid["P2"]["metadata"]["return_callback"].lower() == "cb2"
|
|
91
|
+
|
|
68
92
|
|
|
69
93
|
# --- Schedule ---------------------------------------------------------------
|
|
70
94
|
|
|
@@ -142,6 +166,27 @@ class TestJobStandaloneTrigger:
|
|
|
142
166
|
assert meta["stop_flag"] == "/stop_nfe.flg"
|
|
143
167
|
assert meta["no_license"] is True
|
|
144
168
|
|
|
169
|
+
def test_rpcsetenv_six_literal_args_extracts_modulo(self) -> None:
|
|
170
|
+
"""v0.4.3 (C3): RpcSetEnv com 6 args literais consecutivos extrai modulo.
|
|
171
|
+
|
|
172
|
+
Antes (bug): regex `(?:[^)]*?['\"](\\w*)['\"])?` consumia args 3 e 4
|
|
173
|
+
sem alcancar o 5o. Agora usa split top-level pra pegar o 5o exato.
|
|
174
|
+
"""
|
|
175
|
+
src = (
|
|
176
|
+
'Main Function J()\n'
|
|
177
|
+
' RpcSetEnv("01","01","","","FAT","J")\n'
|
|
178
|
+
' While !File("/stop_j.flg")\n'
|
|
179
|
+
' Sleep(60000)\n'
|
|
180
|
+
' EndDo\n'
|
|
181
|
+
'Return\n'
|
|
182
|
+
)
|
|
183
|
+
triggers = extract_execution_triggers(src)
|
|
184
|
+
jobs = [t for t in triggers if t["kind"] == "job_standalone"]
|
|
185
|
+
assert len(jobs) == 1
|
|
186
|
+
assert jobs[0]["metadata"]["empresa"] == "01"
|
|
187
|
+
assert jobs[0]["metadata"]["filial"] == "01"
|
|
188
|
+
assert jobs[0]["metadata"]["modulo"] == "FAT"
|
|
189
|
+
|
|
145
190
|
def test_negative_main_function_without_rpcsetenv(self) -> None:
|
|
146
191
|
"""Main Function sem RpcSetEnv (entry point standalone) — NAO eh job."""
|
|
147
192
|
src = (
|
|
@@ -200,6 +245,27 @@ class TestMailSendTrigger:
|
|
|
200
245
|
mails = [t for t in triggers if t["kind"] == "mail_send"]
|
|
201
246
|
assert any(m["metadata"]["variant"] == "UDC" for m in mails)
|
|
202
247
|
|
|
248
|
+
def test_positive_tmailmanager_solo_without_tmailmessage(self) -> None:
|
|
249
|
+
"""v0.4.3 (I1): TMailManager + :Send sem TMailMessage (legacy padrao).
|
|
250
|
+
|
|
251
|
+
Antes: detector exigia TMailMessage; fontes legados (anteriores ao
|
|
252
|
+
TMailMessage) eram ignorados. Agora TMailManager + :Send no mesmo
|
|
253
|
+
scope tambem vira trigger.
|
|
254
|
+
"""
|
|
255
|
+
src = (
|
|
256
|
+
'User Function ZLegacy()\n'
|
|
257
|
+
' Local oSrv := TMailManager():New()\n'
|
|
258
|
+
' oSrv:Init("", SuperGetMv("MV_RELSERV"), SuperGetMv("MV_RELACNT"), "", 0, 587)\n'
|
|
259
|
+
' oSrv:SmtpConnect()\n'
|
|
260
|
+
' oSrv:SendMail(cFrom, aTo, cSubject, cBody)\n'
|
|
261
|
+
' oSrv:Disconnect()\n'
|
|
262
|
+
'Return\n'
|
|
263
|
+
)
|
|
264
|
+
triggers = extract_execution_triggers(src)
|
|
265
|
+
mails = [t for t in triggers if t["kind"] == "mail_send"]
|
|
266
|
+
assert len(mails) >= 1
|
|
267
|
+
assert mails[0]["metadata"]["variant"] == "TMailManager"
|
|
268
|
+
|
|
203
269
|
def test_negative_mail_in_comment(self) -> None:
|
|
204
270
|
"""MailAuto em comentario nao dispara."""
|
|
205
271
|
src = (
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{plugadvpl-0.4.2 → plugadvpl-0.4.3}/plugadvpl/migrations/005_universo3_execution_triggers.sql
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|