diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index c787e15..9e43ac9 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -51,7 +51,7 @@ Não duplicar em outros módulos: - `decode_bytes` — UTF-8 com fallback latin-1 - `strip_line_comments` — remove `//` e `/* */`, rastreia estado de bloco - `update_brace_depth` — rastreia `{}` ignorando literais string/char -- `has_inline_deprecated` — detecta `@DEPRECATED` inline +- `pragma_deprecated_message` — detecta `#pragma deprecated` e devolve sua mensagem ## Diagnósticos diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index f7ff48c..768e2c7 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -42,7 +42,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Initialize CodeQL - uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8 + uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 with: languages: ${{ matrix.language }} # Neither language needs a compiled build for CodeQL to extract it. @@ -50,6 +50,6 @@ jobs: queries: security-extended - name: Perform CodeQL analysis - uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8 + uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 with: category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b5d263a..0ba753a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -152,7 +152,7 @@ jobs: cat release_body.md - name: Create Release and upload binaries - uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2 + uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3 with: files: dist/* body_path: release_body.md diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 127f612..a140a26 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -35,6 +35,6 @@ jobs: publish_results: true - name: Upload to code-scanning - uses: github/codeql-action/upload-sarif@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8 + uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 with: sarif_file: results.sarif diff --git a/CHANGELOG.md b/CHANGELOG.md index 5544757..e287501 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,9 +13,29 @@ caso encontre por favor relate para ajudar a manter a consistência dos dados. --- -## [Unreleased] +## [1.4.0] - 30/08/2026 ### Adicionado +- **Comentários de documentação nos hovers, signature help e autocomplete** — o + comentário imediatamente acima de uma declaração passa a ser interpretado, não + apenas repassado como texto cru. Duas convenções são reconhecidas, detectadas + pelo próprio conteúdo: + - **Javadoc** (`@param`, `@return`, `@remarks`/`@note`) — o primeiro parágrafo + vira o resumo, e linhas seguintes a uma tag continuam o texto dela + - **XMLdoc** (``, ``, ``, ``) — a + convenção herdada do C# que o `omp-stdlib` usa. O HTML inline vira Markdown + (`` → negrito, `` → código, `
` → quebra de linha); ``, + `` e as âncoras `` são metadados do gerador da wiki + do open.mp e não aparecem no hover, onde só ocupariam espaço + + Em ambos, cada parâmetro é casado **pelo nome** (o sufixo `[]` é ignorado) e + não pela posição — um comentário pode omitir parâmetros ou listá-los fora de + ordem. O **signature help** passa a mostrar a descrição do parâmetro sob o + cursor; o **autocomplete** mostra só o resumo, e o **hover**, o bloco inteiro + formatado, com as seções traduzidas para o idioma resolvido. Um comentário sem + marcação continua virando a descrição, como antes +- **Tags de documentação no autocomplete** — o trigger `@`, dentro de um + comentário, passa a oferecer `@param`, `@return` e `@remarks` com snippets - **Diagnósticos e hovers traduzidos para Espanhol, Romeno e Russo** — as tabelas de mensagens `messages/langs/{es,ro,ru}.rs` existiam como esqueleto (texto ainda em inglês, copiado de `en.rs`) e agora estão de fato traduzidas: as 75 mensagens @@ -24,6 +44,53 @@ caso encontre por favor relate para ajudar a manter a consistência dos dados. `{}` / `{n}` / `{style}` preservados; termos técnicos do Pawn (`native`, `forward`, `stock`, `public`, `static`, `enum`) mantidos em inglês +### Alterado +- **`#pragma deprecated` substitui o marcador `@DEPRECATED`** — a depreciação + passa a usar a diretiva do próprio compilador Pawn, em vez do marcador em + comentário que a engine reconhecia antes. A semântica acompanha a do + compilador: a diretiva marca o **próximo símbolo declarado** e não tem forma + inline. O texto que a segue é opcional e, quando presente, é **anexado ao + aviso** PP0007 — é a parte acionável, já que normalmente diz o que usar no + lugar: + + ```pawn + #pragma deprecated Use BanPlayerFor em vez desta + stock BanTemporario(playerid, seconds) { } + ``` + + Cobertura inalterada: `native`, `stock`, `public`, `forward`, `static`, + `#define`, variáveis globais e `#include` (PP0008), incluindo o pareamento + automático entre `forward` e `public` +- **`@DEPRECATED` deixa de ser reconhecido** — comentários com o marcador antigo + não marcam mais nada. Quem o usava precisa trocar por `#pragma deprecated` + (ver acima). O completion do `@` que existia só para inseri-lo deu lugar às + tags de documentação +- **Dependências (Rust)** — `tokio` 1.52.3 → 1.53.1, `regex` 1.12.4 → 1.13.1, + `serde` 1.0.228 → 1.0.229, `serde_json` 1.0.150 → 1.0.151 e `futures` 0.3.32 → + 0.3.34, além das transitivas do `Cargo.lock` +- **CI — GitHub Actions atualizadas** (pinadas por SHA): `github/codeql-action` + 4.36.2 → 4.37.9, `actions/checkout` 4.2.2 → 7.0.1, `actions/upload-artifact` + 6.0.0 → 7.0.1, `actions/download-artifact` 7.0.0 → 8.0.1, + `actions/upload-pages-artifact` 3.0.1 → 5.0.0, `Swatinem/rust-cache` 2.9.1 → + 2.9.2, `ossf/scorecard-action` 2.4.3 → 2.4.4 e `softprops/action-gh-release` + 3.0.1 → 3.0.3 +- **Docs (CI)** — `mkdocs-material` para 9.7.7 e `pymdown-extensions` 10.21.3 → + 11.0.2 (pinados por hash) +- **Dependabot — um PR por ecossistema** — as atualizações passam a ser agrupadas + por ecossistema (cargo, GitHub Actions, pip) em vez de abrir um PR por + dependência, reduzindo o ruído de manutenção + +### Corrigido +- **CodeQL: análise ausente em PRs de docs e dependências** — o repositório usava + o *default setup* do CodeQL, que só analisa um pull request quando ele toca + arquivos das linguagens configuradas. Um PR que mexia apenas em documentação ou + em manifestos de dependência não gerava análise alguma, enquanto `master` + continuava com uma por linguagem — a regra de proteção `code_scanning` não + conseguia comparar os dois lados e reportava *"configurations not found"*, + travando o merge. Substituído pelo *advanced setup* (`.github/workflows/codeql.yml`), + sem filtro de caminho: as duas configurações (`/language:actions` e + `/language:rust`) passam a existir em todo pull request + ## [1.3.0] - 04/07/2026 ### Adicionado diff --git a/CLAUDE.md b/CLAUDE.md index 80b90de..9ab19ff 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -30,7 +30,7 @@ src/ config.rs ← EngineConfig recebida via initializationOptions parser/ lexer.rs ← decode_bytes, strip_line_comments, update_brace_depth, - has_inline_deprecated — utilitários de texto + pragma_deprecated_message — utilitários de texto symbols.rs ← parser principal: extrai Symbol, IncludeDirective, macros types.rs ← ParsedFile, Symbol, SymbolKind, IncludeDirective, Param mod.rs ← re-exports públicos @@ -41,7 +41,7 @@ src/ semantic.rs ← PP0002, PP0003, PP0004 — erros estruturais unused.rs ← PP0005, PP0006, PP0011, PP0012 — código morto hints.rs ← PP0009 — parâmetros não utilizados - deprecated.rs ← PP0007, PP0008 — @DEPRECATED + deprecated.rs ← PP0007, PP0008 — #pragma deprecated undefined.rs ← PP0010 — funções não declaradas mod.rs ← re-exports + função analyze() que orquestra tudo intellisense/ @@ -81,7 +81,7 @@ Armazenado como `Arc` no cache. Contém: - `symbols: Vec` — todas as declarações - `includes: Vec` — todas as diretivas `#include` / `#tryinclude` - `macro_names: Vec` — nomes de `#define` -- `deprecated_macros: Vec` — macros marcadas com `@DEPRECATED` +- `deprecated_macros: Vec` — macros marcadas com `#pragma deprecated` - `func_macro_prefixes: Vec` — prefixos como `CMD`, `BPR` - `namespace_aliases: HashMap` @@ -101,7 +101,8 @@ pub struct ResolvedIncludes { - `StaticConst` — constante: membro de enum, `stock const`, `static const` - `Enum` — nome do enum declarado (`enum NomeDoEnum { ... }`) - `Const` — constante declarada com `const` -- `deprecated: bool` — marcado com `@DEPRECATED` +- `deprecated: bool` — marcado com `#pragma deprecated` +- `deprecated_message: Option` — texto da diretiva, anexado ao aviso - `doc: Option` — comentário de documentação acima da declaração - `line: u32`, `col: u32` — posição 0-based em bytes UTF-8 @@ -171,7 +172,7 @@ Funções utilitárias canônicas — **não duplicar em outros módulos**: | `decode_bytes(bytes)` | UTF-8 com fallback latin-1 | | `strip_line_comments(line, in_block)` | Remove `//` e `/* */`, rastreia estado de bloco | | `update_brace_depth(ch, depth, in_str, in_char)` | Rastreia profundidade de `{}` ignorando literais | -| `has_inline_deprecated(line)` | Detecta `@DEPRECATED` inline na mesma linha | +| `pragma_deprecated_message(line)` | Detecta `#pragma deprecated` e devolve sua mensagem | --- diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2649e05..c8890a3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -57,7 +57,7 @@ docs/ ← documentação detalhada (não incluída nos releases) - **Nunca interpolar input não escapado em `Regex::new`**. - **`cargo clippy -- -D warnings` deve passar sem erros** — obrigatório. - **Novos diagnósticos sempre com constante em `analyzer/codes.rs`**. -- **Funções utilitárias de texto em `parser/lexer.rs`** — não duplicar `decode_bytes`, `strip_line_comments`, `update_brace_depth` ou `has_inline_deprecated` em outros módulos. +- **Funções utilitárias de texto em `parser/lexer.rs`** — não duplicar `decode_bytes`, `strip_line_comments`, `update_brace_depth` ou `pragma_deprecated_message` em outros módulos. ## Adicionando um novo diagnóstico diff --git a/Cargo.lock b/Cargo.lock index 016933d..0cf5040 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,22 +4,22 @@ version = 4 [[package]] name = "aho-corasick" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" dependencies = [ "memchr", ] [[package]] name = "async-trait" -version = "0.1.89" +version = "0.1.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.4", ] [[package]] @@ -30,7 +30,7 @@ checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -41,15 +41,15 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.13.0" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" [[package]] name = "bytes" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cfg-if" @@ -59,9 +59,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "crossbeam-utils" -version = "0.8.21" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" [[package]] name = "dashmap" @@ -92,13 +92,13 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.4", ] [[package]] @@ -176,7 +176,7 @@ checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 3.0.1", + "syn 3.0.4", ] [[package]] @@ -222,9 +222,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "icu_collections" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" dependencies = [ "displaydoc", "potential_utf", @@ -236,9 +236,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" dependencies = [ "displaydoc", "litemap", @@ -249,9 +249,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -263,16 +263,17 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" [[package]] name = "icu_properties" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" dependencies = [ + "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", @@ -283,15 +284,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" [[package]] name = "icu_provider" -version = "2.2.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" dependencies = [ "displaydoc", "icu_locale_core", @@ -331,15 +332,15 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "libc" -version = "0.2.186" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "litemap" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" [[package]] name = "lock_api" @@ -365,15 +366,15 @@ dependencies = [ [[package]] name = "memchr" -version = "2.8.2" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "mio" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" dependencies = [ "libc", "wasi", @@ -411,7 +412,7 @@ dependencies = [ [[package]] name = "pawnpro-engine" -version = "1.3.0" +version = "1.4.0" dependencies = [ "dashmap 6.2.1", "futures", @@ -446,7 +447,7 @@ checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -457,27 +458,27 @@ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "potential_utf" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" dependencies = [ "zerovec", ] [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.46" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] @@ -488,7 +489,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", ] [[package]] @@ -505,9 +506,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.16" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" dependencies = [ "aho-corasick", "memchr", @@ -562,7 +563,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.1", + "syn 3.0.4", ] [[package]] @@ -580,13 +581,13 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.20" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.4", ] [[package]] @@ -613,9 +614,9 @@ checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "socket2" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", "windows-sys", @@ -629,9 +630,9 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "syn" -version = "2.0.118" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" dependencies = [ "proc-macro2", "quote", @@ -640,9 +641,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.1" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5edbec4ed188954a10c12c038215f8ce7606b2d5c973cd8dc43e8795065c5f2f" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" dependencies = [ "proc-macro2", "quote", @@ -657,14 +658,14 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] name = "tinystr" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" dependencies = [ "displaydoc", "zerovec", @@ -689,24 +690,25 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.0" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.4", ] [[package]] name = "tokio-util" -version = "0.7.18" +version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" dependencies = [ "bytes", "futures-core", "futures-sink", + "libc", "pin-project-lite", "tokio", ] @@ -762,7 +764,7 @@ checksum = "84fd902d4e0b9a4b27f2f440108dc034e1758628a9b702f8ec61ad66355422fa" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -790,7 +792,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -869,9 +871,9 @@ dependencies = [ [[package]] name = "writeable" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" [[package]] name = "yoke" @@ -892,7 +894,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", "synstructure", ] @@ -913,15 +915,15 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", "synstructure", ] [[package]] name = "zerotrie" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" dependencies = [ "displaydoc", "yoke", @@ -930,9 +932,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.6" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" dependencies = [ "yoke", "zerofrom", @@ -941,17 +943,17 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.3" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.4", ] [[package]] name = "zmij" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml index 801c7cb..20655bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pawnpro-engine" -version = "1.3.0" +version = "1.4.0" edition = "2024" description = "PawnPro IntelliSense engine — LSP server for Pawn language" license = "SEE LICENSE IN LICENSE.md" diff --git a/README.md b/README.md index 7ad191e..0cc369a 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ A extensão PawnPro inicia o motor automaticamente ao detectar o binário. Se o - **Diagnósticos** — 17 códigos `PP####` cobrindo erros de estrutura, código morto, símbolos não declarados, depreciação e indentação (ver [docs/diagnostics.md](docs/diagnostics.md)). - **Completions** — símbolos de todos os includes transitivos com snippets de parâmetros; itens depreciados marcados. -- **Hover** — assinatura e comentário de documentação; em `#include` mostra o caminho resolvido. +- **Hover** — assinatura e comentário de documentação formatado (Javadoc `@param` e XMLdoc ``, o formato do `omp-stdlib`); em `#include` mostra o caminho resolvido. - **Signature Help** — parâmetro ativo destacado ao digitar `(` e `,`. - **CodeLens** — contagem de referências para todas as funções; clicável. - **References** — `textDocument/references` (Shift+F12). diff --git a/docs/diagnostics.md b/docs/diagnostics.md index 4c8a38f..433db1f 100644 --- a/docs/diagnostics.md +++ b/docs/diagnostics.md @@ -12,8 +12,8 @@ O motor emite diagnósticos identificados por códigos `PP####`. | `PP0004` | Aviso | `public`/`stock`/`static` sem corpo `{}` | | `PP0005` | Aviso¹ | Variável global declarada e não utilizada | | `PP0006` | Aviso¹ | Função `stock`/`static` não utilizada | -| `PP0007` | Aviso | Uso de símbolo ou include marcado com `@DEPRECATED` | -| `PP0008` | Aviso | `#include` precedido de comentário `@DEPRECATED` | +| `PP0007` | Aviso | Uso de símbolo marcado com `#pragma deprecated` | +| `PP0008` | Aviso | `#include` precedido de `#pragma deprecated` | | `PP0009` | Hint¹ | Parâmetro de função declarado e não utilizado | | `PP0010` | Aviso | Função chamada não declarada em nenhum include ativo | | `PP0011` | Hint¹ | `#define` declarado mas não utilizado | @@ -23,6 +23,7 @@ O motor emite diagnósticos identificados por códigos `PP####`. | `PP0015` | Hint | `forward` declarado mas nunca chamado | | `PP0016` | Aviso¹ | Função sem keyword declarada mas nunca chamada | | `PP0017` | Aviso | Indentação inconsistente dentro de um bloco | +| `PP0018` | Hint | Nome de identificador pobre (assistente de nomes; desligado por padrão) | > ¹ Marcados com `DiagnosticTag::UNNECESSARY` — o editor exibe o símbolo desbotado além do sublinhado diagnóstico. @@ -45,7 +46,14 @@ Emitido quando um `#include ` ou `#include "caminho"` não é resolvido e - O motor verifica o workspace inteiro — um símbolo usado em qualquer arquivo `.pwn`/`.inc` do projeto não dispara o aviso. ### PP0007 / PP0008 — Depreciação -Aplique `// @DEPRECATED` (ou `/* @DEPRECATED */`) na linha anterior à declaração ou inline. +Use a diretiva do compilador, `#pragma deprecated`, na linha anterior à declaração — ela marca o **próximo** símbolo declarado: + +```pawn +#pragma deprecated Use BanPlayerFor em vez desta +stock BanTemporario(playerid, seconds) { } +``` + +O texto após a diretiva é opcional e, quando presente, é anexado ao aviso — normalmente diz o que usar no lugar. - **PP0007** é emitido na própria declaração depreciada (hint visual com strikethrough) e em cada uso posterior. - Cobre: `native`, `stock`, `public`, `forward`, `static`, `#define` e variáveis globais. diff --git a/docs/lsp.md b/docs/lsp.md index b6b4ebd..1749509 100644 --- a/docs/lsp.md +++ b/docs/lsp.md @@ -63,9 +63,42 @@ O motor registra três caracteres de disparo: |---------|--------------| | `.` | Completions de namespace (aliases definidos via `#define NAMESPACE:: PREFIX_`) | | `#` | Completions de diretivas (`#include`, `#define`, `#if`, `#ifdef`, etc.) com snippets | -| `@` | Completion de `@DEPRECATED` — em comentários insere a tag; fora de comentários insere `// @DEPRECATED` | +| `@` | Tags de documentação (`@param`, `@return`, `@remarks`) — apenas dentro de comentários | -Completions normais (sem trigger) listam símbolos de todos os includes transitivos com snippets de parâmetros. Itens marcados com `@DEPRECATED` aparecem com tag de depreciação. +Completions normais (sem trigger) listam símbolos de todos os includes transitivos com snippets de parâmetros. Itens marcados com `#pragma deprecated` aparecem com tag de depreciação. + +## Comentários de documentação + +O comentário imediatamente acima de uma declaração alimenta o hover, o signature help e o autocomplete. Duas convenções são reconhecidas, detectadas pelo próprio conteúdo: + +**Javadoc** — `@param`, `@return`, `@remarks`/`@note`. O primeiro parágrafo é o resumo; linhas seguintes a uma tag continuam seu texto. + +```pawn +/** + * Ban a connected player's address permanently, with a reason. + * + * @param playerid the player to ban + * @param reason[] why, shown to them and kept in the record + * @return 1 always + */ +native BanEx(playerid, const reason[]); +``` + +**XMLdoc** — a convenção do C# que o `omp-stdlib` usa: ``, ``, ``, ``. O HTML inline vira Markdown (`` → negrito, `` → código, `
` → quebra); ``, `` e as âncoras `
` são metadados do gerador da wiki e não aparecem no hover. + +```pawn +/** + * omp_actor + * Checks if an actor is streamed in for a player. + * The ID of the actor + * 1 if streamed in, 0 otherwise. + */ +native bool:IsActorStreamedIn(actorid, playerid); +``` + +Em ambos, cada parâmetro é casado **pelo nome** (o sufixo `[]` é ignorado), não pela posição — um comentário pode omitir parâmetros ou listá-los fora de ordem. O signature help mostra a descrição do parâmetro sob o cursor; o autocomplete mostra só o resumo. + +Um comentário sem nenhuma marcação vira a descrição, como antes. ## Sincronização de documentos diff --git a/docs/requirements.txt b/docs/requirements.txt index 6694e96..d443436 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -313,9 +313,9 @@ pygments==2.20.0 \ --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 # via mkdocs-material -pymdown-extensions==11.0.1 \ - --hash=sha256:db3943a62bab7e03af1364f0c4083e64b91fb097675a4b6cceccfbe9a77e5eb2 \ - --hash=sha256:dd2905ae6fc5b75582fafb139a1266ffc754705efa902aa50067fa7ff4f94ec0 +pymdown-extensions==11.0.2 \ + --hash=sha256:259910762019732caa1dfd76f3faa62c59f191d46573e80bcb1d13c0f675bbe5 \ + --hash=sha256:9506fcbe66fa355a775b768084334238dd6805020ac4b92bea0c0dda6f8f223d # via mkdocs-material python-dateutil==2.9.0.post0 \ --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ diff --git a/mkdocs.yml b/mkdocs.yml index db5af57..17ea368 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -11,6 +11,10 @@ nav: - Introdução: index.md - Diagnósticos: diagnostics.md - Protocolo LSP: lsp.md + - Idiomas (i18n): i18n.md + - Assistente de nomes: + - Visão geral: naming-assistant.md + - Listas de nomes (.ban/.allow): naming-lists.md theme: name: material diff --git a/src/analyzer/deprecated.rs b/src/analyzer/deprecated.rs index 16e8016..9b304f1 100644 --- a/src/analyzer/deprecated.rs +++ b/src/analyzer/deprecated.rs @@ -4,7 +4,7 @@ use std::path::{Path, PathBuf}; use regex::Regex; use crate::messages::{Locale, MsgKey, msg}; -use crate::parser::lexer::{has_inline_deprecated, strip_line_comments}; +use crate::parser::lexer::{pragma_deprecated_message, strip_line_comments}; use crate::parser::types::IncludeDirective; use crate::parser::{ParsedFile, SymbolKind}; use crate::util::to_u32; @@ -23,11 +23,10 @@ struct DepEntry { kind: DepKind, /// Linha (0-based) da declaração no arquivo atual — `None` para símbolos de includes. decl_line: Option, + /// Texto do `#pragma deprecated`, anexado ao aviso quando presente. + message: Option, } -static RX_DEPRECATED: std::sync::LazyLock = std::sync::LazyLock::new(|| { - Regex::new(r"^\s*(?://\s*@DEPRECATED|/\*\s*@DEPRECATED\s*\*/)\s*$").unwrap() -}); static RX_INCLUDE: std::sync::LazyLock = std::sync::LazyLock::new(|| { Regex::new(r#"^\s*#\s*include\s*(?:<([^>]+)>|"([^"]+)")"#).unwrap() }); @@ -43,8 +42,8 @@ static RX_DECL_PREFIX: std::sync::LazyLock = std::sync::LazyLock::new(|| .unwrap() }); -/// Varre as linhas em busca de `#include` marcados como `@DEPRECATED` (na linha -/// anterior ou inline), emite o diagnóstico PP0008 e devolve o conjunto de +/// Varre as linhas em busca de `#include` precedidos de `#pragma deprecated`, +/// emite o diagnóstico PP0008 e devolve o conjunto de /// arquivos resolvidos como descontinuados, para propagar a marcação aos símbolos. fn collect_deprecated_includes( lines: &[&str], @@ -59,7 +58,7 @@ fn collect_deprecated_includes( for (line_idx, raw_line) in lines.iter().enumerate() { let raw_line = raw_line.trim_end_matches('\r'); - if RX_DEPRECATED.is_match(raw_line) { + if pragma_deprecated_message(raw_line).is_some() { pending_deprecated = true; let s = strip_line_comments(raw_line, in_block); in_block = s.in_block; @@ -73,10 +72,7 @@ fn collect_deprecated_includes( continue; } - let inline_deprecated = has_inline_deprecated(raw_line); - if (pending_deprecated || inline_deprecated) - && let Some(cap) = RX_INCLUDE.captures(line) - { + if pending_deprecated && let Some(cap) = RX_INCLUDE.captures(line) { let token = cap.get(1).or(cap.get(2)).map_or("", |m| m.as_str().trim()); let is_angle = cap.get(1).is_some(); let dir = IncludeDirective { @@ -153,6 +149,7 @@ pub fn analyze_deprecated( dep_macros.entry(m.clone()).or_insert(DepEntry { kind: DepKind::Individual, decl_line: None, + message: None, }); } @@ -161,6 +158,7 @@ pub fn analyze_deprecated( dep_callables.entry(sym.name.clone()).or_insert(DepEntry { kind: DepKind::Individual, decl_line: Some(sym.line), + message: sym.deprecated_message.clone(), }); } } @@ -205,6 +203,7 @@ pub fn analyze_deprecated( dep_macros.entry(m.clone()).or_insert(DepEntry { kind, decl_line: None, + message: None, }); } } @@ -215,6 +214,7 @@ pub fn analyze_deprecated( dep_callables.entry(sym.name.clone()).or_insert(DepEntry { kind: DepKind::Individual, decl_line: Some(sym.line), + message: sym.deprecated_message.clone(), }); } } @@ -232,7 +232,13 @@ pub fn analyze_deprecated( sym.col, col_end, codes::PP0007, - msg(locale, MsgKey::SymDeprecated).replace("{}", &sym.name), + match &sym.deprecated_message { + Some(m) if !m.is_empty() => format!( + "{}: {m}", + msg(locale, MsgKey::SymDeprecated).replace("{}", &sym.name) + ), + _ => msg(locale, MsgKey::SymDeprecated).replace("{}", &sym.name), + }, )); } @@ -292,7 +298,7 @@ fn emit_deprecated_usages( col, col + to_u32(name.len()), codes::PP0007, - dep_msg(name, &entry.kind, locale), + dep_msg(name, entry, locale), )); } } @@ -311,7 +317,7 @@ fn emit_deprecated_usages( col, col + to_u32(name.len()), codes::PP0007, - dep_msg(name, &entry.kind, locale), + dep_msg(name, entry, locale), )); } } @@ -326,7 +332,11 @@ fn classify_sym( kind: DepKind, decl_line: Option, ) { - let entry = DepEntry { kind, decl_line }; + let entry = DepEntry { + kind, + decl_line, + message: sym.deprecated_message.clone(), + }; match sym.kind { SymbolKind::Define => { macros.entry(sym.name.clone()).or_insert(entry); @@ -340,9 +350,65 @@ fn classify_sym( } } -fn dep_msg(name: &str, kind: &DepKind, locale: Locale) -> String { - match kind { +fn dep_msg(name: &str, entry: &DepEntry, locale: Locale) -> String { + let base = match entry.kind { DepKind::Individual => msg(locale, MsgKey::SymDeprecatedUsage).replace("{}", name), DepKind::FromFile => msg(locale, MsgKey::SymFromDeprecatedFile).replace("{}", name), + }; + // A mensagem da diretiva costuma dizer o que usar no lugar — é a parte + // acionável do aviso, então vai junto. + match &entry.message { + Some(m) if !m.is_empty() => format!("{base}: {m}"), + _ => base, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::parser::parse_file; + + fn analyze(src: &str) -> Vec { + analyze_deprecated( + src, + Path::new("/tmp/test.pwn"), + &parse_file(src), + &[], + &ResolvedIncludes { + paths: Vec::new(), + files: HashMap::new(), + reverse_deps: HashMap::new(), + }, + Locale::En, + ) + } + + #[test] + fn usage_of_deprecated_symbol_is_reported() { + let d = analyze("#pragma deprecated\nstock Old() {}\nmain() { Old(); }"); + assert!( + d.iter().any(|x| x.code == codes::PP0007 && x.line == 2), + "{d:?}" + ); + } + + #[test] + fn pragma_message_is_appended_to_the_warning() { + let d = analyze("#pragma deprecated use New instead\nstock Old() {}\nmain() { Old(); }"); + let usage = d + .iter() + .find(|x| x.code == codes::PP0007 && x.line == 2) + .expect("aviso de uso"); + assert!( + usage.message.contains("use New instead"), + "{}", + usage.message + ); + } + + #[test] + fn symbol_without_pragma_is_not_reported() { + let d = analyze("stock Fine() {}\nmain() { Fine(); }"); + assert!(!d.iter().any(|x| x.code == codes::PP0007), "{d:?}"); } } diff --git a/src/analyzer/naming.rs b/src/analyzer/naming.rs index d4277f7..19ae056 100644 --- a/src/analyzer/naming.rs +++ b/src/analyzer/naming.rs @@ -104,6 +104,7 @@ mod tests { signature: None, params, deprecated: false, + deprecated_message: None, doc: None, line: 0, col: 0, diff --git a/src/intellisense/completion.rs b/src/intellisense/completion.rs index 265ad0c..bd134f6 100644 --- a/src/intellisense/completion.rs +++ b/src/intellisense/completion.rs @@ -2,8 +2,8 @@ use std::collections::HashSet; use regex::Regex; use tower_lsp::lsp_types::{ - CompletionItem, CompletionItemKind, CompletionItemTag, CompletionTextEdit, Documentation, - InsertTextFormat, MarkupContent, MarkupKind, Position, Range, TextEdit, + CompletionItem, CompletionItemKind, CompletionItemTag, Documentation, InsertTextFormat, + MarkupContent, MarkupKind, Position, }; use crate::messages::{Locale, MsgKey, msg}; @@ -430,46 +430,33 @@ fn extract_param_name(part: &str) -> String { } } -pub fn get_at_completions( - in_comment: bool, - line: u32, - at_col: u32, - locale: Locale, -) -> Vec { - let detail = Some(msg(locale, MsgKey::KwAtDeprecated).to_string()); - if in_comment { - vec![CompletionItem { - label: "@DEPRECATED".to_string(), - kind: Some(CompletionItemKind::KEYWORD), - detail, - insert_text: Some("DEPRECATED".to_string()), - insert_text_format: Some(InsertTextFormat::PLAIN_TEXT), - sort_text: Some("0_DEPRECATED".to_string()), - ..Default::default() - }] - } else { - let range = Range { - start: Position { - line, - character: at_col, - }, - end: Position { - line, - character: at_col + 1, - }, - }; - vec![CompletionItem { - label: "@DEPRECATED".to_string(), - kind: Some(CompletionItemKind::KEYWORD), - detail, - text_edit: Some(CompletionTextEdit::Edit(TextEdit { - range, - new_text: "// @DEPRECATED".to_string(), - })), - sort_text: Some("0_DEPRECATED".to_string()), - ..Default::default() - }] +/// Completions do trigger `@`: as tags de documentação Javadoc, úteis apenas +/// dentro de um comentário. Fora dele, `@` não inicia nada em Pawn. +pub fn get_at_completions(in_comment: bool, locale: Locale) -> Vec { + if !in_comment { + return Vec::new(); } + [ + ( + "param", + MsgKey::DocTagParam, + "param ${1:nome} ${2:descrição}", + ), + ("return", MsgKey::DocTagReturn, "return ${1:descrição}"), + ("remarks", MsgKey::DocTagRemarks, "remarks ${1:nota}"), + ] + .into_iter() + .map(|(label, detail_key, insert)| CompletionItem { + label: format!("@{label}"), + kind: Some(CompletionItemKind::KEYWORD), + detail: Some(msg(locale, detail_key).to_string()), + // O `@` já foi digitado e disparou o trigger. + insert_text: Some(insert.to_string()), + insert_text_format: Some(InsertTextFormat::SNIPPET), + sort_text: Some(format!("0_{label}")), + ..Default::default() + }) + .collect() } // `locale` (i18n) e `locals` (variáveis locais) são termos corretos do domínio; @@ -578,12 +565,19 @@ fn build_symbol_item(sym: &crate::parser::types::Symbol) -> CompletionItem { label: sym.name.clone(), kind, detail: sym.signature.clone(), - documentation: sym.doc.as_ref().map(|d| { - Documentation::MarkupContent(MarkupContent { - kind: MarkupKind::Markdown, - value: d.clone(), - }) - }), + // Na lista de autocomplete cabe o resumo, não o bloco inteiro; o hover + // mostra a doc completa. + documentation: sym + .doc + .as_deref() + .map(super::parse_doc) + .and_then(|d| d.short()) + .map(|v| { + Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: v, + }) + }), insert_text, insert_text_format, sort_text: Some(format!("0_{}", sym.name)), diff --git a/src/intellisense/docs.rs b/src/intellisense/docs.rs new file mode 100644 index 0000000..8ec11fd --- /dev/null +++ b/src/intellisense/docs.rs @@ -0,0 +1,559 @@ +//! Normalização de comentários de documentação. +//! +//! Duas convenções circulam no ecossistema Pawn e ambas precisam render o mesmo +//! resultado: o estilo Javadoc (`@param`, `@return`) e o `XMLdoc` herdado do C# +//! que o `omp-stdlib` usa (``, ``), cujas tags são +//! lidas pelo gerador da wiki do open.mp. + +use std::fmt::Write as _; + +use regex::Regex; + +/// Um comentário de documentação já separado em partes, independente da +/// convenção em que foi escrito. +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub struct DocComment { + /// Primeira frase / parágrafo — o resumo curto. + pub summary: Option, + /// Parágrafos adicionais de descrição. + pub description: Option, + /// Parâmetros na ordem em que aparecem no comentário. + pub params: Vec, + pub returns: Option, + /// `` do `XMLdoc`, ou `@remarks` / `@note`. + pub remarks: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DocParam { + /// Nome como escrito no comentário, sem o `[]` de array. + pub name: String, + pub text: String, +} + +impl DocComment { + pub fn is_empty(&self) -> bool { + self.summary.is_none() + && self.description.is_none() + && self.params.is_empty() + && self.returns.is_none() + && self.remarks.is_none() + } + + /// Texto do parâmetro, casando pelo nome sem o sufixo `[]`. + pub fn param(&self, name: &str) -> Option<&str> { + let name = name.trim_end_matches("[]"); + self.params + .iter() + .find(|p| p.name == name) + .map(|p| p.text.as_str()) + } + + /// Resumo e descrição, sem a lista de parâmetros — para o autocomplete, + /// onde só cabe uma linha ou duas. + pub fn short(&self) -> Option { + self.summary.clone().or_else(|| self.description.clone()) + } + + /// Renderiza tudo como Markdown, para o hover. + pub fn to_markdown(&self, labels: &DocLabels) -> Option { + if self.is_empty() { + return None; + } + let mut md = String::new(); + + if let Some(s) = &self.summary { + md.push_str(s); + } + if let Some(d) = &self.description { + if !md.is_empty() { + md.push_str("\n\n"); + } + md.push_str(d); + } + + if !self.params.is_empty() { + if !md.is_empty() { + md.push_str("\n\n"); + } + let _ = write!(md, "**{}**", labels.params); + for p in &self.params { + if p.text.is_empty() { + let _ = write!(md, "\n- `{}`", p.name); + } else { + let _ = write!(md, "\n- `{}` — {}", p.name, p.text); + } + } + } + + if let Some(r) = &self.returns { + if !md.is_empty() { + md.push_str("\n\n"); + } + let _ = write!(md, "**{}** {}", labels.returns, r); + } + + if let Some(r) = &self.remarks { + if !md.is_empty() { + md.push_str("\n\n"); + } + let _ = write!(md, "**{}** {}", labels.remarks, r); + } + + if md.is_empty() { None } else { Some(md) } + } +} + +/// Rótulos das seções, para que o hover saia no idioma do usuário. +pub struct DocLabels { + pub params: String, + pub returns: String, + pub remarks: String, +} + +/// Remove os marcadores do bloco (`/**`, `*`, `*/`, `//`) preservando a +/// indentação relativa do texto — que separa parágrafos no `XMLdoc`. +fn strip_markers(doc: &str) -> Vec { + let mut out = Vec::new(); + for raw in doc.lines() { + let mut l = raw.trim(); + if l.starts_with("/**") { + l = &l[3..]; + } else if l.starts_with("/*") { + l = &l[2..]; + } + if l.ends_with("*/") { + l = &l[..l.len() - 2]; + } + let l = l.trim_start(); + // `*` de continuação: só quando não é o início de um `*/` já removido. + let l = l.strip_prefix('*').unwrap_or(l); + let l = l.strip_prefix("//").unwrap_or(l); + // Uma única casa de indentação depois do marcador é ruído do estilo. + let l = l.strip_prefix(' ').unwrap_or(l); + out.push(l.trim_end().to_string()); + } + // Linhas vazias nas pontas não carregam informação. + while out.first().is_some_and(|l| l.trim().is_empty()) { + out.remove(0); + } + while out.last().is_some_and(|l| l.trim().is_empty()) { + out.pop(); + } + out +} + +static RX_XML_TAG: std::sync::LazyLock = + std::sync::LazyLock::new(|| Regex::new(r"]*)?/?>").unwrap()); + +/// Converte o HTML inline que o open.mp usa para ênfase em Markdown, e remove +/// o resto das tags. `` é "code" na convenção do C#. +fn html_to_markdown(s: &str) -> String { + let mut out = String::with_capacity(s.len()); + let bytes = s.as_bytes(); + let mut i = 0usize; + while i < bytes.len() { + if bytes[i] == b'<' { + let Some(close) = s[i..].find('>') else { + out.push('<'); + i += 1; + continue; + }; + let tag_raw = &s[i + 1..i + close]; + let tag = tag_raw.trim_end_matches('/').trim(); + let lower = tag.to_ascii_lowercase(); + let name = lower + .split(|c: char| c.is_whitespace()) + .next() + .unwrap_or("") + .trim_start_matches('/'); + match name { + "b" | "strong" => out.push_str("**"), + "i" | "em" => out.push('*'), + "c" | "code" => out.push('`'), + "br" => out.push('\n'), + // texto vira só o texto: âncoras da wiki do + // open.mp não resolvem para nada dentro do editor. + _ => {} + } + i += close + 1; + } else { + out.push(bytes[i] as char); + i += 1; + } + } + // Reconstrói para não quebrar UTF-8 no caminho byte a byte acima. + if !s.is_ascii() { + return html_to_markdown_unicode(s); + } + decode_entities(&out) +} + +/// Mesmo que `html_to_markdown`, por chars, para texto não-ASCII. +fn html_to_markdown_unicode(s: &str) -> String { + let replaced = RX_XML_TAG.replace_all(s, |c: ®ex::Captures| { + let t = c[0].to_ascii_lowercase(); + let name = t + .trim_start_matches("') + .trim_end_matches('/') + .split(|c: char| c.is_whitespace()) + .next() + .unwrap_or("") + .to_string(); + match name.as_str() { + "b" | "strong" => "**".to_string(), + "i" | "em" => "*".to_string(), + "c" | "code" => "`".to_string(), + "br" => "\n".to_string(), + _ => String::new(), + } + }); + decode_entities(&replaced) +} + +fn decode_entities(s: &str) -> String { + s.replace("<", "<") + .replace(">", ">") + .replace(""", "\"") + .replace("'", "'") + .replace("&", "&") +} + +/// Junta linhas em um parágrafo, colapsando espaços — o `XMLdoc` do open.mp +/// quebra frases em qualquer coluna e a quebra não é significativa. +fn join_wrapped(lines: &[String]) -> String { + let mut paragraphs: Vec = Vec::new(); + let mut cur: Vec<&str> = Vec::new(); + for l in lines { + let t = l.trim(); + if t.is_empty() { + if !cur.is_empty() { + paragraphs.push(cur.join(" ")); + cur.clear(); + } + } else { + cur.push(t); + } + } + if !cur.is_empty() { + paragraphs.push(cur.join(" ")); + } + paragraphs + .iter() + .map(|p| p.split_whitespace().collect::>().join(" ")) + .collect::>() + .join("\n\n") + .trim() + .to_string() +} + +/// Detecta a convenção e delega. Um bloco sem nenhuma marcação vira descrição. +pub fn parse_doc(doc: &str) -> DocComment { + let lines = strip_markers(doc); + if lines.is_empty() { + return DocComment::default(); + } + let joined = lines.join("\n"); + if joined.contains("") + || joined.contains("") + || joined.contains("") + { + parse_xmldoc(&joined) + } else { + parse_javadoc(&lines) + } +} + +// A crate `regex` não tem backreferences, então cada bloco tem seu próprio +// padrão em vez de `<(tag)>...`. +fn block_rx(tag: &str) -> Regex { + Regex::new(&format!(r"(?s)<{tag}>(.*?)")).unwrap() +} +static RX_SUMMARY: std::sync::LazyLock = std::sync::LazyLock::new(|| block_rx("summary")); +static RX_RETURNS: std::sync::LazyLock = std::sync::LazyLock::new(|| block_rx("returns")); +static RX_REMARKS: std::sync::LazyLock = std::sync::LazyLock::new(|| block_rx("remarks")); +static RX_VALUE: std::sync::LazyLock = std::sync::LazyLock::new(|| block_rx("value")); +static RX_XML_PARAM: std::sync::LazyLock = std::sync::LazyLock::new(|| { + Regex::new(r#"(?s)(.*?)"#).unwrap() +}); +/// `` sem corpo — legal no C#, aparece em includes gerados. +static RX_XML_PARAM_EMPTY: std::sync::LazyLock = + std::sync::LazyLock::new(|| Regex::new(r#""#).unwrap()); + +fn parse_xmldoc(text: &str) -> DocComment { + let mut doc = DocComment::default(); + + let first = |rx: &Regex| -> Option { + rx.captures(text) + .map(|c| clean_block(&c[1])) + .filter(|b| !b.is_empty()) + }; + doc.summary = first(&RX_SUMMARY); + // `` documenta o valor de uma propriedade; o mais próximo aqui é o + // retorno. + doc.returns = first(&RX_RETURNS).or_else(|| first(&RX_VALUE)); + doc.remarks = first(&RX_REMARKS); + + for cap in RX_XML_PARAM.captures_iter(text) { + doc.params.push(DocParam { + name: cap[1].trim().trim_end_matches("[]").to_string(), + text: clean_block(&cap[2]), + }); + } + for cap in RX_XML_PARAM_EMPTY.captures_iter(text) { + let name = cap[1].trim().trim_end_matches("[]").to_string(); + if !doc.params.iter().any(|p| p.name == name) { + doc.params.push(DocParam { + name, + text: String::new(), + }); + } + } + + // `` e `` são metadados do gerador da wiki: não cabem no + // hover, e uma lista de 20 `` afogaria o texto que importa. + doc +} + +fn clean_block(body: &str) -> String { + let converted = html_to_markdown(body); + let lines: Vec = converted.lines().map(|l| l.trim().to_string()).collect(); + join_wrapped(&lines) +} + +static RX_TAG: std::sync::LazyLock = + std::sync::LazyLock::new(|| Regex::new(r"^@(\w+)\b[ \t]*(.*)$").unwrap()); + +fn parse_javadoc(lines: &[String]) -> DocComment { + let mut doc = DocComment::default(); + let mut free: Vec = Vec::new(); + // Tag corrente e as linhas de continuação que a seguem. + let mut cur: Option<(String, String, Vec)> = None; + + let flush = |doc: &mut DocComment, cur: Option<(String, String, Vec)>| { + let Some((tag, head, rest)) = cur else { return }; + let mut all = vec![head]; + all.extend(rest); + let body = join_wrapped(&all); + match tag.as_str() { + "param" | "arg" => { + let mut it = body.splitn(2, char::is_whitespace); + let name = it.next().unwrap_or("").trim_end_matches("[]").to_string(); + let text = it.next().unwrap_or("").trim().to_string(); + if !name.is_empty() { + doc.params.push(DocParam { name, text }); + } + } + "return" | "returns" => doc.returns = Some(body), + "remarks" | "note" | "notes" => doc.remarks = Some(body), + // @seealso, @library, @deprecated e afins: o hover já mostra o + // aviso de descontinuado por outro caminho. + _ => {} + } + }; + + for l in lines { + let t = l.trim(); + if let Some(cap) = RX_TAG.captures(t) { + flush(&mut doc, cur.take()); + cur = Some((cap[1].to_string(), cap[2].trim().to_string(), Vec::new())); + } else if let Some((_, _, rest)) = cur.as_mut() { + // Linha em branco encerra a continuação da tag. + if t.is_empty() { + flush(&mut doc, cur.take()); + } else { + rest.push(t.to_string()); + } + } else { + free.push(t.to_string()); + } + } + flush(&mut doc, cur.take()); + + let text = join_wrapped(&free); + if !text.is_empty() { + // O primeiro parágrafo é o resumo; o resto, descrição. + let mut parts = text.splitn(2, "\n\n"); + doc.summary = parts.next().map(str::to_string).filter(|s| !s.is_empty()); + doc.description = parts.next().map(str::to_string).filter(|s| !s.is_empty()); + } + + doc +} + +#[cfg(test)] +mod tests { + use super::*; + + fn labels() -> DocLabels { + DocLabels { + params: "Parâmetros".into(), + returns: "Retorna:".into(), + remarks: "Notas:".into(), + } + } + + #[test] + fn javadoc_simple() { + let src = "/**\n * Ban a connected player's address, permanently, and disconnect them.\n *\n * @param playerid the player to ban\n * @return 1 always\n */"; + let d = parse_doc(src); + assert_eq!( + d.summary.as_deref(), + Some("Ban a connected player's address, permanently, and disconnect them.") + ); + assert_eq!(d.params.len(), 1); + assert_eq!(d.params[0].name, "playerid"); + assert_eq!(d.params[0].text, "the player to ban"); + assert_eq!(d.returns.as_deref(), Some("1 always")); + } + + #[test] + fn javadoc_multi_paragraph_and_array_param() { + let src = "/**\n * Ban for a limited time.\n *\n * SA-MP has no equivalent: every ban there is\n * forever.\n *\n * @param seconds how long it lasts; 0 means permanent\n * @param reason[] why, shown to them\n * @return 1 always\n */"; + let d = parse_doc(src); + assert_eq!(d.summary.as_deref(), Some("Ban for a limited time.")); + assert_eq!( + d.description.as_deref(), + Some("SA-MP has no equivalent: every ban there is forever.") + ); + // O `[]` é removido para casar com o nome do parâmetro na assinatura. + assert_eq!(d.params[1].name, "reason"); + assert_eq!(d.param("reason[]"), Some("why, shown to them")); + } + + #[test] + fn javadoc_tag_continuation_lines() { + let src = "/**\n * Doc.\n *\n * @param subject[] an address or a range\n * spanning many hosts\n * @return 1 on success, 0 if the subject\n * could not be read\n */"; + let d = parse_doc(src); + assert_eq!( + d.params[0].text, + "an address or a range spanning many hosts" + ); + assert_eq!( + d.returns.as_deref(), + Some("1 on success, 0 if the subject could not be read") + ); + } + + #[test] + fn xmldoc_open_mp() { + let src = r#"/** + * omp_actor + * Create a static 'actor' in the world. These 'actors' are like NPCs, however they have limited + * functionality. + * The model ID (skin ID) the actor should have + * The x coordinate to create the actor at + * + * + * + * Actors are limited to 1000 (MAX_ACTORS).
+ *
+ * + * The created Actor ID (start at 0).
+ * INVALID_ACTOR_ID If the actor limit is reached. + *
+ */"#; + let d = parse_doc(src); + assert_eq!( + d.summary.as_deref(), + Some( + "Create a static 'actor' in the world. These 'actors' are like NPCs, however they have limited functionality." + ) + ); + assert_eq!(d.params.len(), 2); + assert_eq!(d.params[0].name, "skin"); + assert_eq!( + d.param("x"), + Some("The x coordinate to create the actor at") + ); + // X vira **`X`**; e saem fora. + let r = d.returns.as_deref().unwrap(); + assert!(r.contains("**`0`**"), "returns: {r}"); + assert!(r.contains("**`INVALID_ACTOR_ID`**"), "returns: {r}"); + let md = d.to_markdown(&labels()).unwrap(); + assert!(!md.contains("seealso"), "{md}"); + assert!(!md.contains("omp_actor"), "{md}"); + } + + #[test] + fn xmldoc_inline_returns_and_anchor() { + let src = r##"/** + * Destroy an actor which was created with CreateActor. + * The ID of the actor to destroy + * 1 if streamed in, or 0 if it is + * not. + */"##; + let d = parse_doc(src); + // A âncora vira o texto puro: `#CreateActor` não resolve no editor. + assert_eq!( + d.summary.as_deref(), + Some("Destroy an actor which was created with CreateActor.") + ); + assert_eq!( + d.returns.as_deref(), + Some("**`1`** if streamed in, or **`0`** if it is not.") + ); + } + + #[test] + fn plain_comment_becomes_description() { + let d = parse_doc("// Devolve o nome do jogador.\n// Vazio se o id for inválido."); + assert_eq!( + d.summary.as_deref(), + Some("Devolve o nome do jogador. Vazio se o id for inválido.") + ); + assert!(d.params.is_empty()); + } + + /// O hover renderizado de cada formato, de ponta a ponta. + #[test] + fn end_to_end_rendering() { + let javadoc = "/**\n * Ban an address or a whole range that is not currently connected.\n *\n * Accepts either a plain address or CIDR notation.\n *\n * @param subject[] an address or a range\n * @param reason[] why, kept in the record\n * @param seconds how long it lasts; 0 means permanent\n * @return 1 on success, 0 otherwise\n */"; + let md = parse_doc(javadoc).to_markdown(&labels()).unwrap(); + assert!(md.starts_with("Ban an address or a whole range that is not currently connected.")); + assert!(md.contains("Accepts either a plain address or CIDR notation.")); + assert!(md.contains("- `subject` — an address or a range")); + assert!(md.contains("- `seconds` — how long it lasts; 0 means permanent")); + assert!(md.contains("**Retorna:** 1 on success, 0 otherwise")); + // Nenhuma tag crua sobrevive ao render. + assert!(!md.contains('@'), "{md}"); + + let xmldoc = "/**\n * omp_actor\n * Checks if an actor is streamed in for a player.\n * The ID of the actor\n * The ID of the player\n * \n * 1 if the actor is streamed in, or 0 if it is\n * not.\n */"; + let md = parse_doc(xmldoc).to_markdown(&labels()).unwrap(); + assert!(md.starts_with("Checks if an actor is streamed in for a player.")); + assert!(md.contains("- `actorid` — The ID of the actor")); + assert!(md.contains("**Retorna:** **`1`** if the actor is streamed in")); + assert!(!md.contains('<'), "{md}"); + } + + #[test] + fn empty_doc_is_empty() { + assert!(parse_doc("/**\n *\n */").is_empty()); + assert!(parse_doc("").is_empty()); + } + + #[test] + fn non_ascii_survives_html_conversion() { + let src = "/**\n * Cria um ator no mundo — não ocupa slot.\n */"; + let d = parse_doc(src); + assert_eq!( + d.summary.as_deref(), + Some("Cria um **ator** no mundo — não ocupa slot.") + ); + } + + #[test] + fn markdown_has_all_sections() { + let src = + "/**\n * Resumo.\n *\n * @param a primeiro\n * @return zero\n * @note cuidado\n */"; + let md = parse_doc(src).to_markdown(&labels()).unwrap(); + assert!(md.contains("Resumo.")); + assert!(md.contains("**Parâmetros**")); + assert!(md.contains("- `a` — primeiro")); + assert!(md.contains("**Retorna:** zero")); + assert!(md.contains("**Notas:** cuidado")); + } +} diff --git a/src/intellisense/hover.rs b/src/intellisense/hover.rs index 45eeda2..fbfd3f4 100644 --- a/src/intellisense/hover.rs +++ b/src/intellisense/hover.rs @@ -83,6 +83,14 @@ fn hover_include( }) } +pub(super) fn doc_labels(locale: crate::messages::Locale) -> super::DocLabels { + super::DocLabels { + params: msg(locale, MsgKey::HoverParams).to_string(), + returns: msg(locale, MsgKey::HoverReturns).to_string(), + remarks: msg(locale, MsgKey::HoverRemarks).to_string(), + } +} + fn format_symbol(sym: &Symbol, locale: crate::messages::Locale) -> Hover { let kw = match sym.kind { SymbolKind::Native => "native", @@ -113,21 +121,13 @@ fn format_symbol(sym: &Symbol, locale: crate::messages::Locale) -> Hover { let _ = write!(md, "\n\n> {}", msg(locale, MsgKey::HoverDeprecated)); } - if let Some(doc) = &sym.doc { - let clean: Vec<&str> = doc - .lines() - .map(|l| { - l.trim() - .trim_start_matches("//") - .trim_start_matches('*') - .trim_start_matches('/') - .trim() - }) - .filter(|l| !l.is_empty() && !l.starts_with("/*") && !l.starts_with("*/")) - .collect(); - if !clean.is_empty() { - let _ = write!(md, "\n\n---\n{}", clean.join("\n")); - } + if let Some(rendered) = sym + .doc + .as_deref() + .map(super::parse_doc) + .and_then(|d| d.to_markdown(&doc_labels(locale))) + { + let _ = write!(md, "\n\n---\n{rendered}"); } Hover { diff --git a/src/intellisense/mod.rs b/src/intellisense/mod.rs index c5e5182..15dea55 100644 --- a/src/intellisense/mod.rs +++ b/src/intellisense/mod.rs @@ -1,5 +1,6 @@ mod codelens; mod completion; +mod docs; mod format_engine; mod format_indent; mod format_style; @@ -13,6 +14,7 @@ mod signature; pub use codelens::get_code_lens; pub use completion::{get_at_completions, get_completions}; +pub use docs::{DocLabels, parse_doc}; pub use format_style::{BracePlacement, FormatStyle, Preset}; pub use formatter::{format_document, format_range}; pub use hover::get_hover; diff --git a/src/intellisense/signature.rs b/src/intellisense/signature.rs index f204711..ec2b391 100644 --- a/src/intellisense/signature.rs +++ b/src/intellisense/signature.rs @@ -5,7 +5,8 @@ use tower_lsp::lsp_types::{ use crate::workspace::WorkspaceState; -use super::collect_all_symbols; +use super::hover::doc_labels; +use super::{collect_all_symbols, parse_doc}; use crate::util::to_u32; pub fn get_signature_help( @@ -37,6 +38,9 @@ pub fn get_signature_help( .iter() .find(|s| s.name == func_name && s.signature.is_some())?; + let doc = sym.doc.as_deref().map(parse_doc); + let labels = doc_labels(state.locale); + let param_infos: Vec = sym .params .iter() @@ -46,9 +50,21 @@ pub fn get_signature_help( } else { p.name.clone() }; + // A doc do parâmetro casa pelo nome, não pela posição: um + // comentário pode omitir parâmetros ou listá-los fora de ordem. + let documentation = doc + .as_ref() + .and_then(|d| d.param(&p.name)) + .filter(|t| !t.is_empty()) + .map(|t| { + Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: t.to_string(), + }) + }); ParameterInformation { label: ParameterLabel::Simple(label), - documentation: None, + documentation, } }) .collect(); @@ -58,10 +74,10 @@ pub fn get_signature_help( Some(SignatureHelp { signatures: vec![SignatureInformation { label: sym.signature.clone().unwrap_or_default(), - documentation: sym.doc.as_ref().map(|d| { + documentation: doc.as_ref().and_then(|d| d.to_markdown(&labels)).map(|v| { Documentation::MarkupContent(MarkupContent { kind: MarkupKind::Markdown, - value: d.clone(), + value: v, }) }), parameters: Some(param_infos), diff --git a/src/messages/langs/en.rs b/src/messages/langs/en.rs index 5648b90..e64a345 100644 --- a/src/messages/langs/en.rs +++ b/src/messages/langs/en.rs @@ -34,6 +34,9 @@ pub fn get(key: MsgKey) -> &'static str { MsgKey::RefsOne => "1 reference", MsgKey::RefsMany => "{n} references", MsgKey::HoverDeprecated => "**Deprecated**", + MsgKey::HoverParams => "Parameters", + MsgKey::HoverReturns => "Returns:", + MsgKey::HoverRemarks => "Notes:", MsgKey::KwIf => "if (condition) { }", MsgKey::KwIfElse => "if/else", MsgKey::KwElse => "else", @@ -77,7 +80,9 @@ pub fn get(key: MsgKey) -> &'static str { MsgKey::KwAssert => "#assert condition (compile-time)", MsgKey::KwError => "#error message (compile-time)", MsgKey::KwWarning => "#warning message (compile-time)", - MsgKey::KwAtDeprecated => "Marks the next symbol as deprecated", + MsgKey::DocTagParam => "Documents a parameter", + MsgKey::DocTagReturn => "Documents the return value", + MsgKey::DocTagRemarks => "Additional notes", MsgKey::KwLocal => "local", MsgKey::NameTooShort => "\"{}\" is very short — consider a more descriptive name", MsgKey::NamePlaceholder => "\"{}\" is a generic name — consider a more descriptive one", diff --git a/src/messages/langs/es.rs b/src/messages/langs/es.rs index 0f85a05..f965a56 100644 --- a/src/messages/langs/es.rs +++ b/src/messages/langs/es.rs @@ -43,6 +43,9 @@ pub fn get(key: MsgKey) -> &'static str { MsgKey::RefsOne => "1 referencia", MsgKey::RefsMany => "{n} referencias", MsgKey::HoverDeprecated => "**Obsoleto**", + MsgKey::HoverParams => "Parámetros", + MsgKey::HoverReturns => "Devuelve:", + MsgKey::HoverRemarks => "Notas:", MsgKey::KwIf => "if (condición) { }", MsgKey::KwIfElse => "if/else", MsgKey::KwElse => "else", @@ -86,7 +89,9 @@ pub fn get(key: MsgKey) -> &'static str { MsgKey::KwAssert => "#assert condición (en compilación)", MsgKey::KwError => "mensaje #error (en compilación)", MsgKey::KwWarning => "mensaje #warning (en compilación)", - MsgKey::KwAtDeprecated => "Marca el siguiente símbolo como obsoleto", + MsgKey::DocTagParam => "Documenta un parámetro", + MsgKey::DocTagReturn => "Documenta el valor de retorno", + MsgKey::DocTagRemarks => "Notas adicionales", MsgKey::KwLocal => "local", MsgKey::NameTooShort => "\"{}\" es muy corto — considera un nombre más descriptivo", MsgKey::NamePlaceholder => "\"{}\" es un nombre genérico — considera uno más descriptivo", diff --git a/src/messages/langs/pt_br.rs b/src/messages/langs/pt_br.rs index 3b732e3..73e3f1c 100644 --- a/src/messages/langs/pt_br.rs +++ b/src/messages/langs/pt_br.rs @@ -38,6 +38,9 @@ pub fn get(key: MsgKey) -> &'static str { MsgKey::RefsOne => "1 referência", MsgKey::RefsMany => "{n} referências", MsgKey::HoverDeprecated => "**Depreciado**", + MsgKey::HoverParams => "Parâmetros", + MsgKey::HoverReturns => "Retorna:", + MsgKey::HoverRemarks => "Notas:", MsgKey::KwIf => "if (condição) { }", MsgKey::KwIfElse => "if/else", MsgKey::KwElse => "else", @@ -81,7 +84,9 @@ pub fn get(key: MsgKey) -> &'static str { MsgKey::KwAssert => "#assert condição (compile-time)", MsgKey::KwError => "#error mensagem (compile-time)", MsgKey::KwWarning => "#warning mensagem (compile-time)", - MsgKey::KwAtDeprecated => "Marca o símbolo seguinte como depreciado", + MsgKey::DocTagParam => "Documenta um parâmetro", + MsgKey::DocTagReturn => "Documenta o valor de retorno", + MsgKey::DocTagRemarks => "Notas adicionais", MsgKey::KwLocal => "local", MsgKey::NameTooShort => "\"{}\" é muito curto — considere um nome mais descritivo", MsgKey::NamePlaceholder => "\"{}\" é um nome genérico — considere um mais descritivo", diff --git a/src/messages/langs/ro.rs b/src/messages/langs/ro.rs index a688d1a..2368e49 100644 --- a/src/messages/langs/ro.rs +++ b/src/messages/langs/ro.rs @@ -43,6 +43,9 @@ pub fn get(key: MsgKey) -> &'static str { MsgKey::RefsOne => "1 referință", MsgKey::RefsMany => "{n} referințe", MsgKey::HoverDeprecated => "**Învechit**", + MsgKey::HoverParams => "Parametri", + MsgKey::HoverReturns => "Returnează:", + MsgKey::HoverRemarks => "Note:", MsgKey::KwIf => "if (condiție) { }", MsgKey::KwIfElse => "if/else", MsgKey::KwElse => "else", @@ -86,7 +89,9 @@ pub fn get(key: MsgKey) -> &'static str { MsgKey::KwAssert => "#assert condiție (la compilare)", MsgKey::KwError => "mesaj #error (la compilare)", MsgKey::KwWarning => "mesaj #warning (la compilare)", - MsgKey::KwAtDeprecated => "Marchează următorul simbol ca învechit", + MsgKey::DocTagParam => "Documentează un parametru", + MsgKey::DocTagReturn => "Documentează valoarea returnată", + MsgKey::DocTagRemarks => "Note suplimentare", MsgKey::KwLocal => "local", MsgKey::NameTooShort => "\"{}\" este foarte scurt — ia în calcul un nume mai descriptiv", MsgKey::NamePlaceholder => "\"{}\" este un nume generic — ia în calcul unul mai descriptiv", diff --git a/src/messages/langs/ru.rs b/src/messages/langs/ru.rs index 5b69ae0..f09e242 100644 --- a/src/messages/langs/ru.rs +++ b/src/messages/langs/ru.rs @@ -39,6 +39,9 @@ pub fn get(key: MsgKey) -> &'static str { MsgKey::RefsOne => "1 ссылка", MsgKey::RefsMany => "{n} ссылок", MsgKey::HoverDeprecated => "**Устарело**", + MsgKey::HoverParams => "Параметры", + MsgKey::HoverReturns => "Возвращает:", + MsgKey::HoverRemarks => "Примечания:", MsgKey::KwIf => "if (условие) { }", MsgKey::KwIfElse => "if/else", MsgKey::KwElse => "else", @@ -82,7 +85,9 @@ pub fn get(key: MsgKey) -> &'static str { MsgKey::KwAssert => "#assert условие (во время компиляции)", MsgKey::KwError => "сообщение #error (во время компиляции)", MsgKey::KwWarning => "сообщение #warning (во время компиляции)", - MsgKey::KwAtDeprecated => "Помечает следующий символ как устаревший", + MsgKey::DocTagParam => "Описывает параметр", + MsgKey::DocTagReturn => "Описывает возвращаемое значение", + MsgKey::DocTagRemarks => "Дополнительные заметки", MsgKey::KwLocal => "локальная", MsgKey::NameTooShort => "\"{}\" очень короткое — стоит выбрать более описательное имя", MsgKey::NamePlaceholder => "\"{}\" — общее имя — стоит выбрать более описательное", diff --git a/src/messages/mod.rs b/src/messages/mod.rs index bc11e40..5fe52d3 100644 --- a/src/messages/mod.rs +++ b/src/messages/mod.rs @@ -58,6 +58,9 @@ pub enum MsgKey { RefsOne, RefsMany, HoverDeprecated, + HoverParams, + HoverReturns, + HoverRemarks, KwIf, KwIfElse, KwElse, @@ -101,7 +104,9 @@ pub enum MsgKey { KwAssert, KwError, KwWarning, - KwAtDeprecated, + DocTagParam, + DocTagReturn, + DocTagRemarks, KwLocal, NameTooShort, NamePlaceholder, @@ -137,6 +142,9 @@ mod tests { // Toda variante deve devolver texto não vazio para uma chave qualquer. for loc in [Locale::PtBr, Locale::Es, Locale::Ru, Locale::Ro, Locale::En] { assert!(!msg(loc, MsgKey::HoverDeprecated).is_empty()); + assert!(!msg(loc, MsgKey::HoverParams).is_empty()); + assert!(!msg(loc, MsgKey::HoverReturns).is_empty()); + assert!(!msg(loc, MsgKey::HoverRemarks).is_empty()); } } } diff --git a/src/parser/lexer.rs b/src/parser/lexer.rs index 14a1d17..b0995e2 100644 --- a/src/parser/lexer.rs +++ b/src/parser/lexer.rs @@ -29,19 +29,26 @@ pub fn decode_bytes(bytes: &[u8]) -> String { } } -pub fn has_inline_deprecated(raw_line: &str) -> bool { - if let Some(p) = raw_line.find("//") - && raw_line[p..].contains("@DEPRECATED") - { - return true; +/// Reconhece `#pragma deprecated [mensagem]`, que marca o **próximo** símbolo +/// declarado — a diretiva do compilador Pawn, que emite o warning 234 com a +/// mensagem quando o símbolo é usado. +/// +/// Devolve a mensagem (vazia quando a diretiva não traz uma). +pub fn pragma_deprecated_message(raw_line: &str) -> Option { + let t = raw_line.trim(); + let rest = t.strip_prefix('#')?.trim_start(); + let rest = rest.strip_prefix("pragma")?; + // Exige separador: `#pragmadeprecated` não é a diretiva. + if !rest.starts_with(|c: char| c.is_whitespace()) { + return None; } - if let Some(p) = raw_line.find("/*") { - let end = raw_line[p..].find("*/").map_or(raw_line.len(), |q| p + q); - if raw_line[p..end].contains("@DEPRECATED") { - return true; - } + let rest = rest.trim_start().strip_prefix("deprecated")?; + if !rest.is_empty() && !rest.starts_with(|c: char| c.is_whitespace()) { + return None; } - false + // Um comentário na mesma linha não faz parte da mensagem. + let msg = strip_line_comments(rest, false).text; + Some(msg.trim().to_string()) } #[derive(Debug)] diff --git a/src/parser/symbols.rs b/src/parser/symbols.rs index 5435271..896e640 100644 --- a/src/parser/symbols.rs +++ b/src/parser/symbols.rs @@ -1,15 +1,11 @@ use regex::Regex; use super::{ - lexer::{has_inline_deprecated, strip_line_comments, update_brace_depth}, - types::{IncludeDirective, Param, ParsedFile, Symbol, SymbolKind}, + lexer::{pragma_deprecated_message, strip_line_comments, update_brace_depth}, + types::{Deprecation, IncludeDirective, Param, ParsedFile, Symbol, SymbolKind}, }; use crate::util::to_u32; -static RX_DEPRECATED: std::sync::LazyLock = std::sync::LazyLock::new(|| { - Regex::new(r"^\s*(?://\s*@DEPRECATED|/\*\s*@DEPRECATED\s*\*/)\s*$").unwrap() -}); - static RX_NATIVE: std::sync::LazyLock = std::sync::LazyLock::new(|| { Regex::new(r"^\s*(?:forward\s+)?native\s+(?:[A-Za-z_]\w*::)*(?:[A-Za-z_]\w*:)?\s*([A-Za-z_]\w*)\s*\(([^)]*)\)").unwrap() }); @@ -302,7 +298,7 @@ fn push_func( name: String, params_raw: &str, kind: SymbolKind, - deprecated: bool, + deprecated: &Deprecation, ) { let params = parse_params(params_raw); let col = to_u32(raw_line.find(name.as_str()).unwrap_or(0)); @@ -311,7 +307,8 @@ fn push_func( name, kind, params, - deprecated, + deprecated: deprecated.is_deprecated, + deprecated_message: deprecated.message.clone(), doc: extract_doc(raw_lines, line_idx), line: to_u32(line_idx), col, @@ -321,11 +318,27 @@ fn push_func( /// Assinatura de função cuja `(...)` se estende por várias linhas, acumulada /// entre o `(` de abertura e o `)` de fechamento: /// `(line, col, name, params_acc, kind, deprecated, doc)`. -type MultilineFunc = (u32, u32, String, String, SymbolKind, bool, Option); +type MultilineFunc = ( + u32, + u32, + String, + String, + SymbolKind, + Deprecation, + Option, +); /// Função `plain` (sem keyword) cuja `{` ainda não apareceu — pode estar na /// linha seguinte: `(line, col, name, params, deprecated, doc, kind)`. -type PendingPlain = (u32, u32, String, String, bool, Option, SymbolKind); +type PendingPlain = ( + u32, + u32, + String, + String, + Deprecation, + Option, + SymbolKind, +); /// Concatena um trecho de parâmetros ao acumulador, inserindo `", "` como /// separador apenas quando necessário — evita vírgula dupla (`a,, b`) quando o @@ -354,7 +367,7 @@ fn push_multi_var_names( raw_line: &str, trimmed: &str, line_idx: usize, - deprecated: bool, + deprecated: &Deprecation, ) { for name in extract_var_names(trimmed) { if RESERVED.contains(name.as_str()) { @@ -366,7 +379,8 @@ fn push_multi_var_names( kind: SymbolKind::Variable, signature: None, params: vec![], - deprecated, + deprecated: deprecated.is_deprecated, + deprecated_message: deprecated.message.clone(), doc: None, line: to_u32(line_idx), col, @@ -428,7 +442,8 @@ fn continue_multiline_func( name: pname, kind: pkind, params: parsed_params, - deprecated: pdep, + deprecated: pdep.is_deprecated, + deprecated_message: pdep.message.clone(), doc: pdoc, line: pidx, col: pcol, @@ -453,7 +468,7 @@ struct ParserState { result: ParsedFile, in_block: bool, depth: i32, - pending_deprecated: bool, + pending_deprecated: Deprecation, /// Função cuja `{` ainda não apareceu (pode estar na linha seguinte). pending_plain: Option, /// Parâmetros acumulados de assinatura multi-linha entre `(` ... `)`. @@ -482,9 +497,10 @@ impl ParserState { /// adequada. Cada handler devolve `true` quando consumiu a linha (equivalente /// ao antigo `continue`); ao final, a profundidade de chaves é atualizada. fn process_line(&mut self, raw_line: &str, line_idx: usize, raw_lines: &[&str]) { - // @DEPRECATED deve ser verificado no rawLine, antes do strip - if RX_DEPRECATED.is_match(raw_line) { - self.pending_deprecated = true; + // `#pragma deprecated` marca o próximo símbolo declarado; a mensagem + // que a segue é repassada no aviso de uso. + if let Some(m) = pragma_deprecated_message(raw_line) { + self.pending_deprecated = Deprecation::marked((!m.is_empty()).then_some(m)); let stripped = strip_line_comments(raw_line, self.in_block); self.in_block = stripped.in_block; self.depth = update_brace_depth(&stripped.text, self.depth); @@ -566,7 +582,7 @@ impl ParserState { raw_line, trimmed, line_idx, - self.pending_deprecated, + &self.pending_deprecated, ); if trimmed.contains(';') || trimmed.contains('{') { self.in_multi_var_decl = false; @@ -588,7 +604,8 @@ impl ParserState { name: pname, kind: pkind, params, - deprecated: pdep, + deprecated: pdep.is_deprecated, + deprecated_message: pdep.message.clone(), doc: pdoc, line: pidx, col: pcol, @@ -596,9 +613,7 @@ impl ParserState { return; } - let inline_deprecated = has_inline_deprecated(raw_line); - let deprecated = self.pending_deprecated || inline_deprecated; - self.pending_deprecated = false; + let deprecated = std::mem::take(&mut self.pending_deprecated); if let Some(cap) = RX_INCLUDE.captures(line) { let is_try = cap.get(1).is_some(); @@ -627,7 +642,8 @@ impl ParserState { kind: SymbolKind::Enum, signature: None, params: vec![], - deprecated, + deprecated: deprecated.is_deprecated, + deprecated_message: deprecated.message.clone(), doc: extract_doc(raw_lines, line_idx), line: to_u32(line_idx), col, @@ -651,6 +667,7 @@ impl ParserState { signature: None, params: vec![], deprecated: false, + deprecated_message: None, doc: None, line: to_u32(line_idx), col, @@ -678,7 +695,7 @@ impl ParserState { }; self.result.macro_names.push(name.clone()); - if deprecated { + if deprecated.is_deprecated { self.result.deprecated_macros.push(name.clone()); } if is_func_macro { @@ -691,7 +708,8 @@ impl ParserState { kind: SymbolKind::Define, signature: None, params: vec![], - deprecated, + deprecated: deprecated.is_deprecated, + deprecated_message: deprecated.message.clone(), doc: extract_doc(raw_lines, line_idx), line: to_u32(line_idx), col, @@ -709,7 +727,7 @@ impl ParserState { return; } - if self.try_function_decl(line, raw_line, line_idx, raw_lines, deprecated) { + if self.try_function_decl(line, raw_line, line_idx, raw_lines, &deprecated) { return; } @@ -728,7 +746,8 @@ impl ParserState { kind: kind.clone(), signature: None, params: vec![], - deprecated, + deprecated: deprecated.is_deprecated, + deprecated_message: deprecated.message.clone(), doc: None, line: to_u32(line_idx), col, @@ -746,7 +765,8 @@ impl ParserState { kind: SymbolKind::Variable, signature: None, params: vec![], - deprecated, + deprecated: deprecated.is_deprecated, + deprecated_message: deprecated.message.clone(), doc: None, line: to_u32(line_idx), col, @@ -758,7 +778,7 @@ impl ParserState { /// Detecção dentro de um corpo aninhado (`depth > 0`): basicamente membros /// de `enum` quando ele se estende por várias linhas. fn process_nested(&mut self, trimmed: &str, raw_line: &str, line_idx: usize) { - self.pending_deprecated = false; + self.pending_deprecated = Deprecation::NONE; self.in_multi_var_decl = false; if self.in_enum @@ -776,6 +796,7 @@ impl ParserState { signature: None, params: vec![], deprecated: false, + deprecated_message: None, doc: None, line: to_u32(line_idx), col, @@ -797,7 +818,7 @@ impl ParserState { raw_line: &str, line_idx: usize, raw_lines: &[&str], - deprecated: bool, + deprecated: &Deprecation, ) -> bool { if let Some(cap) = RX_NATIVE.captures(line) { let params_raw = cap.get(2).map_or("", |m| m.as_str()); @@ -872,7 +893,8 @@ impl ParserState { kind: SymbolKind::StaticConst, signature: None, params: vec![], - deprecated, + deprecated: deprecated.is_deprecated, + deprecated_message: deprecated.message.clone(), doc: extract_doc(raw_lines, line_idx), line: to_u32(line_idx), col, @@ -888,7 +910,8 @@ impl ParserState { kind: SymbolKind::StaticConst, signature: None, params: vec![], - deprecated, + deprecated: deprecated.is_deprecated, + deprecated_message: deprecated.message.clone(), doc: extract_doc(raw_lines, line_idx), line: to_u32(line_idx), col, @@ -937,7 +960,8 @@ impl ParserState { kind: SymbolKind::Stock, signature, params, - deprecated, + deprecated: deprecated.is_deprecated, + deprecated_message: deprecated.message.clone(), doc: extract_doc(raw_lines, line_idx), line: to_u32(line_idx), col, @@ -972,7 +996,7 @@ impl ParserState { col, name, params_raw.to_string(), - deprecated, + deprecated.clone(), extract_doc(raw_lines, line_idx), kind, )); @@ -1022,7 +1046,7 @@ impl ParserState { effective_name, partial.to_string(), kind, - deprecated, + deprecated.clone(), extract_doc(raw_lines, line_idx), )); return true; @@ -1084,15 +1108,54 @@ mod tests { #[test] fn parses_deprecated() { - let src = "// @DEPRECATED\nstock OldFunc() {}"; + let src = "#pragma deprecated\nstock OldFunc() {}"; let f = parse_file(src); - assert!( - f.symbols - .iter() - .any(|s| s.name == "OldFunc" && s.deprecated) + let sym = f.symbols.iter().find(|s| s.name == "OldFunc").unwrap(); + assert!(sym.deprecated); + assert_eq!(sym.deprecated_message, None); + } + + #[test] + fn parses_deprecated_with_message() { + let src = "#pragma deprecated Use NewFunc instead\nstock OldFunc() {}"; + let f = parse_file(src); + let sym = f.symbols.iter().find(|s| s.name == "OldFunc").unwrap(); + assert!(sym.deprecated); + assert_eq!( + sym.deprecated_message.as_deref(), + Some("Use NewFunc instead") ); } + #[test] + fn deprecated_marks_only_the_next_symbol() { + let src = "#pragma deprecated old\nstock A() {}\nstock B() {}"; + let f = parse_file(src); + assert!(f.symbols.iter().find(|s| s.name == "A").unwrap().deprecated); + assert!(!f.symbols.iter().find(|s| s.name == "B").unwrap().deprecated); + } + + #[test] + fn deprecated_survives_multiline_signature() { + let src = + "#pragma deprecated banido\nstock BanEx(\n playerid,\n const reason[]\n)\n{\n}"; + let f = parse_file(src); + let sym = f.symbols.iter().find(|s| s.name == "BanEx").unwrap(); + assert!(sym.deprecated); + assert_eq!(sym.deprecated_message.as_deref(), Some("banido")); + } + + #[test] + fn deprecated_ignores_other_pragmas_and_comment_marker() { + // `@DEPRECATED` em comentário não é mais reconhecido: a diretiva do + // compilador é a única fonte. + let f = parse_file("// @DEPRECATED\nstock A() {}"); + assert!(!f.symbols.iter().find(|s| s.name == "A").unwrap().deprecated); + + let f = parse_file("#pragma tabsize 4\nstock B() {}"); + assert!(!f.symbols.iter().find(|s| s.name == "B").unwrap().deprecated); + } + #[test] fn parses_include_angle() { let src = "#include "; diff --git a/src/parser/types.rs b/src/parser/types.rs index acec81e..a000c1e 100644 --- a/src/parser/types.rs +++ b/src/parser/types.rs @@ -26,6 +26,28 @@ pub struct Param { pub is_variadic: bool, // "..." } +/// Marcação de `#pragma deprecated`: se o símbolo está descontinuado e, quando +/// a diretiva trouxe uma, a mensagem que acompanha o aviso de uso. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct Deprecation { + pub is_deprecated: bool, + pub message: Option, +} + +impl Deprecation { + pub const NONE: Self = Self { + is_deprecated: false, + message: None, + }; + + pub fn marked(message: Option) -> Self { + Self { + is_deprecated: true, + message, + } + } +} + #[derive(Debug, Clone)] pub struct Symbol { pub name: String, @@ -33,6 +55,9 @@ pub struct Symbol { pub signature: Option, pub params: Vec, pub deprecated: bool, + /// Texto após `#pragma deprecated`, repassado no aviso de uso. `None` + /// quando a diretiva não trouxe mensagem. + pub deprecated_message: Option, pub doc: Option, pub line: u32, pub col: u32, diff --git a/src/server.rs b/src/server.rs index 306dddf..fc574f5 100644 --- a/src/server.rs +++ b/src/server.rs @@ -403,7 +403,7 @@ impl PawnProServer { let before = &line[..col_bytes]; before.contains("//") || before.contains("/*") || line.trim_start().starts_with('*') }); - intellisense::get_at_completions(in_comment, pos.line, at_col, state.locale) + intellisense::get_at_completions(in_comment, state.locale) }) .await .unwrap_or_default()