diff --git a/.gitignore b/.gitignore index e84719570..78c1358cc 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ node_modules/ cli/target/ cli/package-fallback/ integrations/**/target/ +crates/skaza/target/ result* dist/ diff --git a/context/plans/skaza-rust-mutation-discovery.md b/context/plans/skaza-rust-mutation-discovery.md new file mode 100644 index 000000000..891c0e264 --- /dev/null +++ b/context/plans/skaza-rust-mutation-discovery.md @@ -0,0 +1,193 @@ +# Plan: skaza-rust-mutation-discovery + +## Change summary + +Adds `skaza`, a new standalone Rust package (library + binary) at `crates/skaza/`, that discovers Rust mutation candidates without executing them. It extracts the minimum useful Rust-only pieces of the local Ooze repository (`../ooze`): source-file discovery and identity (`src/source_path.rs`, `src/lang/mod.rs`), six mutation operators and their Tree-sitter queries (`src/lang/rust.rs`, `queries/rust/*.scm`), candidate discovery and edit deduplication (`src/mutate/mod.rs`), and Rust skip rules (`src/skip/mod.rs`). The code is copied and simplified, never depended upon: Skaza builds with `../ooze` absent. + +The CLI exposes exactly `--src` (repeatable, default `.`), `--limit` (positive, default `10`), and `--format` (`text`|`json`, default `text`), and prints a discovery-only report of deterministic, deduplicated candidates. This is new behavior; the existing `sce` CLI source, dependencies, and release packaging stay unchanged apart from narrowing the CLI's Nix source fileset so Skaza edits do not invalidate CLI derivations. The root flake gains independent Skaza build/test/clippy/fmt checks so `nix flake check` validates Skaza. Mutation application and execution are deferred to PR 2. + +## Acceptance criteria + +How this plan is proven complete. Each criterion is observable and names the +check that proves it. `/validate` runs these checks; no task in the stack +performs final validation. + +- [ ] AC1: `crates/skaza` is one Cargo package named `skaza` (edition 2021) with its own `Cargo.lock`, a library and a binary, no root Cargo workspace, no dependency on `cli/` or `../ooze`, and `crates/skaza/target/` is git-ignored. + - Validate: inspect `crates/skaza/Cargo.toml` (no `path =` dependencies, `edition = "2021"`, `[lib]`/`[[bin]]` or default targets), confirm no root `Cargo.toml` exists, `git check-ignore crates/skaza/target/x` succeeds, and `nix build .#checks..skaza-tests` passes in the Nix sandbox where `../ooze` is unavailable. +- [ ] AC2: The `sce` CLI's `cli/Cargo.toml`, `cli/Cargo.lock`, `packages.sce`, `packages.sce-release`, and release apps are unaffected by Skaza; editing a file under `crates/skaza/` does not change the `sce` package derivation. + - Validate: `git diff main -- cli/Cargo.toml cli/Cargo.lock` is empty; `nix eval --raw .#packages..sce.drvPath` is identical before and after touching a file under `crates/skaza/src/`; `packages` and release apps do not reference Skaza. +- [ ] AC3: `skaza --help` lists exactly `--src`, `--limit`, `--format` (plus clap's help/version), has no subcommands, `--limit 0` exits non-zero with an actionable error on stderr, and an invalid `--format` value is rejected. + - Validate: `nix develop -c cargo run --manifest-path crates/skaza/Cargo.toml -- --help`; `... -- --limit 0` exit code and stderr; covered by integration tests in `crates/skaza/tests/`. +- [ ] AC4: Repeated `--src` accepts files and directories, deduplicates overlapping/duplicate inputs by canonical identity, respects `.gitignore`, skips `.git`, `target`, generated files, test-only paths, and directory symlinks, and errors clearly on nonexistent or non-`.rs` explicit paths. + - Validate: `source.rs` unit tests and binary integration tests covering duplicate/overlapping `--src`, invalid paths, and order independence pass under `nix flake check`. +- [ ] AC5: The six operators `swap_boolean`, `negate_equality`, `comparison_boundary`, `swap_logical`, `remove_not`, `swap_predicate_method` each produce exact expected candidates (byte range, line/column, original, replacement) and never match inside comments or string literals. A selected file whose Tree-sitter parse contains an `ERROR` node or a missing node is never reported as partial discovery: the run fails non-zero with a stderr diagnostic naming the report path and the 1-based line:column of the first error or missing node, and stdout stays empty in both formats; valid files are unaffected. + - Validate: per-operator unit tests with exact expected candidates in `crates/skaza/src/rust.rs`; malformed-Rust unit tests for an `ERROR`-node fixture and a missing-node fixture (each first asserting the parse tree actually contains that node kind) with exact diagnostics; a binary integration test running a malformed file alongside a valid file in `--format json` asserting non-zero exit, empty stdout, and the diagnostic on stderr; all pass under `nix flake check`. +- [ ] AC6: Outer attributes preceding a Rust item (sibling `attribute_item` nodes, possibly several and possibly interleaved with comments) are associated with that item. No candidates are produced anywhere inside an item annotated `#[cfg(test)]` (modules, functions, and other declaration items, including all nested declarations), inside functions annotated `#[test]`, inside assertion macros, panic/unreachable/todo/unimplemented macros, or in generated files. Items annotated `#[cfg(not(test))]` and other non-matching attributes remain eligible. Only the conditional-attribute forms listed under **Assumptions** are recognized; other `cfg` expressions are documented as unsupported rather than claimed. + - Validate: exclusion unit tests with exact expected candidate lists pass under `nix flake check`, including the T06 regression fixture (only the `production` candidate is discovered), a `#[cfg(not(test))]` fixture whose candidate is kept, a nested-module fixture, and a multiple-attribute fixture in both attribute orders. +- [ ] AC7: Candidates are deduplicated by canonical edit (common UTF-8-safe prefix and suffix of original and replacement trimmed, compared as `(canonical file, canonical start, canonical end, canonical replacement)`), so mutations with different captured ranges that produce the same resulting source collapse to one, while genuinely distinct edits at the same location are kept; the surviving duplicate is chosen by a deterministic operator precedence independent of input order. Candidates are deterministically sorted, receive readable IDs (`:-:` plus a `#` discriminator only when needed) that are unique within the run and stable across repeated runs and reordered `--src`, and `--limit` is applied globally after deduplication and ID assignment. + - Validate: `mutation.rs` unit tests, including a different-captured-range/same-result dedup test and an ID-discriminator test, plus an integration test asserting byte-identical JSON across two runs and across reversed `--src` order. +- [ ] AC8: Text output shows total vs. selected counts, file:line:column, operator, original and replacement, and is labeled discovery-only; JSON output is valid, stable, carries the same information, and stdout contains only the report while diagnostics go to stderr; empty results succeed with zero counts. + - Validate: `report.rs` serialization tests and binary integration tests for both formats and for an empty tree. +- [ ] AC9: Source files are never modified by any invocation, regardless of the existing Git working-tree state. + - Validate: integration test that records the exact bytes of every selected fixture file before running both formats and asserts byte-for-byte equality afterward, while an unrelated non-selected file in the same tree has been modified beforehand (proving the check does not depend on a clean tree); no test uses `git status` or any destructive Git command. +- [ ] AC10: Dogfooding against SCE discovers up to ten distinct valid candidates from production code, the JSON output is identical across two runs, and the dogfooded source file is byte-for-byte unchanged. + - Validate: record `cksum cli/src/services/repository_identity/mod.rs` before, run `nix develop -c cargo run --manifest-path crates/skaza/Cargo.toml -- --src cli/src/services/repository_identity/mod.rs --limit 10 --format json` twice, diff the outputs, confirm `selected` ≤ 10 and > 0, no candidate line falls at or after the `#[cfg(test)]` module, and `cksum` afterward matches the recorded value. Unrelated dirty files elsewhere in the repository do not affect this check. +- [ ] AC11: `nix flake check` builds, tests, lints, and format-checks Skaza through dedicated checks, not only the existing CLI checks. + - Validate: `nix flake show` lists `skaza-tests`, `skaza-clippy` (`--all-targets -- -D warnings`), and `skaza-fmt` checks; `nix flake check` passes. + +### Full validation + +Repository-wide checks `/validate` runs after the last task, regardless of +which criterion they map to. + +- `nix flake check` +- `nix develop -c cargo clippy --manifest-path crates/skaza/Cargo.toml --all-targets -- -D warnings` + +### Context sync + +- New `context/skaza/skaza-mutation-discovery.md` describing the crate boundary, CLI contract, operators, exclusion rules and the supported test-attribute scope (including unsupported `cfg` forms), invalid-syntax failure behavior, canonical-edit deduplication and operator precedence, candidate model/ID scheme, and report shapes. +- `context/context-map.md` entry for the new Skaza context file. +- `context/sce/flake-build-performance.md` and the flake paragraph in `context/overview.md`: the independent Skaza checks and the narrowed CLI source fileset. +- `context/glossary.md`: `Skaza` and `mutation candidate`. +- `AGENTS.md` repository shape: add `crates/skaza/` and its validation commands. + +## Task context synchronization lifecycle + +Persist this field in every plan; this is durable plan state, not chat state: + +- **Task context synchronization:** every task carries `pending | synced | blocked`. + A completed task must be `synced` before another task can start or the plan can + finish. +- For `blocked`, record **Blocker**, **Required action**, and **Retry condition** + beside the status. Never infer `synced` from conversation history; write every + lifecycle transition to the plan file. + +## Constraints and non-goals + +- **In scope:** `crates/skaza/**`, root `.gitignore`, root `flake.nix` (new Skaza checks and narrowing the CLI `workspaceSrc` fileset), and the durable context files listed above. +- **Out of scope:** mutation application, compilation/test execution, worktree management, source backups and recovery, parallel execution, coverage, mutation scoring, agent orchestration, multi-language support, TOML/config files, extra CLI flags or subcommands, report persistence, SARIF, historical reporting, killed/survived outcomes, function complexity/branch-counting metadata, and any change to `../ooze`. +- **Constraints:** Rust edition 2021; Rust 1.95 toolchain and Crane from the existing flake; dependencies limited to clap, tree-sitter, tree-sitter-rust, ignore, serde, serde_json, and anyhow/thiserror (dev-dependencies such as `tempfile` allowed for tests), with no hashing crate and no custom hash implementation; separate `crates/skaza/Cargo.lock`; no root Cargo workspace; `.scm` queries loaded via `include_str!` must be present in the Nix check source; Skaza stays out of `packages`, release apps, and release artifacts; stdout carries only the report; all non-coreutils tools run through Nix. +- **Non-goal:** a generic multi-language operator registry, operator plugin system, or reuse of Ooze's `mutators!` macro, `LanguageSpec`, config, or operator metadata. + +## Assumptions + +- Operator replacement behavior is copied from Ooze's Rust `mutators!` entries: `swap_boolean` (`true`↔`false`), `negate_equality` (`==`↔`!=`), `comparison_boundary` (`<`↔`<=`, `>`↔`>=`), `swap_logical` (`&&`↔`||`), `remove_not` (strip leading `!` from a unary `!` expression; skip if nothing remains), `swap_predicate_method` (`is_some`↔`is_none`, `is_ok`↔`is_err`; other method names produce nothing). Ooze's `describe` strings are not carried over. +- Operators are a `#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)] enum Operator` with `match`-based query selection and replacement (static dispatch); the six queries compile once into a struct reused for every file in a run. +- Test-only path detection keeps Ooze's Rust-relevant rules (path segments `tests`, `benches`; stems `tests`, `*_test`, `*_tests`, `test_*`) and generated-file detection keeps Ooze's rule (`@generated` or `DO NOT EDIT` in a leading comment within the first 15 lines). Directories under `spec/` are not treated as test-only, since SCE's top-level `spec/` holds Quint models, not Rust tests. +- In Tree-sitter Rust, outer attributes are `attribute_item` nodes that precede their item as siblings, not ancestors. Test exclusion therefore walks each capture's ancestors and, for every declaration-item ancestor (`mod_item`, `function_item`, `impl_item`, `trait_item`, `struct_item`, `enum_item`, `union_item`, `const_item`, `static_item`, `type_item`, `macro_definition`, in `source_file` or any `declaration_list`), collects the contiguous run of preceding `attribute_item` siblings, skipping interleaved `line_comment`/`block_comment` nodes (including doc comments). A capture is excluded when any such ancestor carries a recognized test attribute, so the whole annotated item, including nested declarations, is excluded. Macro exclusions use `macro_invocation` ancestor names. This replaces Ooze's byte scanner. Comments and string literals are excluded by construction because queries match only syntax nodes, verified by tests. +- Supported test-attribute scope is exactly: `#[cfg(test)]` (attribute path `cfg` with a single `test` argument, whitespace-insensitive) on any declaration item listed above, and `#[test]` (path `test`, no arguments) on `function_item`. Not recognized, so candidates inside remain eligible: `#[cfg(not(test))]`, compound `cfg` expressions such as `cfg(any(test, …))` or `cfg(all(test, …))`, `cfg_attr`, path-qualified test attributes such as `#[tokio::test]`, inner attributes such as `#![cfg(test)]`, and attributes on statements or expressions. This scope is documented in the Skaza context file. +- Invalid Rust syntax is detected per selected file with the root node's `has_error()`; the first `ERROR` or missing node in document order supplies the diagnostic location. Diagnostic shape: `::: invalid Rust syntax (>); no candidates reported`. The whole run fails before any report is written; no error-recovery framework or partial-results mode is added. +- An explicit `--src` naming a test-only or generated `.rs` file is not an error; it contributes zero candidates and a note is written to stderr. A non-`.rs` explicit file is an error. +- Report paths are rendered relative to the invocation directory with `/` separators when the file lies under it, otherwise as the canonical absolute path. +- Canonical edits follow Ooze's `canonical_edit`: strip the longest common prefix, then the longest common suffix of the remainder, of `original` and `replacement`, counting whole `char`s so offsets stay on UTF-8 boundaries; the canonical edit is `(start byte + prefix, end byte − suffix, trimmed replacement)`. Candidates keep their captured range, original, and replacement for reporting; the canonical edit is used only for deduplication, sorting, and IDs. Only `canonical_edit`, its prefix/suffix helpers, and the dedup rule are ported, not Ooze's mutation framework. +- Deduplication key is `(canonical file, canonical start, canonical end, canonical replacement)`. Among duplicates, operator precedence mirrors Ooze's `dedup_priority`: `comparison_boundary`, `swap_logical`, `remove_not`, and `swap_predicate_method` outrank `swap_boolean` and `negate_equality`; ties within a rank go to the lexicographically first operator name, so the winner does not depend on input order. +- Deterministic sort order is `(report path, canonical start, canonical end, operator, canonical replacement)`. +- Candidate IDs are readable strings `:-:`. When several deduplicated candidates in the run share that triple, every member of the group gets `#` (1-based, ordered by canonical replacement). IDs are assigned to the full deduplicated, sorted set before `--limit`, so they do not depend on the limit, and because report paths are relative to the invocation directory they do not depend on `--src` order. No hashing is used. +- Line and column are 1-based; column counts UTF-8 bytes from the start of the line, matching Tree-sitter's `Point`. +- The root `flake.nix` CLI `workspaceSrc` currently unions `craneLib.fileset.commonCargoSources workspaceRoot`, which would pull `crates/skaza` Rust sources into every CLI derivation; it is narrowed to exclude `crates/` so Skaza remains independent of CLI packaging. This is a fileset-only change with no effect on CLI build inputs. +- The request's `nix develop -c cargo test --manifest-path crates/skaza/Cargo.toml` may be rejected by the repository's `cargo test` bash policy; the `skaza-tests` flake check is the canonical test path, with the direct command used only where policy allows. + +## Task stack + +- [x] T01: `Scaffold the standalone skaza crate and CLI contract` (status:done) + - Task ID: T01 + - Scope: In — `crates/skaza/Cargo.toml`, `Cargo.lock`, `src/lib.rs`, `src/main.rs`, empty-but-compiling `src/source.rs`, `src/mutation.rs`, `src/rust.rs`, `src/report.rs`, `queries/rust/`, `tests/` directories; clap derive `Args` with exactly `--src` (repeatable `Vec`, default `.`), `--limit` (`NonZeroUsize`/validated, default `10`, actionable error on `0`), `--format` (`text`|`json` value enum, default `text`); root `.gitignore` entry `crates/skaza/target/`. Out — discovery, operators, reporting, Nix wiring. + - Dependencies: none + - Done when: crate builds with the minimal dependency set and its own lockfile; argument-parsing unit tests cover defaults, repeated `--src`, zero limit rejection, and invalid format; no root workspace is introduced and `cli/` is untouched. + - Verify: `nix develop -c cargo build --manifest-path crates/skaza/Cargo.toml`; `nix develop -c cargo run --manifest-path crates/skaza/Cargo.toml -- --help`; `... -- --limit 0` exits non-zero; `git check-ignore crates/skaza/target/x`. + - Completed: 2026-10-09 + - Files changed: `.gitignore`, `crates/skaza/Cargo.toml`, `crates/skaza/Cargo.lock`, `crates/skaza/src/lib.rs`, `crates/skaza/src/main.rs`, `crates/skaza/src/source.rs`, `crates/skaza/src/mutation.rs`, `crates/skaza/src/rust.rs`, `crates/skaza/src/report.rs`, `crates/skaza/queries/rust/.gitkeep`, `crates/skaza/tests/.gitkeep` + - Result: Added standalone `skaza` package (lib + bin, edition 2021, own lockfile, deps anyhow/clap/ignore/serde/serde_json/thiserror/tree-sitter 0.25/tree-sitter-rust 0.23, dev-dep tempfile). `skaza::Args` (clap derive) exposes `--src` (`Vec`, default `.`), `--limit` (`NonZeroUsize`, default `10`, custom parser with actionable error), `--format` (`Format::{Text,Json}`, default `text`); no subcommands. `main.rs` parses args only. Four empty modules compile. Seven argument-parsing unit tests in `src/lib.rs` (defaults, repeated `--src`, explicit limit/json, zero limit, non-numeric limit, invalid format, subcommand rejection). `.gitignore` gains `crates/skaza/target/`. No root `Cargo.toml`; `cli/` untouched. + - Verify outcome: `cargo build` passed; `--help` lists only `--src`, `--limit`, `--format`, `-h`, `-V`; `--limit 0` exit 2 with `limit must be at least 1; pass a positive value such as \`--limit 10\``; `--format xml` exit 2; `git check-ignore crates/skaza/target/x` matched; `cargo build --tests` and `cargo clippy --all-targets -- -D warnings` passed. Deviation: unit tests compiled but were not executed because the repo bash policy blocks `cargo test` and the `skaza-tests` flake check arrives in T02; T02 runs them. + - Context impact: none at T01 (scaffold only; durable Skaza context is scheduled under **Context sync** once behavior exists). + - Context synchronization: pending + +- [ ] T02: `Add independent Skaza checks to the Nix flake` (status:todo) + - Task ID: T02 + - Scope: In — root `flake.nix`: a Skaza source fileset (Cargo sources plus `crates/skaza/queries/**/*.scm`), a Skaza `buildDepsOnly` artifact on the existing `craneLib`/Rust 1.95 toolchain, and `checks.skaza-tests`, `checks.skaza-clippy` (`--all-targets -- -D warnings`), `checks.skaza-fmt`; narrow the CLI `workspaceSrc` so `crates/` is excluded. Out — adding Skaza to `packages`, apps, release derivations, or CI workflow files; any other CLI derivation change. + - Dependencies: T01 + - Done when: `nix flake check` runs the three Skaza checks; the `sce` package derivation path is unchanged by edits under `crates/skaza/`; Skaza appears in no package, app, or release output. + - Verify: `nix flake show` lists the new checks; `nix build .#checks..skaza-tests .#checks..skaza-clippy .#checks..skaza-fmt`; compare `nix eval --raw .#packages..sce.drvPath` before/after touching `crates/skaza/src/lib.rs`. + - Context synchronization: pending + +- [ ] T03: `Implement Rust source discovery` (status:todo) + - Task ID: T03 + - Scope: In — `src/source.rs`: resolve each `--src` against the invocation directory; explicit file must exist and end in `.rs`; directories walked with `ignore::WalkBuilder` (gitignore respected, symlinks not followed, `.git`/`target` skipped); test-only path and generated-file filtering; canonical-identity dedup across duplicate/overlapping inputs; deterministic sorted output carrying canonical path, report path, and source text; clear errors for nonexistent/unsupported paths; unit tests. Out — parsing, operators, exclusions inside files. + - Dependencies: T01 + - Done when: unit tests over temp trees prove recursion, `.gitignore`, `target`/`.git` skipping, symlinked-directory non-following, generated and test-only file exclusion, duplicate and overlapping `--src` dedup, stable report paths, and exact error messages for missing and non-`.rs` paths. + - Verify: `nix build .#checks..skaza-tests` (or `nix develop -c cargo test --manifest-path crates/skaza/Cargo.toml source` where policy allows). + - Context synchronization: pending + +- [ ] T04: `Implement the mutation candidate model` (status:todo) + - Task ID: T04 + - Scope: In — `src/mutation.rs`: `Operator` enum with stable snake_case names, `MutationCandidate` (id, report path, line, column, start/end byte, operator, original, replacement) with serde serialization; constructor that verifies the byte range is in bounds and on UTF-8 char boundaries and that the original text matches the source slice, rejecting unchanged replacements; UTF-8-safe canonical-edit computation ported from Ooze's `canonical_edit`/`common_prefix_len`/`common_suffix_len`; deduplication by canonical edit with the deterministic operator precedence from **Assumptions**, keeping genuinely distinct edits at the same location; deterministic sort; readable ID assignment (`:-:` plus `#` only on collision) over the full deduplicated set; global limit selection after deduplication and ID assignment, returning total (post-dedup) and selected counts; unit tests. Out — Tree-sitter queries, file discovery, rendering, hashing of any kind. + - Dependencies: T01 + - Done when: unit tests with exact expected values prove range/UTF-8 validation, unchanged-replacement rejection, canonical-edit trimming (including `true`→`false` sharing the suffix `e` and a multibyte-character case that never splits a char), dedup of two candidates with different captured ranges producing the same resulting source (for example `a == b`→`a != b` over the whole expression and `==`→`!=` over the operator token) to exactly one survivor chosen by precedence regardless of input order, same-location distinct-edit retention, ID uniqueness with the `#` discriminator applied only to colliding groups, sort and IDs independent of input order, `total` counting post-dedup candidates, and limit behavior (below, at, above total) applied after dedup. + - Verify: `nix build .#checks..skaza-tests`. + - Context synchronization: pending + +- [ ] T05: `Port the six Rust mutation operators and queries` (status:todo) + - Task ID: T05 + - Scope: In — copy `swap_boolean.scm`, `negate_equality.scm`, `comparison_boundary.scm`, `swap_logical.scm`, `remove_not.scm`, `swap_predicate_method.scm` from `../ooze/queries/rust/` into `crates/skaza/queries/rust/`; `src/rust.rs`: compile the six queries once into a reusable struct via `include_str!`, parse each file once with `tree-sitter-rust`; if the root reports `has_error()`, return an error carrying the report path and the 1-based line:column of the first `ERROR` or missing node in document order (diagnostic shape in **Assumptions**) and produce no candidates for the run; otherwise run queries over the tree and map captures to `MutationCandidate`s through `match`-based replacement; unit tests for query compilation, every operator with exact candidates and byte ranges, no matches in comments or strings, and malformed input. Out — test/macro/generated exclusions (T06), rendering, error recovery or partial-results modes. + - Dependencies: T03, T04 + - Done when: each operator's fixture yields exactly the expected candidates; non-pair predicate methods yield nothing; queries compile in a dedicated test; comment and string fixtures yield zero candidates; a malformed fixture producing an `ERROR` node (for example `fn broken( { true }`) and one producing a missing node (for example `fn f() -> bool { let x = true }`, missing `;`) each first assert the parsed tree contains that node kind and then assert discovery fails with the exact diagnostic and no candidates; valid fixtures in the same test module still discover normally. + - Verify: `nix build .#checks..skaza-tests .#checks..skaza-clippy`. + - Context synchronization: pending + +- [ ] T06: `Exclude test, assertion, panic, and generated code from discovery` (status:todo) + - Task ID: T06 + - Scope: In — Tree-sitter-based exclusion in `src/rust.rs` (or a private submodule): associate each declaration item with its contiguous run of preceding sibling `attribute_item` nodes (skipping interleaved comments and doc comments, supporting any number of attributes in any order); skip captures anywhere inside an item annotated `#[cfg(test)]` (modules, functions, and the other declaration items listed in **Assumptions**, including all nested declarations and inline modules), inside functions annotated `#[test]`, and inside test-only descendants nested in a test module; skip invocations of `assert*`/`debug_assert*` and `panic`/`unreachable`/`todo`/`unimplemented` macros; recognize only the supported attribute forms from **Assumptions**, leaving `#[cfg(not(test))]` and other attributes eligible; confirm generated files never reach operators; unit tests with exact expected candidate lists. Out — any configuration flag for exclusions; general `cfg` expression evaluation; non-Rust rules. + - Dependencies: T05 + - Done when: every listed exclusion has a fixture proving production candidates remain while excluded-region candidates are absent, and these exact regression tests pass: + - This fixture yields exactly one candidate, `swap_boolean` on the `true` in `production`: + + ```rust + #[cfg(test)] + mod tests { + fn example() -> bool { true } + } + + #[cfg(test)] + fn test_helper() -> bool { false } + + #[test] + fn example_test() { + assert!(true); + } + + #[allow(dead_code)] + #[cfg(test)] + mod more_tests { + fn example() -> bool { true } + } + + fn production() -> bool { true } + ``` + + - `#[cfg(not(test))] fn production_only() -> bool { true }` yields its `swap_boolean` candidate. + - A `#[cfg(test)] mod tests { mod inner { fn nested() -> bool { true } } #[test] fn t() { let _ = true; } }` fixture yields no candidates. + - `#[cfg(test)]` followed by `#[allow(dead_code)]` (reversed order), and `#[cfg(test)]` separated from its item by a doc comment, both exclude the item. + - An unannotated `mod helpers` containing `fn h() -> bool { true }` beside a `#[test]` function keeps the `h` candidate and drops the test function's candidates. + - Unsupported forms (`#[cfg(any(test, unix))]`, `#[tokio::test]`) keep their candidates, documenting the declared scope. + - Verify: `nix build .#checks..skaza-tests`. + - Context synchronization: pending + +- [ ] T07: `Render text and JSON discovery reports from the CLI` (status:todo) + - Task ID: T07 + - Scope: In — `src/report.rs`: stable JSON document (discovery-only marker, `total`, `selected`, `limit`, ordered candidates) and text rendering (discovery-only header, total vs. selected, `path:line:column`, operator, `original -> replacement`); `src/lib.rs` discovery entrypoint and `src/main.rs` wiring that writes the report to stdout and diagnostics to stderr with non-zero exit on errors, writing nothing to stdout when any selected file fails (including invalid Rust syntax from T05), in both formats; empty-result handling; unit tests for JSON serialization and exact text output. Out — persistence, SARIF, history. + - Dependencies: T06 + - Done when: both formats render exact expected output for fixture candidates and for zero candidates; JSON round-trips through `serde_json`; nothing but the report reaches stdout, and a failing discovery writes nothing to stdout. + - Verify: `nix build .#checks..skaza-tests`; `nix develop -c cargo run --manifest-path crates/skaza/Cargo.toml -- --src crates/skaza/src --format json` parses as JSON via `nix shell nixpkgs#jq -c jq .`. + - Context synchronization: pending + +- [ ] T08: `Add binary integration tests and dogfood against repository_identity` (status:todo) + - Task ID: T08 + - Scope: In — `crates/skaza/tests/` tests that run the built `skaza` executable (`CARGO_BIN_EXE_skaza`) over a temporary Rust tree: repeated `--src`, text and JSON modes, identical JSON across two runs, identical selection across reversed `--src` order, `--limit` truncation applied after dedup with correct total/selected, source integrity (exact bytes of every selected file captured before running both formats and compared byte-for-byte afterward, with an unrelated non-selected file in the tree modified beforehand so the test does not depend on a clean tree), a malformed file alongside a valid file in `--format json` (non-zero exit, empty stdout, path:line:column diagnostic on stderr), zero-limit and invalid-path errors on stderr; fix any defects the dogfood run against `cli/src/services/repository_identity/mod.rs` exposes in Skaza code. Out — changes to any file under `cli/`; `git status` checks; any destructive Git command. + - Dependencies: T07 + - Done when: integration tests pass in the Nix sandbox; the dogfood command yields 1–10 candidates, all outside the `#[cfg(test)]` module, with identical JSON across two runs and the dogfooded file byte-for-byte unchanged, whether or not unrelated repository files are dirty. + - Verify: `nix build .#checks..skaza-tests`; record `cksum cli/src/services/repository_identity/mod.rs`, run `nix develop -c cargo run --manifest-path crates/skaza/Cargo.toml -- --src cli/src/services/repository_identity/mod.rs --limit 10 --format json` twice and diff, then confirm `cksum` is unchanged. + - Context synchronization: pending + +## Open questions + +- Should `nix develop -c cargo test --manifest-path crates/skaza/Cargo.toml` be exempted from the repository's `cargo test` bash policy (for example via `satisfied_by` or a Skaza-specific rule), or is the `skaza-tests` flake check enough? The plan treats the flake check as canonical and does not change `.sce/config.json`. diff --git a/crates/skaza/Cargo.lock b/crates/skaza/Cargo.lock new file mode 100644 index 000000000..1ef086d37 --- /dev/null +++ b/crates/skaza/Cargo.lock @@ -0,0 +1,587 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "bstr" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f" +dependencies = [ + "memchr", + "serde_core", +] + +[[package]] +name = "cc" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f74872d07caf508b30a21f6836e7d7016a2eaf7d9ff4f48deaa58cd8a0407630" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "clap" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "crossbeam-deque" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "find-msvc-tools" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "globset" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07c34a9410465b45bd9787443bc7370f37735bad04b0f0cd57ff1a3186c98988" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "ignore" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b69833ed729dc5aa7d19541d96d6cf8e9137194207a04916d658e43168402f" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "libc" +version = "0.2.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rustix" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "indexmap", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "skaza" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "ignore", + "serde", + "serde_json", + "tempfile", + "thiserror", + "tree-sitter", + "tree-sitter-rust", +] + +[[package]] +name = "streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tree-sitter" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78f873475d258561b06f1c595d93308a7ed124d9977cb26b148c2084a4a3cc87" +dependencies = [ + "cc", + "regex", + "regex-syntax", + "serde_json", + "streaming-iterator", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-language" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0af592be68c579aa78a16846bd19422978c3c52e438523d45ff5d1bff1f9d4a" + +[[package]] +name = "tree-sitter-rust" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca8ccb3e3a3495c8a943f6c3fd24c3804c471fd7f4f16087623c7fa4c0068e8a" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "unicode-ident" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/crates/skaza/Cargo.toml b/crates/skaza/Cargo.toml new file mode 100644 index 000000000..c7f59ebd6 --- /dev/null +++ b/crates/skaza/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "skaza" +version = "0.1.0" +edition = "2021" +description = "Discovery-only Rust mutation candidate finder" +license = "Apache-2.0" +publish = false + +[lib] +path = "src/lib.rs" + +[[bin]] +name = "skaza" +path = "src/main.rs" + +[dependencies] +anyhow = "1" +clap = { version = "4", features = ["derive"] } +ignore = "0.4" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +thiserror = "2" +tree-sitter = "0.25" +tree-sitter-rust = "0.23" + +[dev-dependencies] +tempfile = "3" diff --git a/crates/skaza/queries/rust/.gitkeep b/crates/skaza/queries/rust/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/crates/skaza/src/lib.rs b/crates/skaza/src/lib.rs new file mode 100644 index 000000000..d713e6393 --- /dev/null +++ b/crates/skaza/src/lib.rs @@ -0,0 +1,131 @@ +pub mod mutation; +pub mod report; +pub mod rust; +pub mod source; + +use std::num::NonZeroUsize; +use std::path::PathBuf; + +use clap::{Parser, ValueEnum}; + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)] +pub enum Format { + #[default] + Text, + Json, +} + +#[derive(Clone, Debug, Eq, PartialEq, Parser)] +#[command( + name = "skaza", + version, + about = "Discover Rust mutation candidates without applying or executing them" +)] +pub struct Args { + #[arg( + long = "src", + value_name = "PATH", + default_value = ".", + help = "Rust source file or directory to scan; repeat to scan several" + )] + pub src: Vec, + + #[arg( + long, + value_name = "N", + default_value = "10", + value_parser = parse_limit, + help = "Maximum number of candidates to report (must be at least 1)" + )] + pub limit: NonZeroUsize, + + #[arg( + long, + value_enum, + default_value_t = Format::Text, + help = "Report format" + )] + pub format: Format, +} + +fn parse_limit(value: &str) -> Result { + let parsed: usize = value.parse().map_err(|_| { + format!("`{value}` is not a positive integer; pass a value such as `--limit 10`") + })?; + NonZeroUsize::new(parsed).ok_or_else(|| { + "limit must be at least 1; pass a positive value such as `--limit 10`".to_string() + }) +} + +#[cfg(test)] +mod tests { + use std::num::NonZeroUsize; + use std::path::PathBuf; + + use clap::error::ErrorKind; + use clap::Parser; + + use super::{Args, Format}; + + #[test] + fn defaults_scan_current_directory_with_limit_ten_in_text() { + let args = Args::try_parse_from(["skaza"]).unwrap(); + assert_eq!( + args, + Args { + src: vec![PathBuf::from(".")], + limit: NonZeroUsize::new(10).unwrap(), + format: Format::Text, + } + ); + } + + #[test] + fn repeated_src_values_are_kept_in_order() { + let args = + Args::try_parse_from(["skaza", "--src", "a.rs", "--src", "dir", "--src", "a.rs"]) + .unwrap(); + assert_eq!( + args.src, + vec![ + PathBuf::from("a.rs"), + PathBuf::from("dir"), + PathBuf::from("a.rs"), + ] + ); + } + + #[test] + fn explicit_limit_and_json_format_are_accepted() { + let args = Args::try_parse_from(["skaza", "--limit", "3", "--format", "json"]).unwrap(); + assert_eq!(args.limit.get(), 3); + assert_eq!(args.format, Format::Json); + } + + #[test] + fn zero_limit_is_rejected_with_actionable_error() { + let error = Args::try_parse_from(["skaza", "--limit", "0"]).unwrap_err(); + assert_eq!(error.kind(), ErrorKind::ValueValidation); + assert!(error + .to_string() + .contains("limit must be at least 1; pass a positive value such as `--limit 10`")); + } + + #[test] + fn non_numeric_limit_is_rejected() { + let error = Args::try_parse_from(["skaza", "--limit", "many"]).unwrap_err(); + assert_eq!(error.kind(), ErrorKind::ValueValidation); + } + + #[test] + fn invalid_format_is_rejected() { + let error = Args::try_parse_from(["skaza", "--format", "sarif"]).unwrap_err(); + assert_eq!(error.kind(), ErrorKind::InvalidValue); + } + + #[test] + fn subcommands_are_not_accepted() { + let error = Args::try_parse_from(["skaza", "run"]).unwrap_err(); + assert_eq!(error.kind(), ErrorKind::UnknownArgument); + } +} diff --git a/crates/skaza/src/main.rs b/crates/skaza/src/main.rs new file mode 100644 index 000000000..aa7718408 --- /dev/null +++ b/crates/skaza/src/main.rs @@ -0,0 +1,7 @@ +use clap::Parser; + +use skaza::Args; + +fn main() { + let _args = Args::parse(); +} diff --git a/crates/skaza/src/mutation.rs b/crates/skaza/src/mutation.rs new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/crates/skaza/src/mutation.rs @@ -0,0 +1 @@ + diff --git a/crates/skaza/src/report.rs b/crates/skaza/src/report.rs new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/crates/skaza/src/report.rs @@ -0,0 +1 @@ + diff --git a/crates/skaza/src/rust.rs b/crates/skaza/src/rust.rs new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/crates/skaza/src/rust.rs @@ -0,0 +1 @@ + diff --git a/crates/skaza/src/source.rs b/crates/skaza/src/source.rs new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/crates/skaza/src/source.rs @@ -0,0 +1 @@ + diff --git a/crates/skaza/tests/.gitkeep b/crates/skaza/tests/.gitkeep new file mode 100644 index 000000000..e69de29bb