Repository navigation
fix(lsp): robustness of command arguments, URIs and positions - #928
Merged
Merged
Conversation
- pgls.executeStatement indexed its arguments without checking them, so a call without arguments panicked. Missing or malformed arguments are now an InvalidParams error. - Documents of other schemes than file: shared the workspace document of the file with the same path: the git: side of a diff, every cell of a notebook. Closing the diff closed the file's document. They are now keyed by their whole URI. - Positions past the end of a line or of the document failed the whole request. They are clamped, as the LSP specifies, and an inlay hint that can't be placed is left out instead of failing the response.
This was referenced Oct 24, 2026
# Conflicts: # crates/pgls_lsp/tests/server.rs
psteinroe
added a commit
that referenced
this pull request
Oct 26, 2026
#943) Stacked on #928. Adds a `[workspace.lints]` table and opts every member crate into it (`[lints] workspace = true`, also in the `just new-crate` template), and turns on the panic lints for `pgls_lsp`, the crate that handles client input first. **Workspace lints** `clippy::todo`, `clippy::unimplemented` and `clippy::dbg_macro` are now denied everywhere (`dbg!` stays allowed in tests, as before). The existing occurrences are fixed, not allowed: - `MemoryFileSystem::resolve_configuration` was a `todo!()`, so a configuration that `extends` a package name (not a path) panicked on the memory file system, which the WASM build uses. It returns `ResolveError::NotFound` now, which is reported as "can't resolve" like an unresolvable package on disk. - The TypeScript bindings generator had `unimplemented!()` for array and object literals and for boolean schemas. It now prints `[a, b]`, `{ "k": v }`, `unknown` and `never`. The generated bindings don't change. - `generate_tool_configuration` returns an error for `assists` and the transformation category instead of panicking. `unreachable!()` got a message. **pgls_lsp: no `unwrap`, `expect` or indexing** `pgls_lsp/src/lib.rs` warns on `clippy::unwrap_used`, `clippy::expect_used` and `clippy::indexing_slicing`, and CI's `-D warnings` turns them into errors. Tests are exempt (`allow-*-in-tests` in `clippy.toml`). On top of #928, which already removed the `params.arguments[0]` indexing, there were 21 findings (19 `unwrap`, 2 `expect`): - 12 were `std::sync` lock unwraps. `pgls_lsp` uses `parking_lot` locks now, which can't be poisoned. Before, a request handler that panicked while holding the documents lock (`panic_to_lsp_error` catches the panic) poisoned the lock, and every later request on that session panicked too. - `get_cursor_position` and completions `expect`ed/`unwrap`ped the client capabilities. They use `Session::position_encoding()`, which falls back to UTF-16 like the other handlers. - A completion item whose range can't be converted is left out with a warning, instead of panicking the whole completion request. - `diagnostic.advices(..)` errors are returned through `?`. Serialising the `executeStatement` arguments drops the action on failure. The unused `print_markup` (two unwraps) is removed. - Two can't fail and are `#[expect(..., reason = "...")]`: the `u32` offset in `LineIndex::new` (documents over 4 GiB aren't supported anywhere) and the `ConfigurationStatus` atomic, which only ever stores a `ConfigurationStatus as u8`. **Counts for the next crate** `pgls_workspace` (non-test code) has 76 findings: 48 `unwrap`, 5 `expect`, 16 indexing and 7 slicing. About 34 of the unwraps are lock unwraps, which `parking_lot` would remove the same way. `matcher/pattern.rs` (20, the glob matcher's index arithmetic) and `server.rs` (16) have most of the rest. For reference, the same measurement over the other crates found 178 in `pgls_configuration` (mostly generated code), 89 in `pgls_pretty_print`, 70 in `pgls_catalog` and 39 in `pgls_treesitter`. **Risks** Open PRs that add `std::sync` locks with `.unwrap()` to `pgls_lsp` (#895, #908) will fail clippy once this is merged. The fix is to use `parking_lot::{Mutex, RwLock}` and drop the `.unwrap()`. `client_capabilities()` stays because #908, #911 and #919 use it. Tests: `cargo clippy --workspace --all-targets --all-features` is clean, `pgls_lsp` and `pgls_fs` tests pass, and `just gen-lint` leaves the generated files unchanged. **Follow-ups:** #895, #908 and #936 now use `parking_lot` locks. Only #881's own `projects` and `workspace_folders` locks remain to be switched when the two meet.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three ways a client request could break the server or the document state.
pgls.executeStatementwithout arguments panickedThe handler read
params.arguments[0]andparams.arguments[1]directly, soworkspace/executeCommandwith no arguments panicked in the handler. Malformed arguments came back as an internal error. Both are nowInvalidParamserrors with a message that says which argument is wrong.Documents of other schemes shared the workspace document of a file
Session::file_pathmapped every URI to its path, whatever the scheme, and the workspace keys documents by path. So:git:/workspace/a.sql?{"ref":"HEAD"}(the left side of a diff) mapped to/workspace/a.sql.didOpenon it kept the existing document, anddidCloseremoved the document of the real file. The next edit of the file failed with "not found", and its diagnostics were never published again.vscode-notebook-cell:/nb.ipynb#W0...,#W1...) mapped to the same path.untitled:Untitled-1became the relative pathUntitled-1, andntitled-1on Windows, whereto_file_pathstrips the first character.Only
file:URIs map to file paths now. Other schemes are keyed by their whole URI, e.g.untitled:Untitled-1, which never equals a file's path and differs between notebook cells.I gave them paths of their own rather than skipping them because untitled buffers and notebook cells are real editing surfaces, and people do write SQL in them. With their own key they keep diagnostics, completions and hover without touching any file's document. The trade-off is that path-based settings (
files.include/ignore, the migrations directory) don't match them, which is right since they are not files. A git diff's old side is linted like any other buffer, which matches what VS Code does for other languages. The WASM worker's owndocumentSelector({ language: 'sql' }) is unchanged.Out-of-range positions failed the whole request
LineIndex::offsetreturnedNonefor a line past the end, and a column past the end of a line ran into the next line or past the end of the text. Some clients send the line after the last one as the end of a range:Positions from the client are now clamped (
LineIndex::offset_clamped): a column past the end of its line means the end of the line, as the LSP specifies, and a line past the end means the end of the text. This applies to every handler that converts client positions (from_lsp::offset). The internalLineIndex::offsetused for diffs stays exact. A huge UTF-16 column no longer overflows inwide_to_utf8_col. Inlay hints that can't be converted are left out with a warning instead of failing the response.Tests
execute_statement_without_arguments_is_invalid: no arguments, and arguments of the wrong type, both getInvalidParams.closing_a_document_of_another_scheme_keeps_the_file: open the file, open and close itsgit:version, edit the file. Diagnostics for the edit are published.test_inlay_hints_utf16_and_out_of_range: hints after an emoji land on the right UTF-16 columns, and a range that ends at the end of the line, past it, or on the line after the last one gets the same hints.document_pathforfile:(percent-decoding, drive colon),git:, notebook cells anduntitled:, and clamping in UTF-16 includingu32::MAX.Not covered
fluent_uri(since #877) rejects URIs with unencoded spaces, so such a request fails to deserialize before it reaches the server. VS Code always encodes them, and fixing it would need a change inls-types.