plugadvpl 0.4.2__tar.gz → 0.4.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.
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/PKG-INFO +1 -1
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/plugadvpl/_version.py +2 -2
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/plugadvpl/cli.py +125 -16
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/plugadvpl/db.py +1 -1
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/plugadvpl/lookups/execauto_routines.json +50 -0
- plugadvpl-0.4.4/plugadvpl/migrations/008_universo3_funcao_indexes.sql +11 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/plugadvpl/parsing/protheus_doc.py +33 -14
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/plugadvpl/parsing/triggers.py +116 -14
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/plugadvpl/query.py +46 -7
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/integration/test_cli.py +152 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/unit/test_execauto.py +18 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/unit/test_protheus_doc.py +121 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/unit/test_triggers.py +66 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/.gitignore +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/README.md +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/plugadvpl/__init__.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/plugadvpl/__main__.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/plugadvpl/ingest.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/plugadvpl/ingest_sx.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/plugadvpl/lookups/.gitkeep +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/plugadvpl/lookups/funcoes_nativas.json +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/plugadvpl/lookups/funcoes_restritas.json +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/plugadvpl/lookups/lint_rules.json +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/plugadvpl/lookups/modulos_erp.json +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/plugadvpl/lookups/pontos_entrada_padrao.json +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/plugadvpl/lookups/sql_macros.json +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/plugadvpl/migrations/001_initial.sql +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/plugadvpl/migrations/002_universo2_sx.sql +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/plugadvpl/migrations/003_lint_rules_status.sql +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/plugadvpl/migrations/004_consultas_pk_with_tipo.sql +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/plugadvpl/migrations/005_universo3_execution_triggers.sql +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/plugadvpl/migrations/006_universo3_execauto_calls.sql +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/plugadvpl/migrations/007_universo3_protheus_docs.sql +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/plugadvpl/output.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/plugadvpl/parsing/.gitkeep +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/plugadvpl/parsing/__init__.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/plugadvpl/parsing/execauto.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/plugadvpl/parsing/lint.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/plugadvpl/parsing/parser.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/plugadvpl/parsing/stripper.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/plugadvpl/parsing/sx_csv.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/plugadvpl/scan.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/pyproject.toml +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/__init__.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/bench/.gitkeep +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/bench/__init__.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/bench/test_ingest_perf.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/bench/test_sx_ingest_perf.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/e2e_local/.gitkeep +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/e2e_local/__init__.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/e2e_local/test_e2e_local_ingest.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/e2e_local/test_ingest_sx_real.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/fixtures/expected/.gitkeep +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/fixtures/sx_synthetic/six.csv +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/fixtures/sx_synthetic/sx1.csv +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/fixtures/sx_synthetic/sx2.csv +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/fixtures/sx_synthetic/sx3.csv +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/fixtures/sx_synthetic/sx5.csv +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/fixtures/sx_synthetic/sx6.csv +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/fixtures/sx_synthetic/sx7.csv +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/fixtures/sx_synthetic/sx9.csv +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/fixtures/sx_synthetic/sxa.csv +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/fixtures/sx_synthetic/sxb.csv +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/fixtures/sx_synthetic/sxb_with_collisions.csv +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/fixtures/sx_synthetic/sxg.csv +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/fixtures/synthetic/.gitkeep +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/fixtures/synthetic/_generate.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/fixtures/synthetic/classic_browse.prw +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/fixtures/synthetic/corrupted.bak +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/fixtures/synthetic/empty.prw +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/fixtures/synthetic/encoding_cp1252.prw +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/fixtures/synthetic/encoding_utf8.prw +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/fixtures/synthetic/exec_auto.prw +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/fixtures/synthetic/http_outbound.prw +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/fixtures/synthetic/huge.prw +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/fixtures/synthetic/job_rpc.prw +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/fixtures/synthetic/multi_filial.prw +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/fixtures/synthetic/mvc_complete.prw +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/fixtures/synthetic/mvc_hooks.prw +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/fixtures/synthetic/pe_paramixb.prw +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/fixtures/synthetic/pe_simple.prw +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/fixtures/synthetic/pubvars.prw +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/fixtures/synthetic/reclock_alias_dup_trigger.prw +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/fixtures/synthetic/reclock_pattern.prw +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/fixtures/synthetic/reclock_unbalanced.prw +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/fixtures/synthetic/sql_embedded.prw +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/fixtures/synthetic/tlpp_namespace.tlpp +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/fixtures/synthetic/ws_rest.tlpp +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/fixtures/synthetic/ws_restful_classic.prw +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/fixtures/synthetic/ws_soap.prw +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/integration/.gitkeep +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/integration/__init__.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/integration/test_ingest.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/integration/test_ingest_sx.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/unit/.gitkeep +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/unit/__snapshots__/test_parser_snapshots.ambr +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/unit/test_db.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/unit/test_lint.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/unit/test_lint_catalog_consistency.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/unit/test_output.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/unit/test_parser.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/unit/test_parser_snapshots.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/unit/test_query.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/unit/test_scan.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/tests/unit/test_stripper.py +0 -0
- {plugadvpl-0.4.2 → plugadvpl-0.4.4}/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.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.4.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 4,
|
|
21
|
+
__version__ = version = '0.4.4'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 4, 4)
|
|
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,
|
|
@@ -114,6 +115,29 @@ class TableMode(StrEnum):
|
|
|
114
115
|
reclock = "reclock"
|
|
115
116
|
|
|
116
117
|
|
|
118
|
+
# v0.4.4 (UX #4): Enums pros filtros enumeráveis dos comandos Universo 3.
|
|
119
|
+
# Typer rejeita valores fora do enum antes de chegar na query (com mensagem
|
|
120
|
+
# clara listando as opções válidas) — substitui o comportamento antigo de
|
|
121
|
+
# silenciosamente retornar vazio em `--op invalida` / `--kind tipoinexistente`.
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class WorkflowKind(StrEnum):
|
|
125
|
+
"""Kinds do comando ``workflow`` (Universo 3 Feature A)."""
|
|
126
|
+
|
|
127
|
+
workflow = "workflow"
|
|
128
|
+
schedule = "schedule"
|
|
129
|
+
job_standalone = "job_standalone"
|
|
130
|
+
mail_send = "mail_send"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class ExecAutoOp(StrEnum):
|
|
134
|
+
"""Operações do filtro ``--op`` em ``execauto`` (Universo 3 Feature B)."""
|
|
135
|
+
|
|
136
|
+
inc = "inc"
|
|
137
|
+
alt = "alt"
|
|
138
|
+
exc = "exc"
|
|
139
|
+
|
|
140
|
+
|
|
117
141
|
# ---------------------------------------------------------------------------
|
|
118
142
|
# Callback global — popula ctx.obj com flags compartilhadas.
|
|
119
143
|
# ---------------------------------------------------------------------------
|
|
@@ -243,6 +267,41 @@ def _with_ro_db(
|
|
|
243
267
|
conn.close()
|
|
244
268
|
|
|
245
269
|
|
|
270
|
+
def _empty_result_hints(
|
|
271
|
+
filters_applied: bool,
|
|
272
|
+
*,
|
|
273
|
+
table_label: str,
|
|
274
|
+
extra_when_filtered: list[str] | None = None,
|
|
275
|
+
) -> list[str]:
|
|
276
|
+
"""Sugestões para resultado vazio (v0.4.4 UX #3).
|
|
277
|
+
|
|
278
|
+
Diferencia 2 cenários:
|
|
279
|
+
|
|
280
|
+
- ``filters_applied=True``: filtro semanticamente vazio (ex.: --arquivo
|
|
281
|
+
inexistente) → sugere verificar o filtro, NÃO sugere reingest caro.
|
|
282
|
+
- ``filters_applied=False``: tabela realmente vazia → sugere reingest.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
filters_applied: True se o usuário passou pelo menos 1 filtro.
|
|
286
|
+
table_label: rótulo amigável da tabela (ex.: "triggers", "calls").
|
|
287
|
+
extra_when_filtered: hints adicionais úteis quando filtrado
|
|
288
|
+
(ex.: ``--dynamic`` pra execauto).
|
|
289
|
+
"""
|
|
290
|
+
if filters_applied:
|
|
291
|
+
hints = [
|
|
292
|
+
"Filtro retornou vazio. Verifique se os argumentos batem com o índice:",
|
|
293
|
+
" plugadvpl find <termo> # confirma nome",
|
|
294
|
+
" plugadvpl status # ver contadores",
|
|
295
|
+
]
|
|
296
|
+
if extra_when_filtered:
|
|
297
|
+
hints.extend(extra_when_filtered)
|
|
298
|
+
return hints
|
|
299
|
+
return [
|
|
300
|
+
f"Nenhum {table_label} no índice. Rode:",
|
|
301
|
+
" plugadvpl ingest --no-incremental",
|
|
302
|
+
]
|
|
303
|
+
|
|
304
|
+
|
|
246
305
|
# ---------------------------------------------------------------------------
|
|
247
306
|
# version
|
|
248
307
|
# ---------------------------------------------------------------------------
|
|
@@ -934,7 +993,24 @@ def grep(
|
|
|
934
993
|
"""Busca textual no conteúdo dos chunks (FTS5 / LIKE / identifier)."""
|
|
935
994
|
|
|
936
995
|
limit = ctx.obj["limit"] or 50
|
|
937
|
-
|
|
996
|
+
try:
|
|
997
|
+
rows = _with_ro_db(ctx, lambda c: grep_fts(c, pattern, mode=mode.value, limit=limit))
|
|
998
|
+
except sqlite3.OperationalError as exc:
|
|
999
|
+
# v0.4.4 (BUG #1): FTS5 rejeita caracteres como `/`, `(`, `)`. Antes
|
|
1000
|
+
# propagava traceback completo vazando paths internos. Agora mensagem
|
|
1001
|
+
# amigável + sugestão de modo alternativo.
|
|
1002
|
+
if mode == GrepMode.fts and "fts5" in str(exc).lower():
|
|
1003
|
+
typer.echo(
|
|
1004
|
+
f"Padrão FTS5 inválido: {pattern!r}.\n"
|
|
1005
|
+
f"FTS5 não aceita caracteres como '/', '(', ')', '[', ']'. "
|
|
1006
|
+
f"Operadores válidos: '+', '*', '\"frase\"', 'OR', 'AND', 'NEAR'.\n"
|
|
1007
|
+
f"Alternativas:\n"
|
|
1008
|
+
f" plugadvpl grep {pattern!r} -m literal (substring exata via LIKE)\n"
|
|
1009
|
+
f" plugadvpl grep <termo> -m identifier (busca por símbolo)",
|
|
1010
|
+
err=True,
|
|
1011
|
+
)
|
|
1012
|
+
raise typer.Exit(code=2) from exc
|
|
1013
|
+
raise
|
|
938
1014
|
_render_from_ctx(
|
|
939
1015
|
ctx,
|
|
940
1016
|
rows,
|
|
@@ -1110,11 +1186,12 @@ def sx_status_cmd(ctx: typer.Context) -> None:
|
|
|
1110
1186
|
def workflow(
|
|
1111
1187
|
ctx: typer.Context,
|
|
1112
1188
|
kind: Annotated[
|
|
1113
|
-
|
|
1189
|
+
WorkflowKind | None,
|
|
1114
1190
|
typer.Option(
|
|
1115
1191
|
"--kind",
|
|
1116
1192
|
"-k",
|
|
1117
1193
|
help="Filtra por tipo: workflow|schedule|job_standalone|mail_send",
|
|
1194
|
+
case_sensitive=False,
|
|
1118
1195
|
),
|
|
1119
1196
|
] = None,
|
|
1120
1197
|
target: Annotated[
|
|
@@ -1163,9 +1240,12 @@ def workflow(
|
|
|
1163
1240
|
+ (f" (arquivo={arquivo})" if arquivo else "")
|
|
1164
1241
|
),
|
|
1165
1242
|
next_steps=(
|
|
1166
|
-
[f"plugadvpl find {
|
|
1243
|
+
[f"plugadvpl find {t}" for t in {r["target"] for r in rows[:3] if r["target"]}]
|
|
1167
1244
|
if rows
|
|
1168
|
-
else
|
|
1245
|
+
else _empty_result_hints(
|
|
1246
|
+
bool(kind or target or arquivo),
|
|
1247
|
+
table_label="execution trigger",
|
|
1248
|
+
)
|
|
1169
1249
|
),
|
|
1170
1250
|
)
|
|
1171
1251
|
|
|
@@ -1191,8 +1271,13 @@ def execauto(
|
|
|
1191
1271
|
typer.Option("--arquivo", "-a", help="Filtra por arquivo (basename, case-insensitive)."),
|
|
1192
1272
|
] = None,
|
|
1193
1273
|
op: Annotated[
|
|
1194
|
-
|
|
1195
|
-
typer.Option(
|
|
1274
|
+
ExecAutoOp | None,
|
|
1275
|
+
typer.Option(
|
|
1276
|
+
"--op",
|
|
1277
|
+
"-o",
|
|
1278
|
+
help="Filtra por operação: inc|alt|exc (op_code 3/4/5).",
|
|
1279
|
+
case_sensitive=False,
|
|
1280
|
+
),
|
|
1196
1281
|
] = None,
|
|
1197
1282
|
dynamic: Annotated[
|
|
1198
1283
|
bool | None,
|
|
@@ -1249,10 +1334,13 @@ def execauto(
|
|
|
1249
1334
|
for arq in {r["arquivo"] for r in rows[:3]}
|
|
1250
1335
|
]
|
|
1251
1336
|
if rows
|
|
1252
|
-
else
|
|
1253
|
-
|
|
1254
|
-
"
|
|
1255
|
-
|
|
1337
|
+
else _empty_result_hints(
|
|
1338
|
+
bool(routine or modulo or arquivo or op or dynamic is not None),
|
|
1339
|
+
table_label="execauto call",
|
|
1340
|
+
extra_when_filtered=[
|
|
1341
|
+
" plugadvpl execauto --dynamic # ver calls não-resolvíveis",
|
|
1342
|
+
],
|
|
1343
|
+
)
|
|
1256
1344
|
),
|
|
1257
1345
|
)
|
|
1258
1346
|
|
|
@@ -1305,10 +1393,28 @@ def docs(
|
|
|
1305
1393
|
formatado em Markdown. Use ``--orphans`` pra ver funções sem header.
|
|
1306
1394
|
"""
|
|
1307
1395
|
if show:
|
|
1308
|
-
|
|
1309
|
-
|
|
1396
|
+
# v0.4.3 (I2): com homônimos, --arquivo desambiguar; sem --arquivo,
|
|
1397
|
+
# avisa em stderr e mostra o primeiro alfabeticamente.
|
|
1398
|
+
homonyms = _with_ro_db(ctx, lambda c: protheus_doc_homonyms(c, show))
|
|
1399
|
+
if not homonyms:
|
|
1310
1400
|
typer.echo(f"Nenhum Protheus.doc encontrado pra função '{show}'.", err=True)
|
|
1311
1401
|
raise typer.Exit(code=1)
|
|
1402
|
+
if len(homonyms) > 1 and not arquivo:
|
|
1403
|
+
typer.echo(
|
|
1404
|
+
f"Aviso: '{show}' tem doc em {len(homonyms)} fontes: "
|
|
1405
|
+
f"{', '.join(homonyms)}. Mostrando '{homonyms[0]}'. "
|
|
1406
|
+
f"Use --arquivo <nome> pra escolher.",
|
|
1407
|
+
err=True,
|
|
1408
|
+
)
|
|
1409
|
+
d = _with_ro_db(
|
|
1410
|
+
ctx, lambda c: protheus_doc_show(c, show, arquivo=arquivo)
|
|
1411
|
+
)
|
|
1412
|
+
if d is None:
|
|
1413
|
+
typer.echo(
|
|
1414
|
+
f"Nenhum Protheus.doc encontrado pra '{show}' em '{arquivo}'.",
|
|
1415
|
+
err=True,
|
|
1416
|
+
)
|
|
1417
|
+
raise typer.Exit(code=1)
|
|
1312
1418
|
typer.echo(render_pdoc_markdown(d))
|
|
1313
1419
|
return
|
|
1314
1420
|
|
|
@@ -1368,10 +1474,13 @@ def docs(
|
|
|
1368
1474
|
for r in rows[:3] if r.get("funcao")
|
|
1369
1475
|
]
|
|
1370
1476
|
if rows
|
|
1371
|
-
else
|
|
1372
|
-
|
|
1373
|
-
"
|
|
1374
|
-
|
|
1477
|
+
else _empty_result_hints(
|
|
1478
|
+
bool(modulo or author or funcao or arquivo or deprecated is not None or tipo),
|
|
1479
|
+
table_label="Protheus.doc",
|
|
1480
|
+
extra_when_filtered=[
|
|
1481
|
+
" plugadvpl docs --orphans # funções sem header (BP-007)",
|
|
1482
|
+
],
|
|
1483
|
+
)
|
|
1375
1484
|
),
|
|
1376
1485
|
)
|
|
1377
1486
|
|
|
@@ -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,14 +30,21 @@ _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.
|
|
41
|
+
# v0.4.4 (BUG #2): inclui construtos de Web Service (WSSTRUCT/WSSERVICE/
|
|
42
|
+
# WSRESTFUL/WSMETHOD) que não têm parens. Antes ficavam órfãos e
|
|
43
|
+
# `docs --funcao`/`docs --show` não encontrava.
|
|
38
44
|
_NEXT_DECL_RE = re.compile(
|
|
39
45
|
r"^\s*(?:User\s+|Static\s+|Main\s+)?Function\s+(\w+)\s*\("
|
|
40
|
-
r"|^\s*Method\s+(\w+)\s*\("
|
|
46
|
+
r"|^\s*Method\s+(\w+)\s*\("
|
|
47
|
+
r"|^\s*WS(?:STRUCT|SERVICE|RESTFUL|METHOD)\s+(\w+)\b",
|
|
41
48
|
re.IGNORECASE | re.MULTILINE,
|
|
42
49
|
)
|
|
43
50
|
|
|
@@ -95,15 +102,13 @@ def infer_module(arquivo: str, funcao: str | None) -> str | None:
|
|
|
95
102
|
# 1. Exact match (rotina exata no catálogo).
|
|
96
103
|
if funcao_upper in idx:
|
|
97
104
|
return idx[funcao_upper]["module"]
|
|
98
|
-
# 2. Prefix match (4 primeiros chars).
|
|
99
|
-
#
|
|
105
|
+
# 2. Prefix match (4 primeiros chars). v0.4.3 (C5): só aceita se TODOS
|
|
106
|
+
# os matches do prefixo apontam pro MESMO módulo. Ambiguidade → None
|
|
107
|
+
# (não inventar). Antes retornava SIGAEST silenciosamente para MATA*.
|
|
100
108
|
prefix4 = funcao_upper[:4]
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
)
|
|
105
|
-
if matches:
|
|
106
|
-
return matches[0]["module"]
|
|
109
|
+
matched_modules = {e["module"] for k, e in idx.items() if k.startswith(prefix4)}
|
|
110
|
+
if len(matched_modules) == 1:
|
|
111
|
+
return matched_modules.pop()
|
|
107
112
|
return None
|
|
108
113
|
|
|
109
114
|
|
|
@@ -200,15 +205,29 @@ def _line_at(content: str, offset: int) -> int:
|
|
|
200
205
|
return content.count("\n", 0, offset) + 1
|
|
201
206
|
|
|
202
207
|
|
|
208
|
+
_PDOC_ORPHAN_LINE_CAP = 80 # v0.4.3 (C4): cap de proximidade pra associar bloco→decl
|
|
209
|
+
|
|
210
|
+
|
|
203
211
|
def _resolve_next_decl(
|
|
204
212
|
content: str, after_offset: int
|
|
205
213
|
) -> tuple[str | None, int | None]:
|
|
206
|
-
"""Acha próxima decl de função/método após offset. Retorna (nome, linha_1based).
|
|
214
|
+
"""Acha próxima decl de função/método após offset. Retorna (nome, linha_1based).
|
|
215
|
+
|
|
216
|
+
v0.4.3 (C4): cap de ``_PDOC_ORPHAN_LINE_CAP`` linhas entre o offset (fim do
|
|
217
|
+
bloco) e a decl encontrada. Acima disso retorna (None, None) — bloco é
|
|
218
|
+
tratado como órfão (preserva sinal de cobertura BP-007 e impede que função
|
|
219
|
+
distante ganhe doc errada associada).
|
|
220
|
+
"""
|
|
207
221
|
m = _NEXT_DECL_RE.search(content, after_offset)
|
|
208
222
|
if not m:
|
|
209
223
|
return None, None
|
|
210
|
-
|
|
211
|
-
|
|
224
|
+
block_end_line = _line_at(content, max(0, after_offset - 1))
|
|
225
|
+
decl_line = _line_at(content, m.start())
|
|
226
|
+
if decl_line - block_end_line > _PDOC_ORPHAN_LINE_CAP:
|
|
227
|
+
return None, None
|
|
228
|
+
# v0.4.4 (BUG #2): grupo 3 cobre WS constructs (WSSTRUCT/WSSERVICE/etc).
|
|
229
|
+
name = m.group(1) or m.group(2) or m.group(3)
|
|
230
|
+
return name, decl_line
|
|
212
231
|
|
|
213
232
|
|
|
214
233
|
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
|
|
|
@@ -1088,8 +1088,11 @@ def protheus_docs_query(
|
|
|
1088
1088
|
where.append("author LIKE ? COLLATE NOCASE")
|
|
1089
1089
|
params.append(f"%{author}%")
|
|
1090
1090
|
if funcao:
|
|
1091
|
-
|
|
1092
|
-
|
|
1091
|
+
# v0.4.4 (BUG #2): fallback pra funcao_id quando a coluna funcao
|
|
1092
|
+
# ficou NULL (ex.: bloco órfão, ou DB indexado em versão antiga
|
|
1093
|
+
# que não conhecia WSSTRUCT/WSSERVICE/etc).
|
|
1094
|
+
where.append("(funcao = ? COLLATE NOCASE OR funcao_id = ? COLLATE NOCASE)")
|
|
1095
|
+
params.extend([funcao, funcao])
|
|
1093
1096
|
if arquivo:
|
|
1094
1097
|
where.append("arquivo = ? COLLATE NOCASE")
|
|
1095
1098
|
params.append(arquivo)
|
|
@@ -1132,20 +1135,56 @@ def protheus_docs_orphans(conn: sqlite3.Connection) -> list[dict[str, Any]]:
|
|
|
1132
1135
|
|
|
1133
1136
|
|
|
1134
1137
|
def protheus_doc_show(
|
|
1135
|
-
conn: sqlite3.Connection,
|
|
1138
|
+
conn: sqlite3.Connection,
|
|
1139
|
+
funcao: str,
|
|
1140
|
+
*,
|
|
1141
|
+
arquivo: str | None = None,
|
|
1136
1142
|
) -> dict[str, Any] | None:
|
|
1137
1143
|
"""Retorna doc completo de uma função (modo `--show`).
|
|
1138
1144
|
|
|
1139
|
-
|
|
1140
|
-
|
|
1145
|
+
v0.4.3 (I2): aceita ``arquivo`` opcional pra desambiguar quando há
|
|
1146
|
+
homônimos. Caller pode usar :func:`protheus_doc_homonyms` antes pra
|
|
1147
|
+
detectar e listar opções.
|
|
1148
|
+
|
|
1149
|
+
v0.4.4 (BUG #2): match também via ``funcao_id`` quando coluna ``funcao``
|
|
1150
|
+
está NULL (DBs antigos com WSSTRUCT/WSSERVICE não-resolvidos, ou
|
|
1151
|
+
blocos órfãos cuja decl seguinte ficou > 80 linhas adiante).
|
|
1141
1152
|
"""
|
|
1142
|
-
sql =
|
|
1143
|
-
|
|
1153
|
+
sql = (
|
|
1154
|
+
f"SELECT {_PDOC_COLUMNS} FROM protheus_docs "
|
|
1155
|
+
"WHERE (funcao = ? COLLATE NOCASE OR funcao_id = ? COLLATE NOCASE)"
|
|
1156
|
+
)
|
|
1157
|
+
params: list[Any] = [funcao, funcao]
|
|
1158
|
+
if arquivo:
|
|
1159
|
+
sql += " AND arquivo = ? COLLATE NOCASE"
|
|
1160
|
+
params.append(arquivo)
|
|
1161
|
+
sql += " ORDER BY arquivo, linha_bloco_inicio LIMIT 1"
|
|
1162
|
+
row = conn.execute(sql, params).fetchone()
|
|
1144
1163
|
if row is None:
|
|
1145
1164
|
return None
|
|
1146
1165
|
return _row_to_pdoc(row)
|
|
1147
1166
|
|
|
1148
1167
|
|
|
1168
|
+
def protheus_doc_homonyms(
|
|
1169
|
+
conn: sqlite3.Connection, funcao: str
|
|
1170
|
+
) -> list[str]:
|
|
1171
|
+
"""v0.4.3 (I2): lista arquivos com Protheus.doc pra ``funcao``.
|
|
1172
|
+
|
|
1173
|
+
Usado por `docs --show` pra avisar quando há ambiguidade. Retorna lista
|
|
1174
|
+
ordenada de basenames.
|
|
1175
|
+
|
|
1176
|
+
v0.4.4 (BUG #2): match também via ``funcao_id`` (cobertura de blocos
|
|
1177
|
+
órfãos e WS constructs em DBs antigos).
|
|
1178
|
+
"""
|
|
1179
|
+
rows = conn.execute(
|
|
1180
|
+
"SELECT DISTINCT arquivo FROM protheus_docs "
|
|
1181
|
+
"WHERE funcao = ? COLLATE NOCASE OR funcao_id = ? COLLATE NOCASE "
|
|
1182
|
+
"ORDER BY arquivo",
|
|
1183
|
+
(funcao, funcao),
|
|
1184
|
+
).fetchall()
|
|
1185
|
+
return [r[0] for r in rows]
|
|
1186
|
+
|
|
1187
|
+
|
|
1149
1188
|
def render_pdoc_markdown(d: dict[str, Any]) -> str:
|
|
1150
1189
|
"""Renderiza um doc em Markdown estruturado pra modo `--show`."""
|
|
1151
1190
|
lines: list[str] = []
|