Add pre-publish code review #002 (2 medium, 3 low, 2 nit)
Full source read of all 13 src/*.rs files for correctness, panic
safety, and API ergonomics ahead of v0.1.0 crates.io publish.
Findings:
- M1: Definitions::merge_into silently drops data via double-
unwrap_or_default chain; untested public API
- M2: write_bytes truncates u32 length prefix on >4GiB data
(data_len as u32 without bounds check)
- L1: materialize.rs union path uses unchecked offset arithmetic
(sequential_reader.rs sibling uses checked_add)
- L2: three unreachable!() in production code (offset_map, layout_
builder, materialize)
- L3: AlkTypeError::source() returns None for Validation variant
(ValidationError implements std::error::Error)
- N1: is_rfc3339_timestamp is non-strict (Feb 31 passes, seconds
unchecked)
- N2: SequentialReader returns FieldValue::Bytes for Record (API
asymmetry vs other composites)
Verification baseline (commit c0217d9):
- cargo test --release: 438 tests pass (391 crate + 47 integration)
- cargo clippy --all-targets -- -D warnings: clean
- cargo build --target wasm32-unknown-unknown --release: clean
- no unsafe, no TODO/FIXME, all unwrap/expect/panic in test modules
This commit is contained in:
468
docs/reviews/002-code-review.md
Normal file
468
docs/reviews/002-code-review.md
Normal file
@@ -0,0 +1,468 @@
|
||||
---
|
||||
status: open
|
||||
last_updated: 2026-08-11
|
||||
reviewed_artifacts:
|
||||
- src/lib.rs
|
||||
- src/builder.rs
|
||||
- src/data_access.rs
|
||||
- src/engine.rs
|
||||
- src/error.rs
|
||||
- src/layout_builder.rs
|
||||
- src/materialize.rs
|
||||
- src/offset_map.rs
|
||||
- src/schema.rs
|
||||
- src/sequential_reader.rs
|
||||
- src/tunion.rs
|
||||
- src/validation.rs
|
||||
- src/macros.rs
|
||||
- tests/{engine_integration,error_paths,poc_roundtrip,tunion_dispatch}.rs
|
||||
- Cargo.toml
|
||||
tool: manual source read + cargo test/clippy/wasm build
|
||||
reviewer: pre-publish code review (pre-cargo-publish sweep)
|
||||
---
|
||||
|
||||
# Code Review #002 — Pre-Publish Sweep
|
||||
|
||||
## Purpose
|
||||
|
||||
First logic/correctness review of the alktype crate ahead of the
|
||||
v0.1.0 crates.io publish. This pass complements the coverage review
|
||||
(#001) by reading every source file for correctness, code smell, panic
|
||||
safety, and API ergonomics — the things a downstream consumer
|
||||
(notably the upcoming `alkcall` crate, the alknet-call/alknet-channels
|
||||
unification) will trip over.
|
||||
|
||||
This review deliberately defers documentation polish (README, inline
|
||||
doc cleanup for docs.rs) and coverage gaps to subsequent sweeps, per
|
||||
the publisher's stated workflow. The scope here is: no panics in
|
||||
production paths, no silent data corruption, no API that returns
|
||||
nonsense, and nothing that would embarrass the crate on docs.rs on day
|
||||
one.
|
||||
|
||||
## Methodology
|
||||
|
||||
- Full read of all 13 `src/*.rs` files (production + test modules).
|
||||
- Grep for `unwrap`/`expect`/`panic!`/`unreachable!`/`unsafe`/`TODO`/
|
||||
`FIXME` to classify every hit as production or test-only.
|
||||
- Grep for `as u32`/`as usize`/`as i32`/`as i64`/`as u64` to find
|
||||
truncation-prone casts.
|
||||
- `cargo build --release`, `cargo test --release`,
|
||||
`cargo clippy --all-targets -- -D warnings`,
|
||||
`cargo build --target wasm32-unknown-unknown --release`.
|
||||
- Cross-reference every error path against its caller to confirm errors
|
||||
propagate (not swallowed) and carry useful attribution.
|
||||
- Read `docs/reviews/001-coverage-analysis.md` for prior context and
|
||||
resolved/unresolved items.
|
||||
|
||||
## Summary Statistics
|
||||
|
||||
| Severity | Count |
|
||||
|------------|------:|
|
||||
| Critical | 0 |
|
||||
| Medium | 2 (M1, M2) |
|
||||
| Low | 3 (L1, L2, L3) |
|
||||
| Nit | 2 (N1, N2) |
|
||||
|
||||
No critical findings. The crate is in good shape for a 0.1.0 publish
|
||||
once M1 and M2 are resolved. The Medium findings are both small,
|
||||
localized, and have clear fixes; the Low/Nit findings are deferrable.
|
||||
|
||||
## Verification Baseline
|
||||
|
||||
All verification run on the reviewed tree (commit `c0217d9`):
|
||||
|
||||
- `cargo test --release`: **438 tests pass** (391 crate unit tests +
|
||||
47 integration tests across 4 files). Zero failures.
|
||||
- `cargo clippy --all-targets -- -D warnings`: **clean**.
|
||||
- `cargo build --release`: clean.
|
||||
- `cargo build --target wasm32-unknown-unknown --release`: **clean**.
|
||||
The wasm32 target is a first-class supported target (the alkcall
|
||||
consumer will rely on this — confirmed working).
|
||||
- No `unsafe` anywhere in the crate.
|
||||
- No `TODO`/`FIXME`/`HACK`/`XXX` markers in source.
|
||||
- All `unwrap`/`expect`/`panic!`/`unreachable!` are confined to
|
||||
`#[cfg(test)]` modules, verified by line-context cross-reference.
|
||||
Production code uses `?`, `ok_or_else`, `checked_add`, and
|
||||
`Result`-returning helpers throughout.
|
||||
|
||||
---
|
||||
|
||||
## Findings
|
||||
|
||||
### M1. `Definitions::merge_into` is convoluted and silently drops data
|
||||
|
||||
**File**: `src/builder.rs:493-503`
|
||||
|
||||
**Problem**: `Definitions::merge_into` is the public convenience API
|
||||
for merging `$defs` into a top-level schema. The implementation goes
|
||||
through `self.build()` (which returns `{"$defs": {...}}`), then
|
||||
extracts the inner `$defs` object via a double-`cloned`/double-
|
||||
`unwrap_or_default` chain:
|
||||
|
||||
```rust
|
||||
pub fn merge_into(self, top: &mut Value) {
|
||||
if let Some(obj) = top.as_object_mut() {
|
||||
if let Some(defs) = self.build().as_object() {
|
||||
if let Some(existing) = obj.get_mut("$defs").and_then(Value::as_object_mut) {
|
||||
existing.extend(defs.get("$defs").cloned().unwrap_or_default()
|
||||
.as_object().cloned().unwrap_or_default());
|
||||
} else {
|
||||
obj.insert("$defs".to_string(), Value::Object(
|
||||
defs.get("$defs").cloned().unwrap_or_default()
|
||||
.as_object().cloned().unwrap_or_default()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Two issues:
|
||||
|
||||
1. **Silent data loss**: if `self.build()` ever produces a shape other
|
||||
than `{"$defs": Object(_)}` (a future bug, or a refactor that adds
|
||||
another key), the `.unwrap_or_default()` chain silently substitutes
|
||||
an empty map — the definitions vanish without an error. This is the
|
||||
worst failure mode for a builder API: the consumer thinks they
|
||||
merged definitions, the schema compiles, and `$ref` resolution
|
||||
silently fails downstream.
|
||||
|
||||
2. **Readability**: the double-`cloned().unwrap_or_default().as_object()
|
||||
.cloned().unwrap_or_default()` chain is hard to read and hard to
|
||||
audit. A reviewer cannot tell at a glance whether it's correct.
|
||||
|
||||
**Also**: `merge_into` has **no test coverage**. The
|
||||
`builder_definitions_define_returns_ref` test covers
|
||||
`Definitions::build()` but not `merge_into`. The OQ-006 resolution
|
||||
work referenced `merge_into` in `builder.md` Example 3, so the API is
|
||||
intended to be used — but it's never exercised.
|
||||
|
||||
**Fix**: Simplify by accessing the `defs` field directly (the method
|
||||
already takes `self` by value) instead of round-tripping through
|
||||
`build()`. Then add tests covering both the "merge into schema with
|
||||
existing `$defs`" and "merge into schema without `$defs`" branches.
|
||||
|
||||
```rust
|
||||
pub fn merge_into(self, top: &mut Value) {
|
||||
if let Some(obj) = top.as_object_mut() {
|
||||
match obj.get_mut("$defs").and_then(Value::as_object_mut) {
|
||||
Some(existing) => existing.extend(self.defs),
|
||||
None => {
|
||||
obj.insert("$defs".to_string(), Value::Object(self.defs));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This requires moving the method outside the `impl Definitions` block
|
||||
that consumes `self.defs` via `build()`, or restructuring so `merge_into`
|
||||
takes ownership of `self.defs` directly. Either way, the
|
||||
`unwrap_or_default` chain disappears.
|
||||
|
||||
**Lift**: removes a silent-data-loss bug class, makes the public API
|
||||
auditable, adds test coverage to an untested public method. Small
|
||||
effort (~15 lines + ~15 lines of tests).
|
||||
|
||||
---
|
||||
|
||||
### M2. `write_bytes` silently truncates length prefix on >4 GiB data
|
||||
|
||||
**File**: `src/data_access.rs:264-278`
|
||||
|
||||
**Problem**: `write_bytes` casts `data_len: usize` to `u32` for the
|
||||
length prefix without checking bounds:
|
||||
|
||||
```rust
|
||||
let data_len = value.len();
|
||||
let total = U32_SIZE.checked_add(data_len).ok_or_else(/* overflow */)?;
|
||||
let end = offset.checked_add(total).ok_or_else(/* overflow */)?;
|
||||
check_bounds(buffer.len(), offset, end, field_path)?;
|
||||
write_array(buffer, offset, u32_to(data_len as u32, endian), field_path)?;
|
||||
```
|
||||
|
||||
If `value.len() > u32::MAX` (on a 64-bit platform with a >4 GiB blob),
|
||||
`data_len as u32` truncates. The `check_bounds` call above passes
|
||||
(the buffer genuinely is that large, or `total` overflowed `usize`
|
||||
first on a 32-bit target), but the written length prefix doesn't
|
||||
match the actual data length. A subsequent `read_bytes` returns a
|
||||
short slice — silent data corruption.
|
||||
|
||||
The `checked_add` guards on `total` and `end` catch `usize` overflow
|
||||
but not the `u32` truncation, which can happen before `usize`
|
||||
overflow on 64-bit platforms (where `usize` is 64-bit and `u32::MAX
|
||||
< usize::MAX`).
|
||||
|
||||
The same `as u32` cast pattern appears in:
|
||||
- `src/tunion.rs:457,493` — test-only, fine.
|
||||
- `src/sequential_reader.rs:860` — test-only helper, fine.
|
||||
- `src/engine.rs:857` — test-only, fine.
|
||||
|
||||
Only `data_access::write_bytes` is the public API path.
|
||||
|
||||
**Fix**: validate the cast before writing:
|
||||
|
||||
```rust
|
||||
let data_len_u32 = u32::try_from(data_len).map_err(|_| {
|
||||
access_err(
|
||||
field_path,
|
||||
format!("data length {data_len} exceeds u32::MAX (length prefix width)"),
|
||||
)
|
||||
})?;
|
||||
write_array(buffer, offset, u32_to(data_len_u32, endian), field_path)?;
|
||||
```
|
||||
|
||||
**Lift**: closes a silent-corruption path on large inputs. ~3 lines.
|
||||
The 64-bit >4 GiB case is rare but not impossible (the crate handles
|
||||
binary blobs via `AlkType:Bytes`; a safetensors-style data format
|
||||
could plausibly have large blobs). For a 0.1.0 publish, closing this
|
||||
path is worth the 3 lines.
|
||||
|
||||
---
|
||||
|
||||
### L1. `materialize.rs` union path uses unchecked offset arithmetic
|
||||
|
||||
**File**: `src/materialize.rs:316,319,336`
|
||||
|
||||
**Problem**: The byte-offset discriminator path in
|
||||
`materialize_union_packed` uses plain `+` for `offset + disc_offset`:
|
||||
|
||||
```rust
|
||||
let disc_value = match disc_type {
|
||||
AlkTypeKind::Uint8 => {
|
||||
data_access::read_u8(buffer, offset + disc_offset, field_path)? as u32
|
||||
}
|
||||
AlkTypeKind::Uint16 => {
|
||||
data_access::read_u16(buffer, offset + disc_offset, field_path, endian)? as u32
|
||||
}
|
||||
...
|
||||
};
|
||||
let variant_offset = offset + disc_offset + disc_type.type_size().unwrap_or(1);
|
||||
```
|
||||
|
||||
`offset + disc_offset` can overflow `usize`. The sibling implementation
|
||||
in `sequential_reader.rs::read_union_value` (lines 416-424) correctly
|
||||
uses `checked_add`. The materializer should match.
|
||||
|
||||
Low severity because:
|
||||
- `offset` comes from the materializer's own cursor (always small in
|
||||
practice).
|
||||
- `disc_offset` comes from the schema (`discriminator.offset`), which
|
||||
is attacker-controllable in a "load untrusted schema" scenario — but
|
||||
the crate doesn't currently document whether loading untrusted
|
||||
schemas is a supported use case.
|
||||
|
||||
Consistency with `sequential_reader.rs` is the main argument for
|
||||
fixing this now.
|
||||
|
||||
**Fix**: replace `offset + disc_offset` with
|
||||
`offset.checked_add(disc_offset).ok_or_else(|| ...)?` in both spots.
|
||||
~6 lines, mirrors the existing `sequential_reader.rs` pattern.
|
||||
|
||||
---
|
||||
|
||||
### L2. Three `unreachable!()` in production code
|
||||
|
||||
**Files**: `src/offset_map.rs:277`, `src/layout_builder.rs:285`,
|
||||
`src/materialize.rs:324`
|
||||
|
||||
**Problem**: Three match arms use `unreachable!()` to assert
|
||||
exhaustiveness over `AlkTypeKind`:
|
||||
|
||||
- `offset_map.rs:277`: `_ => unreachable!("all AlkTypeKind variants are covered above")`
|
||||
- `layout_builder.rs:285`: same
|
||||
- `materialize.rs:324`: `_ => unreachable!("disc_type restricted by parse_discriminator")`
|
||||
|
||||
These are all genuinely unreachable given the match arms above them —
|
||||
the `AlkTypeKind` enum is fully covered in each case, and
|
||||
`parse_discriminator` restricts `disc_type` to `Uint8/Uint16/Uint32`
|
||||
before this point. This is the idiomatic Rust pattern for
|
||||
exhaustiveness and the compiler will warn if a new variant is added
|
||||
without updating the match.
|
||||
|
||||
**Not a bug**. But for a library that may eventually load schemas from
|
||||
untrusted input, an `unreachable!` is a panic in production. The
|
||||
defense-in-depth alternative is to return an `AlkTypeError::Schema`
|
||||
instead, so a future enum extension (or a logic bug in
|
||||
`parse_discriminator`) produces an error rather than a panic.
|
||||
|
||||
**Fix** (optional): replace each `unreachable!(msg)` with
|
||||
`Err(AlkTypeError::Schema(format!("internal: {msg}")))`. The compiler
|
||||
still warns on non-exhaustive matches (the `_` arm catches nothing
|
||||
once all variants are listed), so this doesn't lose the exhaustiveness
|
||||
check. ~3 lines per site. Defer if the "untrusted schema" use case
|
||||
isn't on the v0.1.0 roadmap.
|
||||
|
||||
---
|
||||
|
||||
### L3. `AlkTypeError::source()` returns `None` for `Validation` variant
|
||||
|
||||
**File**: `src/error.rs:45`
|
||||
|
||||
**Problem**: `AlkTypeError` implements the blanket
|
||||
`impl std::error::Error for AlkTypeError {}`, so `source()` always
|
||||
returns `None`. The `Validation` variant wraps a
|
||||
`jsonschema::ValidationError<'static>`, which itself implements
|
||||
`std::error::Error` and may carry a cause chain.
|
||||
|
||||
Downstream consumers (the alkcall logging/diagnostics layer) may want
|
||||
to walk the cause chain for structured error reporting. Currently
|
||||
they can only `Display` the `ValidationError` (flattened into the
|
||||
`AlkTypeError::Display` string), not traverse it.
|
||||
|
||||
Verified: `jsonschema::ValidationError` implements
|
||||
`std::error::Error` (in `jsonschema-0.46/src/error.rs`), so the
|
||||
`source()` override is sound.
|
||||
|
||||
**Fix**:
|
||||
|
||||
```rust
|
||||
impl std::error::Error for AlkTypeError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
AlkTypeError::Validation(e) => Some(e),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note the lifetime: `AlkTypeError::Validation` holds a
|
||||
`ValidationError<'static>`, so the `+ 'static` bound in `source()` is
|
||||
satisfied. The existing `source_returns_none_for_all_variants` test
|
||||
(in `error.rs` tests) asserts `source().is_none()` for `Schema`/
|
||||
`Offset`/`Access` — those still pass. The `Validation` arm needs a new
|
||||
assertion (`source().is_some()`).
|
||||
|
||||
**Lift**: small ergonomics win for downstream consumers; ~6 lines +
|
||||
~5 lines of test. Defer if no consumer needs cause-chain walking
|
||||
yet, but cheap to do now.
|
||||
|
||||
---
|
||||
|
||||
### N1. `is_rfc3339_timestamp` is a non-strict hand-rolled validator
|
||||
|
||||
**File**: `src/validation.rs:351-385`
|
||||
|
||||
**Problem**: The `is_rfc3339_timestamp` function is a hand-rolled
|
||||
datetime validator. It checks year > 0, month 1..=12, day 1..=31,
|
||||
hour 0..=23, minute 0..=59 — but:
|
||||
|
||||
- Day-of-month per month is not validated (Feb 31, Apr 31 pass).
|
||||
- Seconds are not checked against 0..=59 (the code checks minutes but
|
||||
not seconds — line 384 only validates `time_parts[1]`, not
|
||||
`time_parts[2]` if present).
|
||||
- The `rfind('-')` timezone-offset heuristic at line 363 uses
|
||||
`pos >= 8` to distinguish a date-separator `-` from a timezone
|
||||
offset `-`. This works for `2026-07-20T10:30:00-05:00` but is
|
||||
fragile for unusual-but-valid inputs.
|
||||
|
||||
The function is documented as "Simple RFC 3339 / ISO 8601 datetime
|
||||
check", so the non-strictness is acknowledged. For 0.1.0 this is
|
||||
acceptable — `AlkType:Timestamp` is a length-prefixed string at the
|
||||
binary level, and strict RFC 3339 validation is the consumer's
|
||||
responsibility if they need it.
|
||||
|
||||
The canonical fix would be to use the `chrono` or `time` crate's
|
||||
parsing, but that adds a dependency. Not worth it for 0.1.0.
|
||||
|
||||
**Fix** (optional): add a doc comment noting the non-strictness
|
||||
explicitly, so consumers on docs.rs know not to rely on it for strict
|
||||
validation. ~2 lines of doc.
|
||||
|
||||
---
|
||||
|
||||
### N2. `SequentialReader` returns `FieldValue::Bytes` for `AlkType:Record`
|
||||
|
||||
**File**: `src/sequential_reader.rs:716`
|
||||
|
||||
**Problem**: `read_record_value` returns
|
||||
`FieldValue::Bytes(&buffer[offset..position])` — a raw byte slice —
|
||||
for a `Record` field. The other composite kinds return typed
|
||||
descriptors (`Struct { start, end }`, `Union { discriminator,
|
||||
variant_start }`, `Array { count, element_start, element_stride }`).
|
||||
|
||||
The doc comment on `read_record_value` says "the consumer recurses
|
||||
into the record's value schema", so the behavior is documented. But
|
||||
returning `FieldValue::Bytes` rather than a `FieldValue::Record { start,
|
||||
end, count }` is a minor API ergonomics smell: the consumer has to
|
||||
know that a `Record` field comes back as `Bytes`, while every other
|
||||
composite comes back as a typed variant.
|
||||
|
||||
Not a bug. Not worth fixing for 0.1.0 (would require adding a
|
||||
`FieldValue::Record` variant and updating consumers). Flagged for
|
||||
awareness — if the alkcall consumer finds the `Record` API awkward,
|
||||
revisit in a follow-up.
|
||||
|
||||
**Fix**: none for 0.1.0. Document as a known API asymmetry.
|
||||
|
||||
---
|
||||
|
||||
## What's Good
|
||||
|
||||
The crate is in notably good shape for a 0.1.0. Highlights:
|
||||
|
||||
- **Overflow safety is thorough**: `checked_add` everywhere in hot
|
||||
paths (`data_access`, `sequential_reader`, `layout_builder`,
|
||||
`materialize`). This is rare and good — most crates use `+` and
|
||||
panic on overflow. M2 is the one place this discipline slipped
|
||||
(the `as u32` cast).
|
||||
- **Zero-copy reads**: `read_string`/`read_bytes` return borrowed
|
||||
slices — no allocation in the read path. Critical for the protocol-
|
||||
parsing use case.
|
||||
- **Error attribution**: every `Access`/`Offset` error carries a
|
||||
`field_path` string. Excellent for debugging wire-format issues.
|
||||
- **Bounds checks before slicing**: `check_bounds` then `get(..)`
|
||||
with `ok_or_else` — defensive, no panics on bad offsets.
|
||||
- **`build_validator` registers all 19 keywords** — no silent
|
||||
passthrough where an `AlkType:*` kind is accepted but not validated.
|
||||
- **Tests are excellent**: 438 tests, good coverage of error paths
|
||||
and edge cases (short buffers, unknown discriminators, invalid
|
||||
UTF-8, both endiannesses, overflow guards where reachable). The
|
||||
test discipline is high.
|
||||
- **wasm32 works** — confirmed clean build. The alkcall consumer's
|
||||
napi/wasm adapter path is open.
|
||||
- **No `unsafe`, no `TODO`/`FIXME`** — clean codebase hygiene.
|
||||
- **Error type is well-designed**: four variants covering the three
|
||||
engine phases + validation, with `Display` carrying the field path.
|
||||
L3 is a small ergonomics gap, not a design flaw.
|
||||
|
||||
---
|
||||
|
||||
## Recommended Order
|
||||
|
||||
1. **M2** (u32 truncation in `write_bytes`) — 3 lines, closes a
|
||||
silent-corruption path. Do this first; it's the smallest and
|
||||
highest-value fix.
|
||||
2. **M1** (`Definitions::merge_into`) — ~15 lines + ~15 lines of
|
||||
tests, removes a silent-data-loss path and adds coverage to an
|
||||
untested public API. Do this before publish since `merge_into` is
|
||||
referenced in `builder.md` Example 3.
|
||||
3. **L1** (materializer unchecked arithmetic) — ~6 lines, mirrors
|
||||
the existing `sequential_reader.rs` pattern. Cheap to do
|
||||
alongside M2.
|
||||
4. **L3** (`AlkTypeError::source`) — ~6 lines + ~5 lines of tests.
|
||||
Small ergonomics win; cheap to do now.
|
||||
5. **L2** (`unreachable!` → `Err`) — ~9 lines across three sites.
|
||||
Optional; defer if the "untrusted schema" use case isn't on the
|
||||
v0.1.0 roadmap.
|
||||
6. **N1, N2** — documentation only; defer to the docs sweep.
|
||||
|
||||
After M1, M2, L1, and L3, the crate is ready for the pre-publish
|
||||
sanity check (README, inline docs, docs.rs render) and publish.
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- All line numbers refer to the tree at commit `c0217d9` (the last
|
||||
commit on `main` at review time).
|
||||
- The wasm32-unknown-unknown target was verified as a clean build.
|
||||
The crate's only dependencies (`jsonschema` with
|
||||
`default-features = false`, `serde_json` with `preserve_order`)
|
||||
are wasm-compatible — no `std::time`, no filesystem, no threads.
|
||||
- This review does not cover documentation quality (README, inline
|
||||
docs, docs.rs rendering). Per the publisher's workflow, that's a
|
||||
separate sweep after the code is settled.
|
||||
- Coverage gaps from review #001 (S4 sequential reader error paths,
|
||||
S8 overflow guards) are not re-litigated here. They remain
|
||||
coverage gaps, not correctness issues.
|
||||
Reference in New Issue
Block a user