Servidor LSP em Rust para a linguagem Pawn (SA-MP / open.mp). Comunica-se com a extensão PawnPro via stdin/stdout usando o Language Server Protocol.
- Zero
unwrap()em código de produção fora deLazy::new. Usar?,if let,unwrap_or, ouunwrap_or_else. - Nunca usar
panic!em caminhos de análise. O motor precisa sobreviver a qualquer entrada malformada. - Regexes estáticas via
once_cell::sync::Lazy<Regex>. Nunca compilar regex dentro de loops. - Nunca interpolar input do usuário diretamente em
Regex::newsem escapar metacaracteres. cargo clippy -- -D warningsdeve passar sem erros antes de qualquer commit.- Novos diagnósticos sempre em
analyzer/codes.rscom constantepub const PP####. parsed_cacheusaPathBufcomo chave, nãoString(URI). Converter URI → path antes de acessar.dep_graphusaPathBufem ambos os lados — nuncaString/URI. Converter antes de inserir.- Sem comentários óbvios. Apenas comentários que explicam por quê — restrições ocultas, invariantes sutis, workarounds de bugs específicos.
src/
main.rs ← entry point LSP (tower-lsp)
server.rs ← handlers LSP: initialize, completion, hover, etc.
workspace.rs ← analisa um arquivo: chama parser + todos os analyzers
config.rs ← EngineConfig recebida via initializationOptions
parser/
lexer.rs ← decode_bytes, strip_line_comments, update_brace_depth,
has_inline_deprecated — 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
analyzer/
codes.rs ← constantes PP0001–PP0013
diagnostic.rs ← PawnDiagnostic, Severity, construtores
includes.rs ← PP0001, PP0013 — resolve #include / #tryinclude
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
undefined.rs ← PP0010 — funções não declaradas
mod.rs ← re-exports + função analyze() que orquestra tudo
intellisense/
completion.rs ← textDocument/completion
hover.rs ← textDocument/hover
signature.rs ← textDocument/signatureHelp
codelens.rs ← textDocument/codeLens + codeLens/resolve
references.rs ← textDocument/references
semantic_tokens.rs ← textDocument/semanticTokens/full
mod.rs
pub struct WorkspaceState {
pub parsed_cache: DashMap<PathBuf, Arc<ParsedFile>>,
pub dep_graph: DashMap<PathBuf, HashSet<PathBuf>>, // include_path → paths que o incluem
pub tabsize_cache: Mutex<Option<Option<u32>>>,
// ...
}parsed_cacheusaPathBufcomo chave — nunca String/URI.dep_graphé o grafo reverso de dependências puro em paths:c.inc → {b.inc},b.inc → {a.pwn}. Permite invalidar transitivamente pelo BFS.tabsize_cacheusaMutexpara interior mutability em&self.
Métodos relevantes:
open_dependents(uri)— BFS nodep_grapha partir de um URI, retorna URIs dos arquivos abertos no editor que dependem transitivamente dele.evict_dependents(path)— BFS nodep_graphremovendo doparsed_cachetodos os dependentes transitivos.evict_path_from_cache(path)— remove o path e seus dependentes do cache (usado pordid_change_watched_files).
Armazenado como Arc<ParsedFile> no cache. Contém:
symbols: Vec<Symbol>— todas as declaraçõesincludes: Vec<IncludeDirective>— todas as diretivas#include/#tryincludemacro_names: Vec<String>— nomes de#definedeprecated_macros: Vec<String>— macros marcadas com@DEPRECATEDfunc_macro_prefixes: Vec<String>— prefixos comoCMD,BPRnamespace_aliases: HashMap<String, String>
pub struct ResolvedIncludes {
pub paths: Vec<PathBuf>,
pub files: HashMap<PathBuf, IncludeEntry>,
pub reverse_deps: HashMap<PathBuf, HashSet<PathBuf>>,
}reverse_deps é construído durante a resolução BFS e usado por record_dependencies em workspace.rs para atualizar dep_graph.
kind: SymbolKind—Native | Forward | Public | Stock | Static | Plain | StaticConst | Enum | Define | Variable | ConstPlain— função sem keyword (global não-stock); não exportada no AMXStaticConst— constante: membro de enum,stock const,static constEnum— nome do enum declarado (enum NomeDoEnum { ... })Const— constante declarada comconst
deprecated: bool— marcado com@DEPRECATEDdoc: Option<String>— comentário de documentação acima da declaraçãoline: u32,col: u32— posição 0-based em bytes UTF-8
Construtores disponíveis:
::error(...)— Severity::Error::warning(...)— Severity::Warning::unnecessary_warning(...)— Warning +unnecessary: true(texto desbotado no editor)::hint(...)— Severity::Hint +unnecessary: true::deprecated_decl(...)— Warning +deprecated: true(na própria declaração)::deprecated_warning(...)— Warning +deprecated: true(nos usos)
Struct extraída para eliminar duplicação entre initialize e did_change_configuration:
from_init_options(value)— lê deinitializationOptionsfrom_settings(value)— lê deworkspace/didChangeConfigurationapply_init(&mut config)— aplica campos relevantes ao initapply_change(&mut config)— aplica campos relevantes à atualização
Para cada arquivo aberto ou alterado:
decode_bytes(bytes)— decodifica UTF-8 com fallback latin-1parse_file(text, path)— extraiParsedFile, armazena comoArc<ParsedFile>emparsed_cacheresolve_includes(parsed, path, include_paths)— resolve recursivamente todos os includes transitivos →ResolvedIncludes(incluireverse_deps)record_dependencies(&resolved.reverse_deps)— atualizadep_graph- Cada analyzer recebe
(text, path, parsed, resolved)e retornaVec<PawnDiagnostic> - Diagnósticos publicados via
client.publish_diagnostics()
dep_graph: DashMap<PathBuf, HashSet<PathBuf>> mapeia cada include para o conjunto de arquivos que o incluem diretamente. record_dependencies propaga o mapa completo de reverse_deps retornado por collect_included_files — preservando a estrutura c.inc → {b.inc}, b.inc → {a.pwn}.
Quando um include muda:
evict_dependents(path)— BFS sobredep_graph, remove doparsed_cachetodos os dependentes transitivos.open_dependents(uri)— BFS sobredep_graph, coleta os URIs dos arquivos abertos que dependem transitivamente do include alterado.- Os handlers
did_change,did_saveedid_change_watched_fileschamamopen_dependentse republicam diagnósticos para cada dependente aberto — garantindo quemain.pwnreceba diagnósticos atualizados quandob.incouc.incmudam, mesmo que o usuário não editemain.pwn.
- Adicionar constante em
analyzer/codes.rs:pub const PP00XX: &str = "PP00XX"; - Criar ou editar o analyzer correspondente em
analyzer/ - Chamar o analyzer em
analyzer/mod.rsdentro deanalyze() - Documentar em
docs/diagnostics.md
- Implementar em
intellisense/ - Registrar o handler em
server.rs - Declarar a capability em
server_capabilities()emserver.rs - Documentar em
docs/lsp.md
Funções utilitárias canônicas — não duplicar em outros módulos:
| Função | Descrição |
|---|---|
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 |
pub struct EngineConfig {
pub include_paths: Vec<String>, // suporta ${workspaceFolder}
pub analysis: AnalysisConfig,
}
pub struct AnalysisConfig {
pub warn_unused_in_inc: bool,
pub suppress_diagnostics_in_inc: bool,
pub sdk: SdkConfig,
}
pub struct SdkConfig {
pub platform: String,
pub file_path: String,
}Carregada automaticamente de ~/.pawnpro/config.json (global) e .pawnpro/config.json (projeto). Merge: projeto sobrescreve global; valores não-default ganham. ${workspaceFolder} é substituído em runtime por resolved_include_paths().
Campos aceitos em initializationOptions e em workspace/didChangeConfiguration:
| Campo JSON | Tipo | Destino em WorkspaceState |
|---|---|---|
includePaths |
string[] |
include_paths_override |
warnUnusedInInc |
bool |
config.analysis.warn_unused_in_inc |
suppressDiagnosticsInInc |
bool |
config.analysis.suppress_diagnostics_in_inc |
sdkFilePath |
string |
sdk_file |
locale |
string |
locale ("pt-BR" → Locale::PtBr) |
apply_init aplica na inicialização; apply_change aplica em tempo real e retorna bool indicando se algo mudou (para controlar o republish). Nunca duplicar lógica de parsing entre os dois caminhos — usar ConfigUpdate.
cargo build # debug
cargo build --release # release (LTO + strip + panic=abort)
cargo test # testes unitários
cargo clippy -- -D warnings # deve passar sem errosupdate_brace_depthemlexer.rsé a versão canônica — lida com literais string/char. Não criar versões alternativas em outros módulos.- Diagnósticos PP0005/PP0006 usam
unnecessary_warning, nãowarning— isso ativa o estilo "desbotado" no editor. - PP0010 não é emitido em arquivos
.inc(apenas.pwn). - PP0011/PP0012/PP0013 são emitidos como Hint, não Warning — evitar ruído em bibliotecas.
is_try: falsedeve ser definido explicitamente em todos os literaisIncludeDirectivefora do parser.- Semantic tokens detectam chamadas multiline: olham até 3 linhas adiante por
(. deprecated_declusadeprecated: true— isso ativa o strikethrough no editor viaDiagnosticTag::DEPRECATED. Não usarwarningsimples para declarações depreciadas.Arc<ParsedFile>: ao estender símbolos de um include, usar.clone()—all.extend(inc_parsed.symbols.clone()).unused.rsusacollect_workspace_all()— função única que faz um único walkdir em vez de três chamadas separadas acollect_workspace().open_docsna engine guarda a chave como URI completa (file:///...). Nunca fazerformat!("file://{}", key)— já tem o prefixo.dep_graphguardaPathBuf → HashSet<PathBuf>— nunca URIs.open_dependentsfaz a conversão path→URI na saída, não na entrada.collect_transitive_exportsemunused.rsfaz BFS sobreResolvedIncludespara coletar símbolos diretos e transitivos de um include antes de emitir PP0012 — não checar apenasentry.parsed.symbolsdiretamente.did_change/did_save/did_change_watched_filesemserver.rsrepublicam os dependentes abertos viaopen_dependents, não apenas o arquivo alterado.