microsoft / microsoft/mssql-rs

Enforce the no-panics rule with clippy instead of prose

Open
#462 2 comments 0 reactions 0 assignees View on GitHub
mssql-odbc
Dominant language
Rust
Stars
53
Forks
14
Avg merge
1d 15h
Merged PRs (30d)
137

Description

## Problem

`.github/instructions/mssql-odbc.instructions.md` ("No panics") says:

> **Never** use `.unwrap()` or `.expect()` on `Result` or `Option` in non-test code.

Nothing enforces this. It's prose that a contributor (or an automated agent) has to read and remember. `mssql-odbc` in particular is a C shared library `dlopen`ed into arbitrary host processes, so a panic that escapes is fatal and unrecoverable — `ffi_entry!` is documented as a last-resort net, not a license to panic.

This was caught by review in #459, where an `.expect()` landed on the `SQLAllocHandle(SQL_HANDLE_DBC)` path. Review caught it that time; a lint would catch it every time.

## The enforcement point already exists (and is switched off)

Per @saurabh500's [comment](https://github.com/microsoft/mssql-rs/issues/462#issuecomment-5498943107), the right home for this is the workspace root, not a crate-level `#![deny(...)]`. The root `Cargo.toml` already carries a commented-out "Don't Panic" block under `[workspace.lints.clippy]`:

```toml
# string_slice = "warn"
# indexing_slicing = "warn"
# unwrap_used = "warn"
# panic = "warn"
# todo = "warn"
# unimplemented = "warn"
# unreachable = "warn"
# get_unwrap = "warn"
# unwrap_in_result = "warn"
# unchecked_time_subtraction = "warn"
# panic_in_result_fn = "warn"
```

Two gaps worth calling out before anyone uncomments it:

1. **`mssql-odbc` does not inherit workspace lints.** Every other member crate has `[lints] workspace = true` in its `Cargo.toml`; `mssql-odbc` does not. Uncommenting the block alone changes nothing for the crate that motivated this issue.
2. **`expect_used` is not in the block** — only `unwrap_used`. All 6 `mssql-odbc` violations are `.expect()`, and the #459 regression was an `.expect()`. As written, the block would not have caught the thing that prompted this issue.

Also note `cargo bclippy` runs with `-D warnings`, so `= "warn"` is already a CI failure. Flipping a lint on and cleaning up its sites must land in the same change.

## Proposal

1. Add `[lints] workspace = true` to `mssql-odbc/Cargo.toml`.
2. In `[workspace.lints.clippy]`, enable the panic family: `unwrap_used`, **`expect_used`**, `panic`, `todo`, `unimplemented`, `unreachable`, `get_unwrap`.
3. Add a workspace-root `clippy.toml` so tests keep their ergonomics (the instructions already permit `.unwrap()`/`.expect()` under `#[cfg(test)]`):
```toml
allow-unwrap-in-tests = true
allow-expect-in-tests = true
```
4. Add crate-local `[lints.clippy]` overrides for `mssql-mock-tds` and `mssql-tds-bench` — see "Scope" below.
5. Mirror the same lint block into `mssql-py-core` and `mssql-mock-tds-py`, which are workspace-excluded and cannot inherit. `scripts/bclippy.ps1` already runs clippy over them.
6. Clean up the remaining non-test violations, or annotate each with a targeted `#[allow(...)]` plus a justification comment where the invariant genuinely can't fail.

Leave `unwrap_in_result` and `panic_in_result_fn` commented out — high noise, low signal relative to the rest.

## Cost

Measured on `ba888077` with:

```
cargo clippy --workspace --lib --all-features -- \
-W clippy::unwrap_used -W clippy::expect_used -W clippy::panic \
-W clippy::unreachable -W clippy::todo -W clippy::indexing_slicing
```

`--lib`/`--bins` without `--all-targets` excludes `cfg(test)`, so this is exactly the non-test surface. The excluded crates were measured with a separate invocation in their own directories.

| Crate | unwrap | expect | panic | todo | unreachable | **panic family** | indexing_slicing |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| `mssql-odbc` | 0 | 6 | 0 | 0 | 0 | **6** | 62 |
| `mssql-tds` | 16 | 14 | 0 | 1 | 6 | **37** | 138 |
| `mssql-js` | 4 | 4 | 0 | 0 | 0 | **8** | 0 |
| `mssql-py-core` * | 3 | 6 | 0 | 0 | 6 | **15** | 5 |
| `mssql-tds-cli` | 0 | 0 | 0 | 0 | 0 | **0** | 0 |
| `mssql-tds-bench` † | 0 | 12 | 2 | 0 | 0 | **14** | 0 |
| `mssql-mock-tds` † | 0 | 2 | 0 | 0 | 0 | **2** | 71 |
| `mssql-mock-tds-py` * † | 0 | 0 | 1 | 0 | 0 | **1** | 0 |
| **Total** | **23** | **44** | **3** | **1** | **12** | **83** | **276** |

\* workspace-excluded — needs its own lint block, covered by `scripts/bclippy.ps1`.
† test/benchmark support crate, not shipped surface.

The 6 `mssql-odbc` sites:

| File | Line |
| --- | --- |
| `mssql-odbc/src/api/catalog.rs` | 259 |
| `mssql-odbc/src/api/catalog.rs` | 309 |
| `mssql-odbc/src/api/catalog.rs` | 321 |
| `mssql-odbc/src/api/execute.rs` | 288 |
| `mssql-odbc/src/api/execute.rs` | 301 |
| `mssql-odbc/src/auth/msqa.rs` | 238 |

The three in `catalog.rs` are `.expect("peeked Some above")`-style invariants right after a `peek()`, which restructure cleanly into `while let`/`if let`.

## Scope

**In scope:** the panic family across shipped crates — `mssql-odbc` (6), `mssql-tds` (37), `mssql-js` (8), `mssql-py-core` (15). 66 sites, mostly mechanical.

**Opt out, don't clean up:** `mssql-mock-tds`, `mssql-mock-tds-py`, and `mssql-tds-bench` are a test double and benchmarks. They aren't shipped surface, and a panic there fails a test run rather than taking down a host process. `allow-unwrap-in-tests` won't cover them because they are ordinary libs, not `cfg(test)` modules — so give each a crate-local `[lints.clippy]` override with a comment explaining why.

**Out of scope — file a follow-up:** `indexing_slicing` and `string_slice` (276 sites). These frequently need real restructuring rather than a mechanical rewrite, and bundling them with the panic family is how the first attempt at this stalled. Land the panic family first, then take slicing as its own tracked effort.

**Suggested sequencing:** step 1 + `mssql-odbc` cleanup can land on its own as a crate-local `[lints.clippy]` block (6 sites, low risk), with the workspace-wide flip following once the other crates are clean. That keeps this issue from blocking on 66 unrelated fixes.

## Why it's worth doing

The existing rule is correct; the gap is enforcement. A workspace-level `deny` means the guidance holds even when someone hasn't read the instructions file, which is the failure mode that actually occurred.

Contributor guide

Open the contributing guide

Research direction

Start with the workspace lint block in Cargo.toml, mssql-odbc/Cargo.toml, and the listed violations in mssql-odbc/src/api/catalog.rs, mssql-odbc/src/api/execute.rs, and mssql-odbc/src/auth/msqa.rs. Run the provided cargo clippy command, then inspect the other in-scope crates and scripts/bclippy.ps1. Done means the panic-family lints are enforced, non-test violations are cleaned up or justified, and excluded support crates have documented overrides.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
build-system, ci-cd
Issue type
Refactor
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
58/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.