From 829c9a246c7315a00f4420b88e3aeffa1daacb36 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Mon, 4 Nov 2024 12:00:02 +1100 Subject: [PATCH 01/16] preparatory work : now all references to `matches!()` to use alias `std_matches!()` --- src/lib.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 3812d38..c1727b8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,7 @@ use std::{ error as std_error, fmt as std_fmt, + matches as std_matches, result as std_result, }; @@ -1480,7 +1481,7 @@ impl CompiledMatcher { let mut num_bytes = 0; for c in pattern.chars() { - debug_assert!(continuum_prior.is_none() || matches!(state, ParseState::InNotRange | ParseState::InRange)); + debug_assert!(continuum_prior.is_none() || std_matches!(state, ParseState::InNotRange | ParseState::InRange)); if escaped { match c { @@ -1501,7 +1502,7 @@ impl CompiledMatcher { escaped = false; - if matches!(state, ParseState::None) { + if std_matches!(state, ParseState::None) { state = ParseState::InLiteral; } } else { @@ -1575,7 +1576,7 @@ impl CompiledMatcher { }, }; - minimum_required = if matches!(state, ParseState::InRange) { + minimum_required = if std_matches!(state, ParseState::InRange) { matchers.prepend_Range(character_range, flags, minimum_required) } else { matchers.prepend_NotRange(character_range, flags, minimum_required) From d31eaed50ffb13944e643dce4dde1758561efc7d Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Mon, 4 Nov 2024 14:09:03 +1100 Subject: [PATCH 02/16] + added macros `assert_shwild_matches!()` and `assert_shwild_not_matches!()` --- Cargo.lock | 5 ++-- Cargo.toml | 8 ++++- src/lib.rs | 87 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 95 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 84eca69..603da24 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -31,9 +31,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "base-traits" -version = "0.0.8" +version = "0.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556031355dea4fc81a56aa4850032c9b433c926e7296c30780f63ece19ecf082" +checksum = "a9c676f0beeccff874a11e4853c9b6f254cdc64ab1135a63b2968602820067e5" [[package]] name = "bumpalo" @@ -423,6 +423,7 @@ dependencies = [ name = "shwild" version = "0.1.2" dependencies = [ + "base-traits", "collect-rs", "criterion", "regex", diff --git a/Cargo.toml b/Cargo.toml index 1354bc8..c49d00e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -95,7 +95,13 @@ test-regex = [ [dependencies] -collect-rs = { version = "0.2", optional = true } +base-traits = "*" + +# base-traits = { version = "0", default-features = false, features = [ +# "implement-ASI64-for-built_ins", +# ]} +collect-rs = { version = "0.2", optional = true, default-features = false, features = [ +]} regex = { version = "1.11", optional = true } diff --git a/src/lib.rs b/src/lib.rs index c1727b8..c09760c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1884,13 +1884,62 @@ macro_rules! shwild_matches { }; } +#[macro_export] +macro_rules! assert_shwild_matches { + ($expected_pattern:expr, $actual:expr) => { + + assert_shwild_matches!($expected_pattern, $actual, 0i64); + }; + ($expected_pattern:expr, $actual:expr, $flags:expr) => { + let expected_pattern : &str = &$expected_pattern; + let actual = &$actual; + let flags : &dyn base_traits::AsI64 = &$flags; + let flags = flags.as_i64(); + + match $crate::matches(expected_pattern, actual, flags) { + Err(e) => { + panic!("could not evaluate actual value due to a failure in the parsing of expected pattern '{}': {}", expected_pattern, e); + }, + Ok(b) => { + assert!(b, "assertion failed: actual value '{}' does not match the expected pattern '{}'", actual, expected_pattern); + } + } + }; +} + +#[macro_export] +macro_rules! assert_shwild_not_matches { + ($expected_pattern:expr, $actual:expr) => { + + assert_shwild_not_matches!($expected_pattern, $actual, 0i64); + }; + ($expected_pattern:expr, $actual:expr, $flags:expr) => { + let expected_pattern : &str = &$expected_pattern; + let actual = &$actual; + let flags : &dyn base_traits::AsI64 = &$flags; + let flags = flags.as_i64(); + + match $crate::matches(expected_pattern, actual, flags) { + Err(e) => { + panic!("could not evaluate actual value due to a failure in the parsing of expected pattern '{}': {}", expected_pattern, e); + }, + Ok(b) => { + assert!(!b, "assertion failed: actual value '{}' match unexpectedly with the pattern '{}'", actual, expected_pattern); + } + } + }; +} + #[cfg(test)] mod tests { #![allow(non_snake_case)] - use crate as shwild; - use crate::shwild_matches; + use crate::{ + assert_shwild_matches, + self as shwild, + shwild_matches, + }; mod TEST_CompiledMatcher_PARSING { @@ -3308,6 +3357,40 @@ mod tests { } } } + + + mod TEST_ASSERTION_MATCHES { + #![allow(non_snake_case)] + + use super::*; + + + #[test] + fn TEST_assert_shwild_matches_1() { + assert_shwild_matches!("[a-d]", "a"); + assert_shwild_matches!("[a-d]", "b"); + assert_shwild_matches!("[a-d]", "c"); + + assert_shwild_matches!("?", "a"); + assert_shwild_matches!("?", "z"); + } + + #[test] + #[should_panic(expected = "could not evaluate actual value due to a failure in the parsing of expected pattern '[a-d': pattern syntax error (at 0:4): incomplete range")] + fn TEST_assert_shwild_matches_2() { + assert_shwild_matches!("[a-d", "a"); + } + + #[test] + fn TEST_assert_shwild_not_matches_1() { + assert_shwild_not_matches!("[a-d]", "e"); + assert_shwild_not_matches!("[a-d]", "f"); + assert_shwild_not_matches!("[a-d]", "D"); + + assert_shwild_not_matches!("??", "a"); + assert_shwild_not_matches!("??", "z"); + } + } } From 6fc87952382ff6e5dfc8b09a78de00c90699c103 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Tue, 7 Jul 2026 19:10:40 +1000 Subject: [PATCH 03/16] squash-commit --- .cargo/config.toml | 3 + .cursor/rules/rust-standards.mdc | 51 ++++ .gitattributes | 2 + .github/workflows/ci.yml | 72 +++++ .gitignore | 1 + CHANGES.md | 57 ++++ Cargo.lock | 251 +++++------------- Cargo.toml | 33 ++- EXAMPLES.md | 11 + NEWS.md | 12 + README.md | 30 ++- TODO.md | 103 +++++++ benches/cw-regex.rs | 7 +- benches/range_string-creation_functions.rs | 41 +-- benches/shwild-compiled_matcher.rs | 5 +- benches/shwild-matches.rs | 5 +- examples/character-play.md | 99 +++++++ examples/list-matching-files-compiled.md | 113 ++++++++ examples/list-matching-files-compiled/main.rs | 2 +- examples/list-matching-files.md | 109 ++++++++ .rustfmt.toml => rustfmt.toml | 10 +- scripts/check_derives.py | 129 +++++++++ scripts/check_doc_76.py | 66 +++++ scripts/check_test_names.py | 239 +++++++++++++++++ scripts/fmt | 12 + src/lib.rs | 75 ++++-- 26 files changed, 1262 insertions(+), 276 deletions(-) create mode 100644 .cargo/config.toml create mode 100644 .cursor/rules/rust-standards.mdc create mode 100644 .github/workflows/ci.yml create mode 100644 CHANGES.md create mode 100644 EXAMPLES.md create mode 100644 NEWS.md create mode 100644 TODO.md create mode 100644 examples/character-play.md create mode 100644 examples/list-matching-files-compiled.md create mode 100644 examples/list-matching-files.md rename .rustfmt.toml => rustfmt.toml (92%) create mode 100755 scripts/check_derives.py create mode 100644 scripts/check_doc_76.py create mode 100644 scripts/check_test_names.py create mode 100755 scripts/fmt diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..bcd5e07 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,3 @@ +# .cargo/config.toml +# +# Formatting requires nightly rustfmt — use ./scripts/fmt (see rustfmt.toml). diff --git a/.cursor/rules/rust-standards.mdc b/.cursor/rules/rust-standards.mdc new file mode 100644 index 0000000..5c0edd5 --- /dev/null +++ b/.cursor/rules/rust-standards.mdc @@ -0,0 +1,51 @@ +--- +description: Rust coding standards for shwild.Rust (aligned with Synesis Information Systems' internal project standards) +globs: **/*.rs +alwaysApply: false +--- + +# Rust Software Quality Rules + + +## DOC_76 (The 76-Rule for Documentation Comments) + +- Documentation comments (`///`, `//!`) on **public** constructs must be at + most **76 characters** per line, including leading indentation; +- Wrap text **greedily** (pack to maximum width) — do not wrap early if the + next word fits on the current line; +- Code blocks inside doc comments (triple backticks) are exempt; +- List items in doc comments should end with a semicolon; +- Inline comments (`//`) inside functions are limited to **100 characters** + per line, wrapped greedily; +- Private/non-`pub` constructs are exempt from DOC_76 for their doc + comments; + +Run `./scripts/check_doc_76.py` to verify compliance. + + +## DERIVE_LAYOUT (Derive Layout & Formatting) + +Multi-trait `#[derive(...)]` macros must be split into separate, +single-trait lines, ordered alphabetically by trait name. Tightly coupled +traits may remain on one line: `#[derive(Eq, PartialEq)]`, +`#[derive(Ord, PartialOrd)]`. + +`rustfmt.toml` sets `merge_derives = false` to preserve this layout. + +Run `./scripts/check_derives.py` to verify compliance. + + +## RUST_TEST_NAMING + +All test methods must use `SHOUTING_SNAKE_CASE` - e.g. `TEST_PARSING()` - +except where a word in the snake case represents a specific construct (such +as a struct, enum, type, function, macro, or field name), in which case that +word must preserve its exact correct case - e.g. +`TEST_AutoBuffer_WITH_INTERNAL_SIZE()`, `TEST_doom_scope_1()`, +`TEST_type_name_only_WITH_SomeCustomType()`. + +When a construct is embedded as a SHOUTING_SNAKE_CASE constant (or other +construct name), use an extra underscore on each side as a delimiter - e.g. +`TEST_MatcherSequence_WITH_Range_HAVING__IGNORE_CASE__1()`. + +Run `./scripts/check_test_names.py` to verify compliance. diff --git a/.gitattributes b/.gitattributes index e69de29..7beaa80 100644 --- a/.gitattributes +++ b/.gitattributes @@ -0,0 +1,2 @@ + +*.rs linguist-language=Rust diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5084a8c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,72 @@ +name: CI + +on: + push: + branches: + - master + - dev + - boilerplate + pull_request: + +env: + CARGO_TERM_COLOR: always + +jobs: + check: + name: Test, Clippy, Fmt, Checkers + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy + + - name: Install nightly rustfmt + run: rustup toolchain install nightly --profile minimal --component rustfmt + + - uses: Swatinem/rust-cache@v2 + + - name: cargo test + run: cargo test --locked + + - name: cargo clippy + run: cargo clippy --all-targets --locked -- -D warnings + + - name: cargo build (examples) + run: cargo build --examples --locked + + - name: cargo build (character-play example) + run: cargo build --example character-play --features test-regex --locked + + - name: cargo doc + run: cargo doc --no-deps --locked + + - name: rustfmt + run: ./scripts/fmt --check + + - name: DOC_76 checker + run: python3 scripts/check_doc_76.py + + - name: RUST_TEST_NAMING checker + run: python3 scripts/check_test_names.py + + - name: DERIVE_LAYOUT checker + run: python3 scripts/check_derives.py + + msrv: + name: MSRV (1.79) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@1.79.0 + + - uses: Swatinem/rust-cache@v2 + + - name: cargo check (library) + # Full `cargo test` needs dev-deps (criterion → clap_lex 2024 edition), + # which exceeds MSRV; the stable job runs the full test suite. + # MSRV 1.79: default `lookup-ranges` → collect-rs → base-traits uses + # `CStr::count_bytes()` (stabilized in Rust 1.79). + run: cargo check --lib --locked diff --git a/.gitignore b/.gitignore index 3589c67..8ea0d84 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ # directories (by name) /_build/ +/scripts/__pycache__/ /scratch/ /target/ diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 0000000..d4484d1 --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,57 @@ +# shwild.Rust - CHANGES + + +## 0.1.4 - 8th July 2026 + +* added **CHANGES.md** (back-filled) and **NEWS.md**; +* added **EXAMPLES.md** and per-example documentation; +* **README.md** badges, dependency links, related projects, and `null-feature` documentation; +* added crate-level `//!` documentation; +* added CI (`.github/workflows/ci.yml`) and quality scripts (`scripts/fmt`, checkers); +* added `rust-version` (MSRV 1.79); +* renamed `.rustfmt.toml` => **rustfmt.toml**; updated formatting settings; +* added `.gitattributes`; +* shortened `description` in **Cargo.toml**; +* Clippy and test-naming fixes for CI; `check_test_names.py` allows `__CONSTRUCT__` padding; +* upgraded **criterion** from 0.5 => 0.8; + + +## 0.1.3 - 28th March 2025 + +* crates.io packaging metadata — `categories`, `keywords`, `documentation`, and expanded `description`; +* added `exclude` for `target` and `.github` in **Cargo.toml**; +* added **TODO.md**; +* **character-play** changed from `[[bin]]` to `[[example]]`; + + +## 0.1.2 - 3rd November 2024 + +* added `test-regex` feature — optional **regex** dependency for benchmarks and scratch programs; +* enabled `lookup-ranges` in default features; +* added **cw-regex** benchmark; +* added **character-play** scratch program; +* added `regex_comparision_tests` unit tests (gated on `test-regex`); + + +## 0.1.1 - 3rd November 2024 + +* added `lookup-ranges` feature — optional **collect-rs** dependency for `UnicodePointMap`-based range matching; +* added **Cargo.lock**; +* README: added Features section; clarified Wild-1 escape behaviour in pattern elements; + + +## 0.1.0 - 3rd November 2024 + +* first public release; +* added `matches()` and `shwild_matches!()`; +* added `CompiledMatcher`; +* added `Error`, `Result`, and `IGNORE_CASE`; +* added example programs **list-matching-files** and **list-matching-files-compiled**; +* added benchmarks **range_string-creation_functions**, **shwild-compiled_matcher**, and **shwild-matches**; +* added **README.md**; + + +All history before this day is moot! + + + diff --git a/Cargo.lock b/Cargo.lock index 603da24..2a69046 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 4 +version = 3 [[package]] name = "aho-corasick" @@ -11,6 +11,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloca" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4" +dependencies = [ + "cc", +] + [[package]] name = "anes" version = "0.1.6" @@ -35,18 +44,22 @@ version = "0.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9c676f0beeccff874a11e4853c9b6f254cdc64ab1135a63b2968602820067e5" -[[package]] -name = "bumpalo" -version = "3.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" - [[package]] name = "cast" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" +[[package]] +name = "cc" +version = "1.2.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -116,25 +129,22 @@ dependencies = [ [[package]] name = "criterion" -version = "0.5.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3" dependencies = [ + "alloca", "anes", "cast", "ciborium", "clap", "criterion-plot", - "is-terminal", "itertools", "num-traits", - "once_cell", "oorandom", - "plotters", - "rayon", + "page_size", "regex", "serde", - "serde_derive", "serde_json", "tinytemplate", "walkdir", @@ -142,39 +152,14 @@ dependencies = [ [[package]] name = "criterion-plot" -version = "0.5.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea" dependencies = [ "cast", "itertools", ] -[[package]] -name = "crossbeam-deque" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" - [[package]] name = "crunchy" version = "0.2.2" @@ -187,6 +172,12 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + [[package]] name = "half" version = "2.4.1" @@ -197,28 +188,11 @@ dependencies = [ "crunchy", ] -[[package]] -name = "hermit-abi" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" - -[[package]] -name = "is-terminal" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" -dependencies = [ - "hermit-abi", - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "itertools" -version = "0.10.5" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] @@ -229,26 +203,11 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" -[[package]] -name = "js-sys" -version = "0.3.72" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" -dependencies = [ - "wasm-bindgen", -] - [[package]] name = "libc" -version = "0.2.161" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" - -[[package]] -name = "log" -version = "0.4.22" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "memchr" @@ -265,12 +224,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "once_cell" -version = "1.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" - [[package]] name = "oorandom" version = "11.1.4" @@ -278,31 +231,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" [[package]] -name = "plotters" -version = "0.3.7" +name = "page_size" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" dependencies = [ - "num-traits", - "plotters-backend", - "plotters-svg", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "plotters-backend" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" - -[[package]] -name = "plotters-svg" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" -dependencies = [ - "plotters-backend", + "libc", + "winapi", ] [[package]] @@ -323,26 +258,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "rayon" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - [[package]] name = "regex" version = "1.11.1" @@ -419,9 +334,15 @@ dependencies = [ "serde", ] +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + [[package]] name = "shwild" -version = "0.1.2" +version = "0.2.0" dependencies = [ "base-traits", "collect-rs", @@ -477,69 +398,20 @@ dependencies = [ ] [[package]] -name = "wasm-bindgen" -version = "0.2.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" -dependencies = [ - "cfg-if", - "once_cell", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.95" +name = "winapi" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", ] [[package]] -name = "wasm-bindgen-shared" -version = "0.2.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" - -[[package]] -name = "web-sys" -version = "0.3.72" +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" -dependencies = [ - "js-sys", - "wasm-bindgen", -] +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" @@ -547,17 +419,14 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.59.0", + "windows-sys", ] [[package]] -name = "windows-sys" -version = "0.52.0" +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets", -] +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-sys" diff --git a/Cargo.toml b/Cargo.toml index c49d00e..dad0525 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,14 +8,31 @@ authors = [ "Matt Wilson ", "Zach Struck ", ] -description = "shwild (for Rust)" +categories = [ + "command-line-interface", + "parser-implementations", + "development-tools", + "text-processing", +] +description = "Shell-compatible wildcard matching library" +documentation = "https://docs.rs/shwild" edition = "2021" +exclude = [ + "target", + ".github" +] homepage = "https://github.com/synesissoftware/shwild.Rust" +keywords = [ + "pattern-matching", + "shell", + "wildcards", +] license = "BSD-3-Clause" name = "shwild" readme = "README.md" repository = "https://github.com/synesissoftware/shwild.Rust" -version = "0.1.2" +rust-version = "1.79" +version = "0.2.0" # ########################################################## @@ -44,7 +61,7 @@ harness = false name = "shwild-matches" harness = false -[[bin]] +[[example]] name = "character-play" path = "test/scratch/character-play/main.rs" required-features = [ @@ -102,13 +119,17 @@ base-traits = "*" # ]} collect-rs = { version = "0.2", optional = true, default-features = false, features = [ ]} -regex = { version = "1.11", optional = true } +regex = { version = "1.11", optional = true, default-features = false, features = [ +]} [dev-dependencies] -criterion = { version = "0.5" } -test_help-rs = { version = "0.1" } +criterion = { version = "0.8", default-features = false, features = [ + "html_reports", +]} +test_help-rs = { version = "0.1", default-features = false, features = [ +]} # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/EXAMPLES.md b/EXAMPLES.md new file mode 100644 index 0000000..e1f50c6 --- /dev/null +++ b/EXAMPLES.md @@ -0,0 +1,11 @@ +# shwild.Rust Examples + +|Name|Source & Description|Summary| +|---|---|---| +|**list-matching-files**|[examples/list-matching-files/main.rs](/examples/list-matching-files/main.rs)
[examples/list-matching-files.md](/examples/list-matching-files.md)|Lists files in the current directory whose paths match one or more shell wildcard pattern(s), using `shwild::matches()`.| +|**list-matching-files-compiled**|[examples/list-matching-files-compiled/main.rs](/examples/list-matching-files-compiled/main.rs)
[examples/list-matching-files-compiled.md](/examples/list-matching-files-compiled.md)|Same as **list-matching-files**, but patterns are compiled once into `CompiledMatcher` instances before matching.| +|**character-play**|[test/scratch/character-play/main.rs](/test/scratch/character-play/main.rs)
[examples/character-play.md](/examples/character-play.md)|Scratch program exercising **regex** crate Unicode matching behaviour (requires feature `"test-regex"`).| + + + + diff --git a/NEWS.md b/NEWS.md new file mode 100644 index 0000000..6d66629 --- /dev/null +++ b/NEWS.md @@ -0,0 +1,12 @@ +# shwild.Rust - NEWS + +| Date | News Item | +| --------------------- | ----------------------------------------- | +| 8th July 2026 | shwild.Rust 0.1.4 released | +| 28th March 2025 | shwild.Rust 0.1.3 released | +| 3rd November 2024 | shwild.Rust 0.1.2 released | +| 3rd November 2024 | shwild.Rust 0.1.1 released | +| 3rd November 2024 | shwild.Rust 0.1.0 released | + + + diff --git a/README.md b/README.md index 20649fa..cdd359e 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,15 @@ -# shwild - -**SH**ell-compatible **WILD**cards, for **Rust**. +# shwild.Rust +![Language](https://img.shields.io/badge/Rust-000000?style=flat&logo=rust&logoColor=white) +[![License](https://img.shields.io/badge/License-BSD_3--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause) [![Crates.io](https://img.shields.io/crates/v/shwild.svg)](https://crates.io/crates/shwild) +[![GitHub release](https://img.shields.io/github/v/release/synesissoftware/shwild.Rust.svg)](https://github.com/synesissoftware/shwild.Rust/releases/latest) +![MSRV](https://img.shields.io/badge/MSRV-1.79-lightgrey) +[![CI](https://github.com/synesissoftware/shwild.Rust/actions/workflows/ci.yml/badge.svg)](https://github.com/synesissoftware/shwild.Rust/actions/workflows/ci.yml) +[![Last Commit](https://img.shields.io/github/last-commit/synesissoftware/shwild.Rust)](https://github.com/synesissoftware/shwild.Rust/commits/master) +[![docs.rs](https://img.shields.io/docsrs/shwild/badge.svg)](https://docs.rs/shwild) + +**SH**ell-compatible **WILD**cards, for **Rust** — part of the cross-language **shwild** family. ## Table of Contents @@ -30,9 +37,7 @@ ## Introduction -**shwild** is a small, standalone library, implemented in C++ with a C and a C++ API, that provides shell-compatible wildcard matching. - -**shwild.Rust** is a **Rust** port, with minimal API differences. The design emphasis is on simplicity-of-use, modularity, and performance. +**shwild** is a small, standalone library, implemented in C++ with a C and a C++ API, that provides shell-compatible wildcard matching. **shwild.Rust** is a **Rust** port, with minimal API differences. The design emphasis is on simplicity-of-use, modularity, and performance. ```Rust let pattern = r"Where are the* [🐼🐻]s\?"; @@ -64,7 +69,7 @@ The library (and other **shwild** variants) support the following pattern elemen Reference in **Cargo.toml** in the usual way: ```toml -shwild = { version = "~0.1" } +shwild = { version = "0.1" } ``` @@ -104,6 +109,7 @@ The following crate features are defined: | Name | Effect | Is `"default"`? | Dependent feature(s) | | --------------------------- | ------------------------------------- | --------------- | ------------------------------------- | | `"lookup-ranges"` | Causes match/non-match ranges to be implemented in terms of `UnicodePointMap` (from **collect-rs** crate), resulting in significant performance improvements in parsing and matching | Yes | | +| `"null-feature"` | A feature that has no effect (and, thus, is useful for simplifying driver scripts) | **No** | | | `"test-regex"` | Introduces a dependency to **regex** crate to support benchmark/example program(s) | **No** | | @@ -167,7 +173,7 @@ No public traits are defined at this time. ## Examples -T.B.C. +Examples are provided in the ```examples``` directory, along with a markdown description for each. A detailed list TOC of them is provided in [EXAMPLES.md](./EXAMPLES.md). ## Project Information @@ -186,8 +192,8 @@ Defect reports, feature requests, and pull requests are welcome on https://githu **shwild.Rust** has two dependencies, both optional: -* [**collect-rs**]() - required, for more efficient range matching, if feature `"lookup-ranges"` is specified; -* [**regex**]() - required, by some benchmark/example programs only, if feature `"test-regex"` is specified; +* [**collect-rs**](https://github.com/synesissoftware/collect-rs) - required, for more efficient range matching, if feature `"lookup-ranges"` is specified; +* [**regex**](https://github.com/rust-lang/regex) - required, by some benchmark/example programs only, if feature `"test-regex"` is specified; #### Dev Dependencies @@ -200,7 +206,9 @@ Crates upon which **shwild** has development dependencies: ### Related projects -None at this time. +* [**shwild**](https://github.com/synesissoftware/shwild/); +* [**shwild.Go**](https://github.com/synesissoftware/shwild.Go/); +* [**collect-rs**](https://github.com/synesissoftware/collect-rs/); ### License diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..90c33f3 --- /dev/null +++ b/TODO.md @@ -0,0 +1,103 @@ +# shwild.Rust - TODO + + +## Table of Contents + +- [0.1.4 boilerplate checklist](#014-boilerplate-checklist) +- [Source layout (deferred)](#source-layout-deferred) +- [Functional improvements](#functional-improvements) +- [Performance improvements](#performance-improvements) + + +## 0.1.4 boilerplate checklist + +### Cargo packaging + +* [x] `default-features = false` on optional and dev dependencies; +* [x] `rust-version` (MSRV 1.79); +* [x] `version = "0.1.4"`; +* [x] upgraded **criterion** from 0.5 => 0.8; +* [x] ~~narrow `exclude` (drop `.github`)~~ — **won't fix** (keep `exclude = ["target", ".github"]` since 0.1.3); + +### CI + +* [x] `.github/workflows/ci.yml` (test, clippy, examples, doc, fmt, checkers); +* [x] Clippy, DOC_76, and test-naming fixes; +* [x] MSRV job on 1.79 (`cargo check --lib --locked`); +* [x] `check_test_names.py` allows `__CONSTRUCT__` padding around construct names; + +### Documentation + +* [x] back-filled **CHANGES.md** (0.1.0–0.1.3) and **0.1.4** entry; +* [x] crate-level `//!` documentation; +* [x] **EXAMPLES.md** and per-example `.md` files; +* [x] **NEWS.md** release table (through 0.1.4); +* [x] **README.md** badges, dependency links, related projects, `null-feature`; +* [x] shortened `description` in **Cargo.toml**; + +### Formatting and quality tooling + +* [x] `.cargo/config.toml`; +* [x] `.cursor/rules/rust-standards.mdc`; +* [x] `.gitattributes`; +* [x] `.gitignore` (`/scripts/__pycache__/`); +* [x] `scripts/check_derives.py`, `check_doc_76.py`, `check_test_names.py`, `fmt`; +* [x] renamed `.rustfmt.toml` => **rustfmt.toml**; + +### Layout and structure + +* [x] ~~flatten examples to `examples/*.rs`~~ — **won't fix** (keep `examples/*/main.rs`); +* [x] ~~move **character-play** from `[[example]]` to `[[bin]]`~~ — **won't fix** (since 0.1.3); +* [x] ~~normalise bench filenames to snake_case~~ — **won't fix** (keep current names); + +### Release + +* [ ] merge `boilerplate` branch and tag **0.1.4** on GitHub; +* [ ] publish **0.1.4** to crates.io; + + +## Source layout (deferred) + +Split `src/lib.rs` (~3,450 lines) into multiple source files. **Defer past 0.1.4** +— behaviour-neutral refactor; ship as a dedicated follow-up change. + +**API unit tests remain in `src/lib.rs`** (`#[cfg(test)] mod tests { ... }` and +`regex_comparision_tests`). + +Proposed module layout: + +``` +src/ + lib.rs # crate docs, re-exports, matches(), shwild_matches! macro; + # #[cfg(test)] API unit tests (stay here) + constants.rs # IGNORE_CASE + error.rs # Error + Display/Error trait impls + traits.rs # Match trait + types.rs # CharacterRangeType + match_structures.rs # MatchLiteral, MatchRange, MatchWildN, ... + Match impls + utils.rs # MatcherSequence, parsing helpers, prepare_range_string + compiled_matcher.rs # CompiledMatcher, ParseState, parse logic +``` + +Suggested order of work: + +1. Extract internal modules (`match_structures`, `utils`, `compiled_matcher`, …) + — largest line-count reduction with lowest risk; +2. Extract `error`, `constants`, `traits`, `types` — thin `lib.rs` façade + (Diagnosticism-style); +3. Leave API tests in `lib.rs` (no `tests/mod.rs`). + + +## Functional improvements + +* [ ] **no-std**; +* [ ] unit-test macros; + + +## Performance improvements + +* [ ] special cases (for compiled only) such as `"*brown*"` could just be `strstr()`; +* [ ] thorough optimisation review (including optional "unsafe"); + + + diff --git a/benches/cw-regex.rs b/benches/cw-regex.rs index e890801..f4011af 100644 --- a/benches/cw-regex.rs +++ b/benches/cw-regex.rs @@ -2,17 +2,14 @@ #![allow(non_snake_case)] -use shwild; - -use regex::Regex; - use criterion::{ - black_box, criterion_group, criterion_main, Criterion, }; +use regex::Regex; +use std::hint::black_box; mod constants { pub(crate) const S_TQBFJOTLD : &str = "The quick brown fox jumps over the lazy dog"; diff --git a/benches/range_string-creation_functions.rs b/benches/range_string-creation_functions.rs index a366992..a6e5fc0 100644 --- a/benches/range_string-creation_functions.rs +++ b/benches/range_string-creation_functions.rs @@ -1,18 +1,16 @@ // benches/range_string-creation_functions.rs : evaluates performance of different range-string creation-function approaches #![allow(non_snake_case)] -#![feature(ascii_char)] - -use shwild; use criterion::{ - black_box, criterion_group, criterion_main, // BenchmarkId, Criterion, }; +use std::hint::black_box; + mod constants { #![allow(non_upper_case_globals)] @@ -68,7 +66,7 @@ mod utils { chars.dedup(); chars.into_iter().collect() // appears to be (marginally) faster for `Vec<>`, although it's very close - // chars.as_slice().iter().collect() + // chars.as_slice().iter().collect() } pub(super) fn range_string_from_slice_1( @@ -134,7 +132,7 @@ mod utils { chars.dedup(); chars.into_iter().collect() // appears to be (marginally) faster for `Vec<>`, although it's very close - // chars.as_slice().iter().collect() + // chars.as_slice().iter().collect() } pub(super) fn range_string_from_slice_3( @@ -241,37 +239,6 @@ mod utils { // chars.as_slice().iter().collect() } - #[allow(unexpected_cfgs)] - #[cfg(feature = "auto-buffer")] - pub(super) fn range_string_from_slice_6( - chars : &[char], - flags : i64, - ) -> String { - let mut chars = if 0 != (shwild::IGNORE_CASE & flags) { - let mut ci_chars = auto_buffer::AutoBuffer::::with_capacity(chars.len() * 2); - - for (ix, c) in chars.iter().enumerate() { - if c.is_alphabetic() { - ci_chars.push(c.to_ascii_uppercase()); - - ci_chars.push(c.to_ascii_lowercase()); - } else { - ci_chars.push(*c); - } - } - - ci_chars - } else { - chars.into() - }; - - chars.sort_unstable(); - - chars.dedup(); - - chars.into_iter().collect() - } - pub(super) fn range_string_from_slice_7( chars : &[char], flags : i64, diff --git a/benches/shwild-compiled_matcher.rs b/benches/shwild-compiled_matcher.rs index 8674850..bc7da1c 100644 --- a/benches/shwild-compiled_matcher.rs +++ b/benches/shwild-compiled_matcher.rs @@ -2,15 +2,14 @@ #![allow(non_snake_case)] -use shwild; - use criterion::{ - black_box, criterion_group, criterion_main, Criterion, }; +use std::hint::black_box; + mod constants { #![allow(non_upper_case_globals)] diff --git a/benches/shwild-matches.rs b/benches/shwild-matches.rs index e59e051..a2dec17 100644 --- a/benches/shwild-matches.rs +++ b/benches/shwild-matches.rs @@ -2,15 +2,14 @@ #![allow(non_snake_case)] -use shwild; - use criterion::{ - black_box, criterion_group, criterion_main, Criterion, }; +use std::hint::black_box; + mod constants { #![allow(non_upper_case_globals)] diff --git a/examples/character-play.md b/examples/character-play.md new file mode 100644 index 0000000..3266700 --- /dev/null +++ b/examples/character-play.md @@ -0,0 +1,99 @@ +# shwild.Rust Example - **character-play** + +## Summary + +A scratch program that exercises **regex** crate matching behaviour for Unicode text, including emoji and combining characters. It is intended to support development of the `"test-regex"` feature and the `regex_comparision_tests` unit tests; it does not call **shwild** APIs directly. + + +## Source + +```Rust +// test/scratch/character-play/main.rs : Unicode matching experiments with **regex** + +use regex::Regex; + +fn main() { + { + let re = Regex::new("abc").unwrap(); + + assert!(re.is_match("abc")); + assert!(re.is_match("abcd")); + } + + { + let re = Regex::new("abc$").unwrap(); + + assert!(re.is_match("abc")); + assert!(!re.is_match("abcd")); + } + + { + let re = Regex::new("a🐻c").unwrap(); + + assert!(re.is_match("a🐻c")); + assert!(!re.is_match("abc")); + assert!(re.is_match("a🐻cd")); + } + + { + let re = Regex::new("aéc").unwrap(); + + assert!(re.is_match("aéc")); + assert!(!re.is_match("abc")); + assert!(re.is_match("aécd")); + } + + { + let re = Regex::new("aéc").unwrap(); + + assert!(re.is_match("aéc")); + assert!(!re.is_match("abc")); + assert!(re.is_match("aécd")); + } + + { + let re = Regex::new("a[🐻👀🛑]c").unwrap(); + + assert!(re.is_match("a🐻c")); + assert!(re.is_match("a👀c")); + assert!(re.is_match("a🛑c")); + assert!(!re.is_match("abc")); + assert!(re.is_match("a🛑cd")); + } + + { + let re = Regex::new("a👁️c").unwrap(); + + assert!(!re.is_match("a🐻c")); + assert!(!re.is_match("a👀c")); + assert!(!re.is_match("a🛑c")); + assert!(re.is_match("a👁️c")); + assert!(!re.is_match("abc")); + assert!(!re.is_match("a🛑cd")); + } + + { + let re = Regex::new("a[🐻👀🛑]c").unwrap(); + + assert!(re.is_match("a🐻c")); + assert!(re.is_match("a👀c")); + assert!(re.is_match("a🛑c")); + assert!(!re.is_match("abc")); + assert!(re.is_match("a🛑cd")); + } +} +``` + + +## Running and output + +When executed, as in: + +```bash +$ cargo run --example character-play --features test-regex +``` + +it runs to completion with no output on success (all assertions pass). + + + diff --git a/examples/list-matching-files-compiled.md b/examples/list-matching-files-compiled.md new file mode 100644 index 0000000..e426bff --- /dev/null +++ b/examples/list-matching-files-compiled.md @@ -0,0 +1,113 @@ +# shwild.Rust Example - **list-matching-files-compiled** + +## Summary + +An example using **shwild.Rust**'s `CompiledMatcher` to list files in the current directory whose paths match one or more shell wildcard pattern(s) given on the command-line. Each pattern is parsed once up front; this is preferable when the same pattern(s) will be matched repeatedly. When no patterns are specified, `"*"` is assumed. + + +## Source + +```Rust +// examples/list-matching-files-compiled/main.rs : filter files using `CompiledMatcher` + +use std::{ + env as std_env, + fs as std_fs, + process as std_process, +}; + + +fn main() { + let directory = "."; + + let patterns = { + let r = std_env::args().skip(1).collect::>(); + + if r.is_empty() { + vec!["*".into()] + } else { + r + } + }; + let matchers = patterns + .iter() + .map(|pattern| { + shwild::CompiledMatcher::from_pattern_and_flags(&pattern, 0).unwrap_or_else(|e| { + eprintln!("failed to parse pattern '{pattern}': {e}"); + + std_process::exit(1); + }) + }) + .collect::>(); + + println!("searching in '{directory}' with pattern(s) {:?}", patterns); + + match std_fs::read_dir(directory) { + Ok(entries) => { + // for each file in the directory ... + for entry in entries { + match entry { + Ok(entry) => { + let path = entry.path(); + let path_s = format!("{}", path.display()); + + // ... check against ... + for matcher in &matchers { + // ... each pattern ... + if matcher.matches(&path_s) { + // ... and print when it matches any one. + println!("\t{path_s}"); + + break; + } + } + }, + Err(e) => { + eprintln!("failed to read file in '{directory}': {e}"); + }, + }; + } + }, + Err(e) => { + eprintln!("failed to read files in '{directory}': {e}"); + }, + }; +} +``` + + +## Running and output + +When executed, as in: + +```bash +$ cargo run --example list-matching-files-compiled +``` + +it gives output similar to: + +``` +searching in '.' with pattern(s) ["*"] + ./Cargo.toml + ./LICENSE + ./README.md + ... +``` + +With pattern argument(s), as in: + +```bash +$ cargo run --example list-matching-files-compiled -- '*.md' +``` + +it gives output similar to: + +``` +searching in '.' with pattern(s) ["*.md"] + ./README.md + ./CHANGES.md + ./TODO.md +``` + + + diff --git a/examples/list-matching-files-compiled/main.rs b/examples/list-matching-files-compiled/main.rs index 1ea0aa2..edeb487 100644 --- a/examples/list-matching-files-compiled/main.rs +++ b/examples/list-matching-files-compiled/main.rs @@ -23,7 +23,7 @@ fn main() { let matchers = patterns .iter() .map(|pattern| { - shwild::CompiledMatcher::from_pattern_and_flags(&pattern, 0).unwrap_or_else(|e| { + shwild::CompiledMatcher::from_pattern_and_flags(pattern, 0).unwrap_or_else(|e| { eprintln!("failed to parse pattern '{pattern}': {e}"); std_process::exit(1); diff --git a/examples/list-matching-files.md b/examples/list-matching-files.md new file mode 100644 index 0000000..93a84ca --- /dev/null +++ b/examples/list-matching-files.md @@ -0,0 +1,109 @@ +# shwild.Rust Example - **list-matching-files** + +## Summary + +An example using **shwild.Rust**'s `matches()` function to list files in the current directory whose paths match one or more shell wildcard pattern(s) given on the command-line. When no patterns are specified, `"*"` is assumed. + + +## Source + +```Rust +// examples/list-matching-files/main.rs : filter files by name using `matches()` + +use std::{ + env as std_env, + fs as std_fs, +}; + + +fn main() { + let directory = "."; + + let patterns = { + let r = std_env::args().skip(1).collect::>(); + + if r.is_empty() { + vec!["*".into()] + } else { + r + } + }; + + println!("searching in '{directory}' with pattern(s) {patterns:?}"); + + match std_fs::read_dir(directory) { + Ok(entries) => { + // for each file in the directory ... + for entry in entries { + match entry { + Ok(entry) => { + let path = entry.path(); + let path_s = format!("{}", path.display()); + + // ... check against ... + for pattern in &patterns { + // ... each pattern ... + match shwild::matches(pattern, &path_s, 0) { + Ok(is_matched) => { + if is_matched { + // ... and print when it matches any one. + println!("\t{path_s}"); + + break; + } + }, + Err(e) => { + eprintln!("failed to match against '{path_s}': {e}"); + }, + }; + } + }, + Err(e) => { + eprintln!("failed to read file in '{directory}': {e}"); + }, + }; + } + }, + Err(e) => { + eprintln!("failed to read files in '{directory}': {e}"); + }, + }; +} +``` + + +## Running and output + +When executed, as in: + +```bash +$ cargo run --example list-matching-files +``` + +it gives output similar to: + +``` +searching in '.' with pattern(s) ["*"] + ./Cargo.toml + ./LICENSE + ./README.md + ... +``` + +With pattern argument(s), as in: + +```bash +$ cargo run --example list-matching-files -- '*.md' +``` + +it gives output similar to: + +``` +searching in '.' with pattern(s) ["*.md"] + ./README.md + ./CHANGES.md + ./TODO.md +``` + + + diff --git a/.rustfmt.toml b/rustfmt.toml similarity index 92% rename from .rustfmt.toml rename to rustfmt.toml index 41189d4..c08d358 100644 --- a/.rustfmt.toml +++ b/rustfmt.toml @@ -1,22 +1,24 @@ # rustfmt.toml for shwild.Rust # +# Requires nightly rustfmt with unstable features enabled — use ./scripts/fmt +# # configured for cargo-fmt 1.84.0 # array_width=60 # deprecated # attr_fn_like_width=70 # deprecated binop_separator="Front" blank_lines_lower_bound=0 -blank_lines_upper_bound=2 +blank_lines_upper_bound=3 brace_style="SameLineWhere" # chain_width=60 # deprecated color="Auto" combine_control_expr=false -comment_width=100 +comment_width=76 condense_wildcard_suffixes=false control_brace_style="AlwaysSameLine" disable_all_formatting=false -edition="2018" +edition="2021" empty_item_single_line=false enum_discrim_align_threshold=0 error_on_line_overflow=false @@ -75,7 +77,7 @@ tab_spaces=4 # Q: do we want to move to 2?? trailing_comma="Vertical" trailing_semicolon=true type_punctuation_density="Wide" -# unstable_features=false +unstable_features=true use_field_init_shorthand=true use_small_heuristics="Default" use_try_shorthand=true diff --git a/scripts/check_derives.py b/scripts/check_derives.py new file mode 100755 index 0000000..5c595ec --- /dev/null +++ b/scripts/check_derives.py @@ -0,0 +1,129 @@ +#! /usr/bin/env python3 +""" +Verify DERIVE_LAYOUT: multi-trait `#[derive(...)]` macros must be split +into separate single-trait lines, ordered alphabetically by trait name, +except tightly coupled groups (Eq/PartialEq, Ord/PartialOrd). +""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path + + +COUPLED_TRAIT_GROUPS = [ + ["Eq", "PartialEq"], + ["Ord", "PartialOrd"], +] + +PASS = "\N{WHITE HEAVY CHECK MARK}" # ✅ +FAIL = "\N{CROSS MARK}" # ❌ + + +def lint_file(filepath: Path) -> list[str]: + errors: list[str] = [] + + lines = filepath.read_text(encoding="utf-8").splitlines() + i = 0 + + while i < len(lines): + line = lines[i] + + if not re.match(r"^\s*#\[derive\(", line): + i += 1 + continue + + derive_block: list[tuple[int, str]] = [] + start_line_num = i + 1 + + while i < len(lines) and re.match(r"^\s*#\[derive\(", lines[i]): + derive_block.append((i + 1, lines[i])) + i += 1 + + parsed_lines: list[tuple[int, str, str]] = [] + block_has_error = False + + for line_num, line_str in derive_block: + match = re.search(r"#\[derive\((.*?)\)\]", line_str) + + if not match: + continue + + traits = [ + t.strip() + for t in match.group(1).split(",") + if t.strip() + ] + + if len(traits) > 1: + if traits not in COUPLED_TRAIT_GROUPS: + block_has_error = True + allowed = ", ".join( + f"'{', '.join(group)}'" + for group in COUPLED_TRAIT_GROUPS + ) + errors.append( + f"{filepath}:{line_num}: multi-trait derive " + f"'{line_str.strip()}' is not allowed " + f"(except coupled groups: {allowed})", + ) + elif len(traits) == 0: + block_has_error = True + errors.append( + f"{filepath}:{line_num}: empty derive attribute " + f"'{line_str.strip()}'", + ) + + sort_key = traits[0] if traits else "" + parsed_lines.append((line_num, line_str, sort_key)) + + if not block_has_error and len(parsed_lines) > 1: + sort_keys = [item[2] for item in parsed_lines] + + if sort_keys != sorted(sort_keys): + actual = [item[1].strip() for item in parsed_lines] + expected = [ + item[1].strip() + for item in sorted(parsed_lines, key=lambda x: x[2]) + ] + errors.append( + f"{filepath}:{start_line_num}: derive attributes not " + f"sorted alphabetically\n" + f" actual: {actual}\n" + f" expected: {expected}", + ) + + return errors + + +def main() -> int: + root = Path(__file__).resolve().parents[1] + errors: list[str] = [] + + for directory in ("src", "examples", "benches", "test"): + base = root / directory + + if not base.is_dir(): + continue + + for path in sorted(base.rglob("*.rs")): + if "target" in path.parts: + continue + + errors.extend(lint_file(path)) + + if errors: + print( + f"{FAIL} DERIVE_LAYOUT violations:", + file=sys.stderr, + ) + print("\n".join(f" {FAIL} {error}" for error in errors), file=sys.stderr) + return 1 + + print(f"{PASS} DERIVE_LAYOUT: ok") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/check_doc_76.py b/scripts/check_doc_76.py new file mode 100644 index 0000000..2a10562 --- /dev/null +++ b/scripts/check_doc_76.py @@ -0,0 +1,66 @@ +#! /usr/bin/env python3 +""" +Verify DOC_76: public documentation comment lines are at most 76 characters. + +Code blocks inside doc comments (``` ... ```) are exempt, matching Synesis +Information Systems' internal project standards. +""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path + + +DOC_LINE = re.compile(r"^\s*(//!|///)") + +PASS = "\N{WHITE HEAVY CHECK MARK}" # ✅ +FAIL = "\N{CROSS MARK}" # ❌ + + +def iter_doc_violations(path: Path) -> list[str]: + violations: list[str] = [] + in_codeblock = False + + for line_no, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): + stripped = line.rstrip() + + if DOC_LINE.match(stripped) and re.search(r"\s*```\s*$", stripped): + in_codeblock = not in_codeblock + continue + + if in_codeblock or not DOC_LINE.match(stripped): + continue + + if len(stripped) > 76: + violations.append( + f"{path}:{line_no} ({len(stripped)} chars): {stripped}" + ) + + return violations + + +def main() -> int: + root = Path(__file__).resolve().parents[1] + errors: list[str] = [] + + for path in sorted(root.rglob("*.rs")): + if "target" in path.parts: + continue + errors.extend(iter_doc_violations(path)) + + if errors: + print( + f"{FAIL} DOC_76 violations (doc comment lines must be <= 76 characters):", + file=sys.stderr, + ) + print("\n".join(f" {FAIL} {error}" for error in errors), file=sys.stderr) + return 1 + + print(f"{PASS} DOC_76: ok") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/check_test_names.py b/scripts/check_test_names.py new file mode 100644 index 0000000..f2084a9 --- /dev/null +++ b/scripts/check_test_names.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python3 +""" +Verify RUST_TEST_NAMING: test functions and test modules use TEST_ prefix +and SHOUTING_SNAKE_CASE, except words that name a specific Rust construct +(type, function, macro, field, etc.) which must preserve exact case. + +When a construct name is embedded as a SHOUTING_SNAKE_CASE constant (or +PascalCase construct), it may be delimited with an extra underscore on +each side — e.g. HAVING__IGNORE_CASE__1. +""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path + + +TEST_ATTR = re.compile(r"^\s*#\[(\w+::)?test(\(\))?\]") +FN_DEF = re.compile(r"^\s*fn\s+(\w+)") +MOD_DEF = re.compile(r"^\s*mod\s+(\w+)") +SNAKE_PART = re.compile(r"^[a-z][a-z0-9]*$") +CONSECUTIVE_UPPER = re.compile(r"[A-Z]{2,}") + +PASS = "\N{WHITE HEAVY CHECK MARK}" # ✅ +FAIL = "\N{CROSS MARK}" # ❌ + + +def is_pascal_case_atom(atom: str) -> bool: + return ( + atom[0].isupper() + and any(c.islower() for c in atom) + and not CONSECUTIVE_UPPER.search(atom) + and atom.isalnum() + ) + + +def atom_violation(atom: str) -> str | None: + if atom.isupper() or atom.isdigit(): + return None + + if atom[0].islower() and all(SNAKE_PART.match(part) for part in atom.split("_")): + return None + + if is_pascal_case_atom(atom): + return None + + return ( + f"segment '{atom}' must be SHOUTING_SNAKE_CASE, a PascalCase construct name, " + "or a Rust snake_case identifier" + ) + + +def parse_padded_construct( + segments: list[str], start: int +) -> tuple[str | None, int, list[str]]: + """Parse __CONSTRUCT__ padding around a shouting or PascalCase atom.""" + violations: list[str] = [] + i = start + while i < len(segments) and not segments[i]: + i += 1 + if i >= len(segments): + return None, i, [f"empty segment padding without construct"] + + seg = segments[i] + atom: str | None = None + + if seg.isupper(): + parts = [seg] + i += 1 + while i < len(segments) and segments[i] and segments[i].isupper(): + parts.append(segments[i]) + i += 1 + atom = "_".join(parts) + reason = atom_violation(atom) + if reason: + violations.append(reason) + elif seg[0].isupper() and is_pascal_case_atom(seg): + atom = seg + reason = atom_violation(atom) + if reason: + violations.append(reason) + i += 1 + else: + return None, start, [f"empty segment padding without construct"] + + while i < len(segments) and not segments[i]: + i += 1 + + return atom, i, violations + + +def parse_name_atoms(rest: str) -> tuple[list[str], list[str]]: + """Split a test name body into atoms; return (atoms, violations).""" + atoms: list[str] = [] + violations: list[str] = [] + segments = rest.split("_") + i = 0 + + while i < len(segments): + seg = segments[i] + if not seg: + atom, i, viols = parse_padded_construct(segments, i) + violations.extend(viols) + if atom: + atoms.append(atom) + elif not viols: + violations.append(f"empty segment in '{rest}'") + continue + + if seg.isupper() or seg.isdigit(): + reason = atom_violation(seg) + if reason: + violations.append(reason) + else: + atoms.append(seg) + i += 1 + continue + + if seg[0].isupper(): + reason = atom_violation(seg) + if reason: + violations.append(reason) + else: + atoms.append(seg) + i += 1 + continue + + if SNAKE_PART.match(seg): + parts = [seg] + i += 1 + while i < len(segments) and SNAKE_PART.match(segments[i]): + parts.append(segments[i]) + i += 1 + atom = "_".join(parts) + reason = atom_violation(atom) + if reason: + violations.append(reason) + else: + atoms.append(atom) + continue + + violations.append( + f"segment '{seg}' must be SHOUTING_SNAKE_CASE, a PascalCase construct name, " + "or a Rust snake_case identifier" + ) + i += 1 + + return atoms, violations + + +def iter_name_violations(name: str) -> list[str]: + if not name.startswith("TEST_"): + return ["must start with 'TEST_'"] + + rest = name[len("TEST_") :] + if not rest: + return ["must have a name after 'TEST_'"] + + _, violations = parse_name_atoms(rest) + return violations + + +def iter_test_results(path: Path, root: Path) -> list[tuple[bool, str]]: + results: list[tuple[bool, str]] = [] + pending_test = False + display = path.relative_to(root) + + for line_no, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): + stripped = line.rstrip() + + if TEST_ATTR.match(stripped): + pending_test = True + continue + + if pending_test and stripped.startswith("#["): + continue + + fn_match = FN_DEF.match(stripped) + if fn_match: + name = fn_match.group(1) + if pending_test: + pending_test = False + reasons = iter_name_violations(name) + label = f"{display}:{line_no}: test function '{name}'" + if reasons: + for reason in reasons: + results.append((False, f"{label}: {reason}")) + else: + results.append((True, label)) + continue + + mod_match = MOD_DEF.match(stripped) + if mod_match: + pending_test = False + name = mod_match.group(1) + if name.startswith("TEST_"): + reasons = iter_name_violations(name) + label = f"{display}:{line_no}: test module '{name}'" + if reasons: + for reason in reasons: + results.append((False, f"{label}: {reason}")) + else: + results.append((True, label)) + continue + + if stripped and not stripped.startswith("#") and stripped.endswith("{"): + pending_test = False + + return results + + +def main() -> int: + root = Path(__file__).resolve().parents[1] + results: list[tuple[bool, str]] = [] + + for path in sorted(root.rglob("*.rs")): + if "target" in path.parts: + continue + results.extend(iter_test_results(path, root)) + + failures = [line for ok, line in results if not ok] + if failures: + print( + f"{FAIL} RUST_TEST_NAMING violations " + "(test functions and modules must use TEST_ + SHOUTING_SNAKE_CASE):", + file=sys.stderr, + ) + for ok, line in results: + mark = PASS if ok else FAIL + print(f" {mark} {line}", file=sys.stderr) + return 1 + + print(f"{PASS} RUST_TEST_NAMING: ok") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/fmt b/scripts/fmt new file mode 100755 index 0000000..13731a1 --- /dev/null +++ b/scripts/fmt @@ -0,0 +1,12 @@ +#! /usr/bin/env bash +set -euo pipefail + +RUSTFMT="$(rustup which --toolchain nightly rustfmt 2>/dev/null || true)" +if [[ -z "${RUSTFMT}" ]]; then + echo "error: nightly rustfmt is required (see rustfmt.toml)" >&2 + echo " rustup component add rustfmt --toolchain nightly" >&2 + exit 1 +fi + +export RUSTFMT +exec cargo fmt -- --unstable-features "$@" diff --git a/src/lib.rs b/src/lib.rs index c09760c..bdc1ce3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,51 @@ +//! Shell-compatible wildcard matching for Rust — part of the cross-language +//! **shwild** family. +//! +//! **shwild** provides shell-style pattern matching: literals, `?` (any one +//! character), `*` (any number of characters), `[`…`]` ranges, `[^`…`]` +//! not-ranges, escapes, and case flags. **shwild.Rust** is a port with +//! minimal API differences from the original **C/C++** library; the design +//! emphasis is simplicity-of-use, modularity, and performance. +//! +//! (See [**shwild.Go**][sg] for the Go implementation.) +//! +//! # Installation +//! +//! Reference in **Cargo.toml** in the usual way: +//! +//! ```toml +//! shwild = { version = "0.1" } +//! ``` +//! +//! # Components +//! +//! * [`matches()`] — parse `pattern` and test `input` in one step; +//! * [`shwild_matches!`] — shorthand for [`matches()`] (2- or 3-arg); +//! * [`CompiledMatcher`] — parse once, match many times; +//! * [`Error`] and [`Result`] — parse/match error reporting; +//! * [`IGNORE_CASE`] — flag for case-insensitive matching; +//! +//! # Features +//! +//! * `lookup-ranges` (default) — range matching via **collect-rs** +//! `UnicodePointMap`; +//! * `null-feature` — no effect; useful for driver scripts; +//! * `test-regex` — optional **regex** dependency for benchmarks and +//! scratch programs; +//! +//! # Examples +//! +//! ``` +//! use shwild::shwild_matches; +//! +//! assert_eq!(Ok(true), shwild_matches!("*.rs", "lib.rs")); +//! ``` +//! +//! Further examples are in the repository **examples** directory and in +//! the project [README](https://github.com/synesissoftware/shwild.Rust). +//! +//! [sg]: https://github.com/synesissoftware/shwild.Go + // src/lib.rs : Definition of the shwild Rust package // /////////////////////////////////////////////// @@ -102,7 +150,6 @@ mod traits { /// # Returns: /// - `true` - indicates a full match; or /// - `false` - if not a full match. - fn matches( &self, slice : &str, @@ -301,11 +348,7 @@ mod match_structures { let slice_starts_with_literal = slice.starts_with(&self.literal) || match &self.literal_uppercase { Some(literal_uppercase) => { - if slice.len() >= literal_uppercase.len() { - slice.to_uppercase().starts_with(literal_uppercase) - } else { - false - } + slice.len() >= literal_uppercase.len() && slice.to_uppercase().starts_with(literal_uppercase) }, None => false, }; @@ -427,13 +470,12 @@ mod match_structures { mod tests { #![allow(non_snake_case)] + #[cfg(not(feature = "lookup-ranges"))] + use super::super::utils::prepare_range_string; #[cfg(feature = "lookup-ranges")] use super::super::utils::prepare_range_upm_from_slice; use super::{ - super::{ - traits::Match, - utils::prepare_range_string, - }, + super::traits::Match, MatchEnd, MatchLiteral, MatchNotRange, @@ -1385,7 +1427,10 @@ pub type Result = std_result::Result; /// # Examples: /// /// ``` -/// let matcher = shwild::CompiledMatcher::from_pattern_and_flags("a[bc]c?", shwild::IGNORE_CASE).unwrap(); +/// let matcher = shwild::CompiledMatcher::from_pattern_and_flags( +/// "a[bc]c?", +/// shwild::IGNORE_CASE, +/// ).unwrap(); /// /// assert!(matcher.matches("abcd")); /// assert!(matcher.matches("accd")); @@ -3156,10 +3201,10 @@ mod tests { assert!(!matcher.matches("")); assert!(!matcher.matches("Where are the bears?")); - assert_eq!(true, matcher.matches("Where are the 🐻s?")); - assert_eq!(true, matcher.matches("Where are the 🐼s?")); - assert_eq!(true, matcher.matches("Where are their 🐻s?")); - assert_eq!(true, matcher.matches("Where are the big brown 🐻s?")); + assert!(matcher.matches("Where are the 🐻s?")); + assert!(matcher.matches("Where are the 🐼s?")); + assert!(matcher.matches("Where are their 🐻s?")); + assert!(matcher.matches("Where are the big brown 🐻s?")); assert!(!matcher.matches("Where are the teddy-🐻s?")); } } From 6cfac9734088ddd65f1a472f2a88794dd59235d4 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Tue, 7 Jul 2026 19:58:20 +1000 Subject: [PATCH 04/16] wording --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index bdc1ce3..52b5f80 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1969,7 +1969,7 @@ macro_rules! assert_shwild_not_matches { panic!("could not evaluate actual value due to a failure in the parsing of expected pattern '{}': {}", expected_pattern, e); }, Ok(b) => { - assert!(!b, "assertion failed: actual value '{}' match unexpectedly with the pattern '{}'", actual, expected_pattern); + assert!(!b, "assertion failed: actual value '{}' matches unexpectedly with the pattern '{}'", actual, expected_pattern); } } }; From 5c90cda2136630d9a3a8106c134069a46fa43f00 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Tue, 7 Jul 2026 20:04:12 +1000 Subject: [PATCH 05/16] boilerplate --- CHANGES.md | 8 ++++++++ Cargo.toml | 7 ++----- NEWS.md | 3 ++- README.md | 16 ++++++++++++++-- TODO.md | 3 ++- src/lib.rs | 35 ++++++++++++++++++++++++++++++++++- 6 files changed, 62 insertions(+), 10 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index d4484d1..e11219c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,14 @@ # shwild.Rust - CHANGES +## 0.2.0 - 8th July 2026 + +* added `assert_shwild_matches!()` and `assert_shwild_not_matches!()` test assertion macros; +* added required dependency on [**base-traits**](https://github.com/synesissoftware/base-traits) (minimal features: `implement-AsI64-for-built_ins`); +* crate-level and macro `///` documentation for the assertion macros; +* **README.md** macros and dependencies sections updated; + + ## 0.1.4 - 8th July 2026 * added **CHANGES.md** (back-filled) and **NEWS.md**; diff --git a/Cargo.toml b/Cargo.toml index dad0525..ccfa1e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -112,11 +112,8 @@ test-regex = [ [dependencies] -base-traits = "*" - -# base-traits = { version = "0", default-features = false, features = [ -# "implement-ASI64-for-built_ins", -# ]} +base-traits = { version = "0", default-features = false, features = [ +]} collect-rs = { version = "0.2", optional = true, default-features = false, features = [ ]} regex = { version = "1.11", optional = true, default-features = false, features = [ diff --git a/NEWS.md b/NEWS.md index 6d66629..a5a2a54 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,7 +2,8 @@ | Date | News Item | | --------------------- | ----------------------------------------- | -| 8th July 2026 | shwild.Rust 0.1.4 released | +| 8th July 2026 | shwild.Rust 0.2.0 released | +| 7th July 2026 | shwild.Rust 0.1.4 released | | 28th March 2025 | shwild.Rust 0.1.3 released | | 3rd November 2024 | shwild.Rust 0.1.2 released | | 3rd November 2024 | shwild.Rust 0.1.1 released | diff --git a/README.md b/README.md index cdd359e..c67e7f5 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ The library (and other **shwild** variants) support the following pattern elemen Reference in **Cargo.toml** in the usual way: ```toml -shwild = { version = "0.1" } +shwild = { version = "0.2" } ``` @@ -133,6 +133,16 @@ pub mod shwild { The `shwild::shwild_matches!()` macro is a shorthand for the `shwild::matches()` function, providing 2-parameter and 3-parameter forms. The 2-parameter form passes 0 for the `flags` parameter. +The `shwild::assert_shwild_matches!()` and `shwild::assert_shwild_not_matches!()` macros are test-oriented counterparts that panic on failure. Each provides 2-parameter and 3-parameter forms; the 2-parameter form passes 0 for the `flags` parameter. A parse error in the pattern panics with a descriptive message rather than returning `Err`. + +```Rust + use shwild::{assert_shwild_matches, assert_shwild_not_matches, IGNORE_CASE}; + + assert_shwild_matches!("[a-d]", "b"); + assert_shwild_not_matches!("[a-d]", "e"); + assert_shwild_matches!("[a-d]", "B", IGNORE_CASE); +``` + ### Structures @@ -190,8 +200,9 @@ Defect reports, feature requests, and pull requests are welcome on https://githu ### Dependencies -**shwild.Rust** has two dependencies, both optional: +**shwild.Rust** has one required dependency and two optional dependencies: +* [**base-traits**](https://github.com/synesissoftware/base-traits) - required; supports the `flags` parameter type in `assert_shwild_matches!()` and `assert_shwild_not_matches!()` via `AsI64`; * [**collect-rs**](https://github.com/synesissoftware/collect-rs) - required, for more efficient range matching, if feature `"lookup-ranges"` is specified; * [**regex**](https://github.com/rust-lang/regex) - required, by some benchmark/example programs only, if feature `"test-regex"` is specified; @@ -208,6 +219,7 @@ Crates upon which **shwild** has development dependencies: * [**shwild**](https://github.com/synesissoftware/shwild/); * [**shwild.Go**](https://github.com/synesissoftware/shwild.Go/); +* [**base-traits**](https://github.com/synesissoftware/base-traits/); * [**collect-rs**](https://github.com/synesissoftware/collect-rs/); diff --git a/TODO.md b/TODO.md index 90c33f3..ab1788e 100644 --- a/TODO.md +++ b/TODO.md @@ -68,7 +68,8 @@ Proposed module layout: ``` src/ - lib.rs # crate docs, re-exports, matches(), shwild_matches! macro; + lib.rs # crate docs, re-exports, matches(), shwild_matches!, + # assert_shwild_matches!/assert_shwild_not_matches!; # #[cfg(test)] API unit tests (stay here) constants.rs # IGNORE_CASE error.rs # Error + Display/Error trait impls diff --git a/src/lib.rs b/src/lib.rs index 52b5f80..676ea0f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,13 +14,16 @@ //! Reference in **Cargo.toml** in the usual way: //! //! ```toml -//! shwild = { version = "0.1" } +//! shwild = { version = "0.2" } //! ``` //! //! # Components //! //! * [`matches()`] — parse `pattern` and test `input` in one step; //! * [`shwild_matches!`] — shorthand for [`matches()`] (2- or 3-arg); +//! * [`assert_shwild_matches!`] — test assertion that `actual` matches; +//! * [`assert_shwild_not_matches!`] — test assertion that `actual` does +//! not match; //! * [`CompiledMatcher`] — parse once, match many times; //! * [`Error`] and [`Result`] — parse/match error reporting; //! * [`IGNORE_CASE`] — flag for case-insensitive matching; @@ -41,6 +44,16 @@ //! assert_eq!(Ok(true), shwild_matches!("*.rs", "lib.rs")); //! ``` //! +//! In unit tests, prefer [`assert_shwild_matches!`] and +//! [`assert_shwild_not_matches!`]: +//! +//! ``` +//! use shwild::{assert_shwild_matches, assert_shwild_not_matches}; +//! +//! assert_shwild_matches!("[a-d]", "b"); +//! assert_shwild_not_matches!("[a-d]", "e"); +//! ``` +//! //! Further examples are in the repository **examples** directory and in //! the project [README](https://github.com/synesissoftware/shwild.Rust). //! @@ -1929,6 +1942,16 @@ macro_rules! shwild_matches { }; } +/// Defines the macro `assert_shwild_matches!()`. +/// +/// # Parameters: +/// - `$expected_pattern` - the pattern against which `$actual` is +/// evaluated; +/// - `$actual` - the string to be evaluated; +/// - `$flags` - flags that moderate the evaluation; +/// +/// Panics if `$expected_pattern` is invalid or if `$actual` does not match. +/// The 2-parameter form passes 0 for the `flags` parameter. #[macro_export] macro_rules! assert_shwild_matches { ($expected_pattern:expr, $actual:expr) => { @@ -1952,6 +1975,16 @@ macro_rules! assert_shwild_matches { }; } +/// Defines the macro `assert_shwild_not_matches!()`. +/// +/// # Parameters: +/// - `$expected_pattern` - the pattern against which `$actual` is +/// evaluated; +/// - `$actual` - the string to be evaluated; +/// - `$flags` - flags that moderate the evaluation; +/// +/// Panics if `$expected_pattern` is invalid or if `$actual` matches. The +/// 2-parameter form passes 0 for the `flags` parameter. #[macro_export] macro_rules! assert_shwild_not_matches { ($expected_pattern:expr, $actual:expr) => { From 0534ed932ee1fcc1c7269039032fddf9a63a302c Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Tue, 7 Jul 2026 20:15:11 +1000 Subject: [PATCH 06/16] tidying --- Cargo.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index ccfa1e0..3d390ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -97,6 +97,8 @@ null-feature = [] # Crate-specific features: # +# - "lookup-ranges" - enable lookup ranges; +# - "test-regex" - enable test regex; lookup-ranges = [ "dep:collect-rs", From f1db7899171d8b1270206edc759fc9ebb4b9f5db Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Tue, 7 Jul 2026 20:17:32 +1000 Subject: [PATCH 07/16] tidying --- benches/range_string-creation_functions.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/benches/range_string-creation_functions.rs b/benches/range_string-creation_functions.rs index a6e5fc0..bffe66b 100644 --- a/benches/range_string-creation_functions.rs +++ b/benches/range_string-creation_functions.rs @@ -66,7 +66,6 @@ mod utils { chars.dedup(); chars.into_iter().collect() // appears to be (marginally) faster for `Vec<>`, although it's very close - // chars.as_slice().iter().collect() } pub(super) fn range_string_from_slice_1( @@ -132,7 +131,6 @@ mod utils { chars.dedup(); chars.into_iter().collect() // appears to be (marginally) faster for `Vec<>`, although it's very close - // chars.as_slice().iter().collect() } pub(super) fn range_string_from_slice_3( @@ -164,7 +162,6 @@ mod utils { chars.dedup(); chars.into_iter().collect() - // chars.as_slice().iter().collect() } pub(super) fn range_string_from_slice_4( @@ -207,7 +204,6 @@ mod utils { chars.dedup(); chars.into_iter().collect() - // chars.as_slice().iter().collect() } pub(super) fn range_string_from_slice_5( @@ -236,7 +232,6 @@ mod utils { chars.dedup(); chars.into_iter().collect() - // chars.as_slice().iter().collect() } pub(super) fn range_string_from_slice_7( From 49b54c93fd27f0dd765548ae53ff42a7b6e64ebe Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Wed, 8 Jul 2026 07:08:32 +1000 Subject: [PATCH 08/16] ci --- .github/workflows/ci.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5084a8c..73a2a0f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,15 @@ jobs: - name: cargo test run: cargo test --locked + - name: cargo test (no default features) + run: cargo test --no-default-features --locked + + - name: cargo test ("lookup-ranges") + run: cargo test --no-default-features --features lookup-ranges --locked + + - name: cargo test ("test-regex") + run: cargo test --no-default-features --features test-regex --locked + - name: cargo clippy run: cargo clippy --all-targets --locked -- -D warnings From 7c7a0fb0f7f4f8519a19c72b99d5b5114987f3a9 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Wed, 8 Jul 2026 07:10:00 +1000 Subject: [PATCH 09/16] clippy --- src/lib.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 676ea0f..f9d6f6a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2014,9 +2014,8 @@ mod tests { #![allow(non_snake_case)] use crate::{ - assert_shwild_matches, self as shwild, - shwild_matches, + constants::*, }; @@ -2025,8 +2024,6 @@ mod tests { use super::*; - use crate::constants::*; - #[test] fn TEST_CompiledMatcher_parse_EMPTY() { @@ -3440,7 +3437,6 @@ mod tests { mod TEST_ASSERTION_MATCHES { #![allow(non_snake_case)] - use super::*; #[test] From 9b62e9b0302b022c363f4d6a95c3c0f6b46a1713 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Wed, 8 Jul 2026 07:16:25 +1000 Subject: [PATCH 10/16] feature: added and applied `"assertions"` feature --- .github/workflows/ci.yml | 3 + CHANGES.md | 12 ++-- Cargo.toml | 9 ++- README.md | 15 +++-- src/lib.rs | 122 +++++++++++++++++++++++++++++++++++---- 5 files changed, 137 insertions(+), 24 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 73a2a0f..26ed2a1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,6 +33,9 @@ jobs: - name: cargo test (no default features) run: cargo test --no-default-features --locked + - name: cargo test ("assertions") + run: cargo test --no-default-features --features assertions --locked + - name: cargo test ("lookup-ranges") run: cargo test --no-default-features --features lookup-ranges --locked diff --git a/CHANGES.md b/CHANGES.md index e11219c..47d9fe3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,8 +3,8 @@ ## 0.2.0 - 8th July 2026 -* added `assert_shwild_matches!()` and `assert_shwild_not_matches!()` test assertion macros; -* added required dependency on [**base-traits**](https://github.com/synesissoftware/base-traits) (minimal features: `implement-AsI64-for-built_ins`); +* added `assert_shwild_matches!()` and `assert_shwild_not_matches!()` test assertion macros, available with the `"assertions"` feature (enabled by default); +* added optional dependency on [**base-traits**](https://github.com/synesissoftware/base-traits) (via `"assertions"`; minimal features: `implement-AsI64-for-built_ins`); * crate-level and macro `///` documentation for the assertion macros; * **README.md** macros and dependencies sections updated; @@ -34,16 +34,16 @@ ## 0.1.2 - 3rd November 2024 -* added `test-regex` feature — optional **regex** dependency for benchmarks and scratch programs; -* enabled `lookup-ranges` in default features; +* added `"test-regex"` feature — optional **regex** dependency for benchmarks and scratch programs; +* enabled `"lookup-ranges"` in default features; * added **cw-regex** benchmark; * added **character-play** scratch program; -* added `regex_comparision_tests` unit tests (gated on `test-regex`); +* added `regex_comparision_tests` unit tests (gated on `"test-regex"`); ## 0.1.1 - 3rd November 2024 -* added `lookup-ranges` feature — optional **collect-rs** dependency for `UnicodePointMap`-based range matching; +* added `"lookup-ranges"` feature — optional **collect-rs** dependency for `UnicodePointMap`-based range matching; * added **Cargo.lock**; * README: added Features section; clarified Wild-1 escape behaviour in pattern elements; diff --git a/Cargo.toml b/Cargo.toml index 3d390ef..32f164a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -83,6 +83,7 @@ path = "examples/list-matching-files-compiled/main.rs" [features] default = [ + "assertions", "lookup-ranges", ] @@ -97,9 +98,14 @@ null-feature = [] # Crate-specific features: # +# - "assertions" - enable assertions; # - "lookup-ranges" - enable lookup ranges; # - "test-regex" - enable test regex; +assertions = [ + "dep:base-traits", +] + lookup-ranges = [ "dep:collect-rs", ] @@ -114,7 +120,8 @@ test-regex = [ [dependencies] -base-traits = { version = "0", default-features = false, features = [ +base-traits = { version = "0", optional = true, default-features = false, features = [ + "implement-AsI64-for-built_ins", ]} collect-rs = { version = "0.2", optional = true, default-features = false, features = [ ]} diff --git a/README.md b/README.md index c67e7f5..49bc488 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,7 @@ The following crate features are defined: | Name | Effect | Is `"default"`? | Dependent feature(s) | | --------------------------- | ------------------------------------- | --------------- | ------------------------------------- | +| `"assertions"` | Provides `assert_shwild_matches!()` and `assert_shwild_not_matches!()` test assertion macros (via **base-traits** `AsI64`) | Yes | | | `"lookup-ranges"` | Causes match/non-match ranges to be implemented in terms of `UnicodePointMap` (from **collect-rs** crate), resulting in significant performance improvements in parsing and matching | Yes | | | `"null-feature"` | A feature that has no effect (and, thus, is useful for simplifying driver scripts) | **No** | | | `"test-regex"` | Introduces a dependency to **regex** crate to support benchmark/example program(s) | **No** | | @@ -133,10 +134,14 @@ pub mod shwild { The `shwild::shwild_matches!()` macro is a shorthand for the `shwild::matches()` function, providing 2-parameter and 3-parameter forms. The 2-parameter form passes 0 for the `flags` parameter. -The `shwild::assert_shwild_matches!()` and `shwild::assert_shwild_not_matches!()` macros are test-oriented counterparts that panic on failure. Each provides 2-parameter and 3-parameter forms; the 2-parameter form passes 0 for the `flags` parameter. A parse error in the pattern panics with a descriptive message rather than returning `Err`. +The `shwild::assert_shwild_matches!()` and `shwild::assert_shwild_not_matches!()` macros are test-oriented counterparts that panic on failure. Each provides 2-parameter and 3-parameter forms; the 2-parameter form passes 0 for the `flags` parameter. A parse error in the pattern panics with a descriptive message rather than returning `Err`. They are provided only when the feature `"assertions"` is enabled, which it is by default. ```Rust - use shwild::{assert_shwild_matches, assert_shwild_not_matches, IGNORE_CASE}; + use shwild::{ + assert_shwild_matches, + assert_shwild_not_matches, + IGNORE_CASE, + }; assert_shwild_matches!("[a-d]", "b"); assert_shwild_not_matches!("[a-d]", "e"); @@ -200,10 +205,10 @@ Defect reports, feature requests, and pull requests are welcome on https://githu ### Dependencies -**shwild.Rust** has one required dependency and two optional dependencies: +**shwild.Rust** has three optional dependencies: -* [**base-traits**](https://github.com/synesissoftware/base-traits) - required; supports the `flags` parameter type in `assert_shwild_matches!()` and `assert_shwild_not_matches!()` via `AsI64`; -* [**collect-rs**](https://github.com/synesissoftware/collect-rs) - required, for more efficient range matching, if feature `"lookup-ranges"` is specified; +* [**base-traits**](https://github.com/synesissoftware/base-traits) - required if feature `"assertions"` is specified; supports the `flags` parameter type in `assert_shwild_matches!()` and `assert_shwild_not_matches!()` via `AsI64`; +* [**collect-rs**](https://github.com/synesissoftware/collect-rs) - required if feature `"lookup-ranges"` is specified, for more efficient range matching; * [**regex**](https://github.com/rust-lang/regex) - required, by some benchmark/example programs only, if feature `"test-regex"` is specified; diff --git a/src/lib.rs b/src/lib.rs index f9d6f6a..d7b18ec 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,15 +21,18 @@ //! //! * [`matches()`] — parse `pattern` and test `input` in one step; //! * [`shwild_matches!`] — shorthand for [`matches()`] (2- or 3-arg); -//! * [`assert_shwild_matches!`] — test assertion that `actual` matches; +//! * [`assert_shwild_matches!`] — test assertion that `actual` matches +//! (requires `"assertions"` feature); //! * [`assert_shwild_not_matches!`] — test assertion that `actual` does -//! not match; +//! not match (requires `"assertions"` feature); //! * [`CompiledMatcher`] — parse once, match many times; //! * [`Error`] and [`Result`] — parse/match error reporting; //! * [`IGNORE_CASE`] — flag for case-insensitive matching; //! //! # Features //! +//! * `assertions` (default) — [`assert_shwild_matches!`] and +//! [`assert_shwild_not_matches!`] via **base-traits**; //! * `lookup-ranges` (default) — range matching via **collect-rs** //! `UnicodePointMap`; //! * `null-feature` — no effect; useful for driver scripts; @@ -45,13 +48,16 @@ //! ``` //! //! In unit tests, prefer [`assert_shwild_matches!`] and -//! [`assert_shwild_not_matches!`]: +//! [`assert_shwild_not_matches!`] (requires `"assertions"` feature): //! //! ``` +//! # #[cfg(feature = "assertions")] +//! # { //! use shwild::{assert_shwild_matches, assert_shwild_not_matches}; //! //! assert_shwild_matches!("[a-d]", "b"); //! assert_shwild_not_matches!("[a-d]", "e"); +//! # } //! ``` //! //! Further examples are in the repository **examples** directory and in @@ -1539,7 +1545,9 @@ impl CompiledMatcher { let mut num_bytes = 0; for c in pattern.chars() { - debug_assert!(continuum_prior.is_none() || std_matches!(state, ParseState::InNotRange | ParseState::InRange)); + debug_assert!( + continuum_prior.is_none() || std_matches!(state, ParseState::InNotRange | ParseState::InRange) + ); if escaped { match c { @@ -1944,6 +1952,8 @@ macro_rules! shwild_matches { /// Defines the macro `assert_shwild_matches!()`. /// +/// Available when the `"assertions"` feature is enabled. +/// /// # Parameters: /// - `$expected_pattern` - the pattern against which `$actual` is /// evaluated; @@ -1952,6 +1962,7 @@ macro_rules! shwild_matches { /// /// Panics if `$expected_pattern` is invalid or if `$actual` does not match. /// The 2-parameter form passes 0 for the `flags` parameter. +#[cfg(feature = "assertions")] #[macro_export] macro_rules! assert_shwild_matches { ($expected_pattern:expr, $actual:expr) => { @@ -1966,17 +1977,26 @@ macro_rules! assert_shwild_matches { match $crate::matches(expected_pattern, actual, flags) { Err(e) => { - panic!("could not evaluate actual value due to a failure in the parsing of expected pattern '{}': {}", expected_pattern, e); + panic!( + "could not evaluate actual value due to a failure in the parsing of expected pattern '{}': {}", + expected_pattern, e + ); }, Ok(b) => { - assert!(b, "assertion failed: actual value '{}' does not match the expected pattern '{}'", actual, expected_pattern); - } + assert!( + b, + "assertion failed: actual value '{}' does not match the expected pattern '{}'", + actual, expected_pattern + ); + }, } }; } /// Defines the macro `assert_shwild_not_matches!()`. /// +/// Available when the `"assertions"` feature is enabled. +/// /// # Parameters: /// - `$expected_pattern` - the pattern against which `$actual` is /// evaluated; @@ -1985,6 +2005,7 @@ macro_rules! assert_shwild_matches { /// /// Panics if `$expected_pattern` is invalid or if `$actual` matches. The /// 2-parameter form passes 0 for the `flags` parameter. +#[cfg(feature = "assertions")] #[macro_export] macro_rules! assert_shwild_not_matches { ($expected_pattern:expr, $actual:expr) => { @@ -1999,11 +2020,18 @@ macro_rules! assert_shwild_not_matches { match $crate::matches(expected_pattern, actual, flags) { Err(e) => { - panic!("could not evaluate actual value due to a failure in the parsing of expected pattern '{}': {}", expected_pattern, e); + panic!( + "could not evaluate actual value due to a failure in the parsing of expected pattern '{}': {}", + expected_pattern, e + ); }, Ok(b) => { - assert!(!b, "assertion failed: actual value '{}' matches unexpectedly with the pattern '{}'", actual, expected_pattern); - } + assert!( + !b, + "assertion failed: actual value '{}' matches unexpectedly with the pattern '{}'", + actual, expected_pattern + ); + }, } }; } @@ -3434,9 +3462,11 @@ mod tests { } + #[cfg(feature = "assertions")] mod TEST_ASSERTION_MATCHES { #![allow(non_snake_case)] + use super::*; #[test] @@ -3447,14 +3477,47 @@ mod tests { assert_shwild_matches!("?", "a"); assert_shwild_matches!("?", "z"); + + assert_shwild_matches!("*", ""); + assert_shwild_matches!("*", "anything"); + assert_shwild_matches!("ab*", "ab"); + assert_shwild_matches!("ab*", "abcd"); + } + + #[test] + fn TEST_assert_shwild_matches_WITH__IGNORE_CASE__1() { + assert_shwild_matches!("[a-d]", "A", IGNORE_CASE); + assert_shwild_matches!("[a-d]", "B", IGNORE_CASE); + assert_shwild_matches!("[a-d]", "C", IGNORE_CASE); + assert_shwild_matches!("[a-d]", "D", IGNORE_CASE); + } + + #[test] + fn TEST_assert_shwild_matches_WITH_FLAGS_ZERO_1() { + assert_shwild_matches!("[a-d]", "a", 0i64); + assert_shwild_matches!("[a-d]", "d", 0); } #[test] - #[should_panic(expected = "could not evaluate actual value due to a failure in the parsing of expected pattern '[a-d': pattern syntax error (at 0:4): incomplete range")] - fn TEST_assert_shwild_matches_2() { + #[should_panic( + expected = "could not evaluate actual value due to a failure in the parsing of expected pattern '[a-d': pattern syntax error (at 0:4): incomplete range" + )] + fn TEST_assert_shwild_matches_PARSE_ERROR_1() { assert_shwild_matches!("[a-d", "a"); } + #[test] + #[should_panic(expected = "assertion failed: actual value 'e' does not match the expected pattern '[a-d]'")] + fn TEST_assert_shwild_matches_MISMATCH_1() { + assert_shwild_matches!("[a-d]", "e"); + } + + #[test] + #[should_panic(expected = "assertion failed: actual value 'A' does not match the expected pattern '[a-d]'")] + fn TEST_assert_shwild_matches_MISMATCH_WITHOUT__IGNORE_CASE__1() { + assert_shwild_matches!("[a-d]", "A"); + } + #[test] fn TEST_assert_shwild_not_matches_1() { assert_shwild_not_matches!("[a-d]", "e"); @@ -3463,6 +3526,41 @@ mod tests { assert_shwild_not_matches!("??", "a"); assert_shwild_not_matches!("??", "z"); + + assert_shwild_not_matches!("ab", "a"); + assert_shwild_not_matches!("ab", "abc"); + } + + #[test] + fn TEST_assert_shwild_not_matches_WITH__IGNORE_CASE__1() { + assert_shwild_not_matches!("[a-d]", "e", IGNORE_CASE); + assert_shwild_not_matches!("[a-d]", "E", IGNORE_CASE); + } + + #[test] + fn TEST_assert_shwild_not_matches_WITH_FLAGS_ZERO_1() { + assert_shwild_not_matches!("[a-d]", "e", 0i64); + assert_shwild_not_matches!("[a-d]", "D", 0); + } + + #[test] + #[should_panic( + expected = "could not evaluate actual value due to a failure in the parsing of expected pattern '[a-d': pattern syntax error (at 0:4): incomplete range" + )] + fn TEST_assert_shwild_not_matches_PARSE_ERROR_1() { + assert_shwild_not_matches!("[a-d", "a"); + } + + #[test] + #[should_panic(expected = "assertion failed: actual value 'a' matches unexpectedly with the pattern '[a-d]'")] + fn TEST_assert_shwild_not_matches_UNEXPECTED_MATCH_1() { + assert_shwild_not_matches!("[a-d]", "a"); + } + + #[test] + #[should_panic(expected = "assertion failed: actual value 'D' matches unexpectedly with the pattern '[a-d]'")] + fn TEST_assert_shwild_not_matches_UNEXPECTED_MATCH_WITH__IGNORE_CASE__1() { + assert_shwild_not_matches!("[a-d]", "D", IGNORE_CASE); } } } From 1d4b3af43bbf532abd3d069f531c714195367540 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Wed, 8 Jul 2026 07:18:14 +1000 Subject: [PATCH 11/16] fmt --- src/lib.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index d7b18ec..24a43a1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1966,7 +1966,6 @@ macro_rules! shwild_matches { #[macro_export] macro_rules! assert_shwild_matches { ($expected_pattern:expr, $actual:expr) => { - assert_shwild_matches!($expected_pattern, $actual, 0i64); }; ($expected_pattern:expr, $actual:expr, $flags:expr) => { @@ -2009,7 +2008,6 @@ macro_rules! assert_shwild_matches { #[macro_export] macro_rules! assert_shwild_not_matches { ($expected_pattern:expr, $actual:expr) => { - assert_shwild_not_matches!($expected_pattern, $actual, 0i64); }; ($expected_pattern:expr, $actual:expr, $flags:expr) => { From ed649c1e962e122a7a1ef7a5c1c19d64d78b221f Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Wed, 8 Jul 2026 08:42:33 +1000 Subject: [PATCH 12/16] feature: added macro benchmarks --- CHANGES.md | 1 + Cargo.toml | 4 + benches/macros.rs | 721 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 726 insertions(+) create mode 100644 benches/macros.rs diff --git a/CHANGES.md b/CHANGES.md index 47d9fe3..3ab917b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,7 @@ * added `assert_shwild_matches!()` and `assert_shwild_not_matches!()` test assertion macros, available with the `"assertions"` feature (enabled by default); * added optional dependency on [**base-traits**](https://github.com/synesissoftware/base-traits) (via `"assertions"`; minimal features: `implement-AsI64-for-built_ins`); * crate-level and macro `///` documentation for the assertion macros; +* added macro benchmarks * **README.md** macros and dependencies sections updated; diff --git a/Cargo.toml b/Cargo.toml index 32f164a..8f745e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,10 @@ required-features = [ "test-regex", ] +[[bench]] +name = "macros" +harness = false + [[bench]] name = "range_string-creation_functions" harness = false diff --git a/benches/macros.rs b/benches/macros.rs new file mode 100644 index 0000000..ada4487 --- /dev/null +++ b/benches/macros.rs @@ -0,0 +1,721 @@ +// benches/macros.rs : evaluates performance of matching macros + +#![allow(non_snake_case)] + +use criterion::{ + criterion_group, + criterion_main, + Criterion, +}; + +use std::hint::black_box; + +use shwild::shwild_matches; + +#[cfg(feature = "assertions")] +use shwild::{ + assert_shwild_matches, + assert_shwild_not_matches, +}; + + +mod constants { + #![allow(non_upper_case_globals)] + #![allow(unused)] + + pub(crate) const EMPTY_STRING : &str = ""; + pub(crate) const S_hello : &str = "hello"; + pub(crate) const S_TQBFJOTLD : &str = "The quick brown fox jumps over the lazy dog"; + pub(crate) const S_Lorem_ipsum : &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."; + + pub(crate) mod patterns { + #![allow(non_upper_case_globals)] + #![allow(unused)] + + pub(crate) const NRANGE_CONTINUUM_SIMPLE : &str = r"[^a-z]"; + pub(crate) const NRANGE_CONTINUUM_REVERSE : &str = r"[^z-a]"; + pub(crate) const NRANGE_CONTINUUM_CROSSCASE : &str = r"[^a-Z]"; + pub(crate) const RANGE_CONTINUUM_SIMPLE : &str = r"[a-z]"; + pub(crate) const RANGE_CONTINUUM_REVERSE : &str = r"[z-a]"; + pub(crate) const RANGE_CONTINUUM_CROSSCASE : &str = r"[a-Z]"; + + pub(crate) const WINDOWS_PATH : &str = r"[A-Z]:\\?*\\?*.[ce][ox][em]"; + } + + pub(crate) mod windows_path_inputs { + #![allow(non_upper_case_globals)] + #![allow(unused)] + + pub(crate) const ALL : [&str; 7] = [ + "", + "C:/", + "C:/dir", + "C:/dir/stem.com", + "C:/dir/stem.exe", + "C:/directory-with-a-veeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeerrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrryyyyyyyyyyyyyyyyyyyyyyyyyyyyyy-long-name", + "C:/directory-with-a-veeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeerrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrryyyyyyyyyyyyyyyyyyyyyyyyyyyyyy-long-name/stem.com", + ]; + + pub(crate) const MATCHING : [&str; 3] = [ + r"C:\directory\file.exe", + r"X:\dir\filestem.exe", + r"D:\dir\sub-dir\filestem.exe", + ]; + + pub(crate) const NOT_MATCHING : [&str; 4] = [ + "", + r"X:\filestem.exe", + r"_:\dir\filestem.exe", + r"D:\dir\sub-dir\filestem.bat", + ]; + } +} + +mod macros_benches { + #![allow(non_snake_case)] + + use super::*; + + + pub fn shwild_matches_input_empty(c : &mut Criterion) { + let pattern = constants::EMPTY_STRING; + let input = ""; + let flags = 0; + + c.bench_function("`shwild_matches!()` - empty string", |b| { + b.iter(|| { + let r = shwild_matches!(black_box(pattern), black_box(input), black_box(flags)); + + let _ = black_box(r); + }) + }); + } + + pub fn shwild_matches_input_literal_small(c : &mut Criterion) { + let pattern = constants::EMPTY_STRING; + let input = ""; + let flags = 0; + + c.bench_function("`shwild_matches!()` - literal (small)", |b| { + b.iter(|| { + let r = shwild_matches!(black_box(pattern), black_box(input), black_box(flags)); + + let _ = black_box(r); + }) + }); + } + + pub fn shwild_matches_input_literal_medium(c : &mut Criterion) { + let pattern = constants::EMPTY_STRING; + let input = ""; + let flags = 0; + + c.bench_function("`shwild_matches!()` - literal (medium)", |b| { + b.iter(|| { + let r = shwild_matches!(black_box(pattern), black_box(input), black_box(flags)); + + let _ = black_box(r); + }) + }); + } + + pub fn shwild_matches_input_literal_large(c : &mut Criterion) { + let pattern = constants::EMPTY_STRING; + let input = ""; + let flags = 0; + + c.bench_function("`shwild_matches!()` - literal (large)", |b| { + b.iter(|| { + let r = shwild_matches!(black_box(pattern), black_box(input), black_box(flags)); + + let _ = black_box(r); + }) + }); + } + + pub fn shwild_matches_against_nrange_continuum_simple(c : &mut Criterion) { + let pattern = constants::patterns::NRANGE_CONTINUUM_SIMPLE; + let flags = 0; + + let inputs = [ + "", + " ", + "a", + "b", + "c", + "d", + "aa", + "_", + ]; + + c.bench_function("`shwild_matches!()` - nrange continuum (simple)", |b| { + b.iter(|| { + for input in &inputs { + let r = shwild_matches!(black_box(pattern), black_box(input), black_box(flags)); + + let _ = black_box(r); + } + }) + }); + } + + pub fn shwild_matches_against_nrange_continuum_reverse(c : &mut Criterion) { + let pattern = constants::patterns::NRANGE_CONTINUUM_REVERSE; + let flags = 0; + + let inputs = [ + "", + " ", + "a", + "b", + "c", + "d", + "aa", + "_", + ]; + + c.bench_function("`shwild_matches!()` - nrange continuum (reverse)", |b| { + b.iter(|| { + for input in &inputs { + let r = shwild_matches!(black_box(pattern), black_box(input), black_box(flags)); + + let _ = black_box(r); + } + }) + }); + } + + pub fn shwild_matches_against_nrange_continuum_crosscase(c : &mut Criterion) { + let pattern = constants::patterns::NRANGE_CONTINUUM_CROSSCASE; + let flags = 0; + + let inputs = [ + "", + " ", + "a", + "b", + "c", + "d", + "aa", + "_", + ]; + + c.bench_function("`shwild_matches!()` - nrange continuum (crosscase)", |b| { + b.iter(|| { + for input in &inputs { + let r = shwild_matches!(black_box(pattern), black_box(input), black_box(flags)); + + let _ = black_box(r); + } + }) + }); + } + + pub fn shwild_matches_against_range_continuum_simple(c : &mut Criterion) { + let pattern = constants::patterns::RANGE_CONTINUUM_SIMPLE; + let flags = 0; + + let inputs = [ + "", + " ", + "a", + "b", + "c", + "d", + "aa", + "_", + ]; + + c.bench_function("`shwild_matches!()` - range continuum (simple)", |b| { + b.iter(|| { + for input in &inputs { + let r = shwild_matches!(black_box(pattern), black_box(input), black_box(flags)); + + let _ = black_box(r); + } + }) + }); + } + + pub fn shwild_matches_against_range_continuum_reverse(c : &mut Criterion) { + let pattern = constants::patterns::RANGE_CONTINUUM_REVERSE; + let flags = 0; + + let inputs = [ + "", + " ", + "a", + "b", + "c", + "d", + "aa", + "_", + ]; + + c.bench_function("`shwild_matches!()` - range continuum (reverse)", |b| { + b.iter(|| { + for input in &inputs { + let r = shwild_matches!(black_box(pattern), black_box(input), black_box(flags)); + + let _ = black_box(r); + } + }) + }); + } + + pub fn shwild_matches_against_range_continuum_crosscase(c : &mut Criterion) { + let pattern = constants::patterns::RANGE_CONTINUUM_CROSSCASE; + let flags = 0; + + let inputs = [ + "", + " ", + "a", + "b", + "c", + "d", + "aa", + "_", + ]; + + c.bench_function("`shwild_matches!()` - range continuum (crosscase)", |b| { + b.iter(|| { + for input in &inputs { + let r = shwild_matches!(black_box(pattern), black_box(input), black_box(flags)); + + let _ = black_box(r); + } + }) + }); + } + + pub fn shwild_matches_test_against_pattern_WindowsPath(c : &mut Criterion) { + let pattern = constants::patterns::WINDOWS_PATH; + let flags = 0; + let inputs = constants::windows_path_inputs::ALL; + + c.bench_function("`shwild_matches!()` - Windows Path", |b| { + b.iter(|| { + for input in &inputs { + let r = shwild_matches!(black_box(pattern), black_box(input), black_box(flags)); + + let _ = black_box(r); + } + }) + }); + } +} + + + +#[cfg(feature = "assertions")] +mod assertion_benches { + #![allow(non_snake_case)] + + use super::*; + + + pub fn assert_shwild_matches_input_empty(c : &mut Criterion) { + let pattern = constants::EMPTY_STRING; + let input = ""; + + c.bench_function("`assert_shwild_matches!()` - empty string", |b| { + b.iter(|| { + assert_shwild_matches!(black_box(pattern), black_box(input)); + }) + }); + } + + pub fn assert_shwild_matches_input_literal_small(c : &mut Criterion) { + let pattern = constants::EMPTY_STRING; + let input = ""; + + c.bench_function("`assert_shwild_matches!()` - literal (small)", |b| { + b.iter(|| { + assert_shwild_matches!(black_box(pattern), black_box(input)); + }) + }); + } + + pub fn assert_shwild_matches_input_literal_medium(c : &mut Criterion) { + let pattern = constants::EMPTY_STRING; + let input = ""; + + c.bench_function("`assert_shwild_matches!()` - literal (medium)", |b| { + b.iter(|| { + assert_shwild_matches!(black_box(pattern), black_box(input)); + }) + }); + } + + pub fn assert_shwild_matches_input_literal_large(c : &mut Criterion) { + let pattern = constants::EMPTY_STRING; + let input = ""; + + c.bench_function("`assert_shwild_matches!()` - literal (large)", |b| { + b.iter(|| { + assert_shwild_matches!(black_box(pattern), black_box(input)); + }) + }); + } + + pub fn assert_shwild_matches_against_nrange_continuum_simple(c : &mut Criterion) { + let pattern = constants::patterns::NRANGE_CONTINUUM_SIMPLE; + + let inputs = [ + " ", + "_", + ]; + + c.bench_function("`assert_shwild_matches!()` - nrange continuum (simple)", |b| { + b.iter(|| { + for input in &inputs { + assert_shwild_matches!(black_box(pattern), black_box(input)); + } + }) + }); + } + + pub fn assert_shwild_matches_against_nrange_continuum_reverse(c : &mut Criterion) { + let pattern = constants::patterns::NRANGE_CONTINUUM_REVERSE; + + let inputs = [ + " ", + "_", + ]; + + c.bench_function("`assert_shwild_matches!()` - nrange continuum (reverse)", |b| { + b.iter(|| { + for input in &inputs { + assert_shwild_matches!(black_box(pattern), black_box(input)); + } + }) + }); + } + + pub fn assert_shwild_matches_against_nrange_continuum_crosscase(c : &mut Criterion) { + let pattern = constants::patterns::NRANGE_CONTINUUM_CROSSCASE; + + let inputs = [ + " ", + "_", + ]; + + c.bench_function("`assert_shwild_matches!()` - nrange continuum (crosscase)", |b| { + b.iter(|| { + for input in &inputs { + assert_shwild_matches!(black_box(pattern), black_box(input)); + } + }) + }); + } + + pub fn assert_shwild_matches_against_range_continuum_simple(c : &mut Criterion) { + let pattern = constants::patterns::RANGE_CONTINUUM_SIMPLE; + + let inputs = [ + "a", + "b", + "c", + "d", + ]; + + c.bench_function("`assert_shwild_matches!()` - range continuum (simple)", |b| { + b.iter(|| { + for input in &inputs { + assert_shwild_matches!(black_box(pattern), black_box(input)); + } + }) + }); + } + + pub fn assert_shwild_matches_against_range_continuum_reverse(c : &mut Criterion) { + let pattern = constants::patterns::RANGE_CONTINUUM_REVERSE; + + let inputs = [ + "a", + "b", + "c", + "d", + ]; + + c.bench_function("`assert_shwild_matches!()` - range continuum (reverse)", |b| { + b.iter(|| { + for input in &inputs { + assert_shwild_matches!(black_box(pattern), black_box(input)); + } + }) + }); + } + + pub fn assert_shwild_matches_against_range_continuum_crosscase(c : &mut Criterion) { + let pattern = constants::patterns::RANGE_CONTINUUM_CROSSCASE; + + let inputs = [ + "a", + "b", + "c", + "d", + "A", + "B", + "C", + "D", + ]; + + c.bench_function("`assert_shwild_matches!()` - range continuum (crosscase)", |b| { + b.iter(|| { + for input in &inputs { + assert_shwild_matches!(black_box(pattern), black_box(input)); + } + }) + }); + } + + pub fn assert_shwild_matches_test_against_pattern_WindowsPath(c : &mut Criterion) { + let pattern = constants::patterns::WINDOWS_PATH; + let inputs = constants::windows_path_inputs::MATCHING; + + c.bench_function("`assert_shwild_matches!()` - Windows Path", |b| { + b.iter(|| { + for input in &inputs { + assert_shwild_matches!(black_box(pattern), black_box(input)); + } + }) + }); + } + + pub fn assert_shwild_not_matches_input_empty(c : &mut Criterion) { + let pattern = constants::EMPTY_STRING; + let input = " "; + + c.bench_function("`assert_shwild_not_matches!()` - empty string", |b| { + b.iter(|| { + assert_shwild_not_matches!(black_box(pattern), black_box(input)); + }) + }); + } + + pub fn assert_shwild_not_matches_input_literal_small(c : &mut Criterion) { + let pattern = constants::EMPTY_STRING; + let input = " "; + + c.bench_function("`assert_shwild_not_matches!()` - literal (small)", |b| { + b.iter(|| { + assert_shwild_not_matches!(black_box(pattern), black_box(input)); + }) + }); + } + + pub fn assert_shwild_not_matches_input_literal_medium(c : &mut Criterion) { + let pattern = constants::EMPTY_STRING; + let input = " "; + + c.bench_function("`assert_shwild_not_matches!()` - literal (medium)", |b| { + b.iter(|| { + assert_shwild_not_matches!(black_box(pattern), black_box(input)); + }) + }); + } + + pub fn assert_shwild_not_matches_input_literal_large(c : &mut Criterion) { + let pattern = constants::EMPTY_STRING; + let input = " "; + + c.bench_function("`assert_shwild_not_matches!()` - literal (large)", |b| { + b.iter(|| { + assert_shwild_not_matches!(black_box(pattern), black_box(input)); + }) + }); + } + + pub fn assert_shwild_not_matches_against_nrange_continuum_simple(c : &mut Criterion) { + let pattern = constants::patterns::NRANGE_CONTINUUM_SIMPLE; + + let inputs = [ + "a", + "b", + "c", + "d", + "", + "aa", + ]; + + c.bench_function("`assert_shwild_not_matches!()` - nrange continuum (simple)", |b| { + b.iter(|| { + for input in &inputs { + assert_shwild_not_matches!(black_box(pattern), black_box(input)); + } + }) + }); + } + + pub fn assert_shwild_not_matches_against_nrange_continuum_reverse(c : &mut Criterion) { + let pattern = constants::patterns::NRANGE_CONTINUUM_REVERSE; + + let inputs = [ + "a", + "b", + "c", + "d", + "", + "aa", + ]; + + c.bench_function("`assert_shwild_not_matches!()` - nrange continuum (reverse)", |b| { + b.iter(|| { + for input in &inputs { + assert_shwild_not_matches!(black_box(pattern), black_box(input)); + } + }) + }); + } + + pub fn assert_shwild_not_matches_against_nrange_continuum_crosscase(c : &mut Criterion) { + let pattern = constants::patterns::NRANGE_CONTINUUM_CROSSCASE; + + let inputs = [ + "a", + "b", + "c", + "d", + "", + "aa", + ]; + + c.bench_function("`assert_shwild_not_matches!()` - nrange continuum (crosscase)", |b| { + b.iter(|| { + for input in &inputs { + assert_shwild_not_matches!(black_box(pattern), black_box(input)); + } + }) + }); + } + + pub fn assert_shwild_not_matches_against_range_continuum_simple(c : &mut Criterion) { + let pattern = constants::patterns::RANGE_CONTINUUM_SIMPLE; + + let inputs = [ + "", + " ", + "aa", + "_", + ]; + + c.bench_function("`assert_shwild_not_matches!()` - range continuum (simple)", |b| { + b.iter(|| { + for input in &inputs { + assert_shwild_not_matches!(black_box(pattern), black_box(input)); + } + }) + }); + } + + pub fn assert_shwild_not_matches_against_range_continuum_reverse(c : &mut Criterion) { + let pattern = constants::patterns::RANGE_CONTINUUM_REVERSE; + + let inputs = [ + "", + " ", + "aa", + "_", + ]; + + c.bench_function("`assert_shwild_not_matches!()` - range continuum (reverse)", |b| { + b.iter(|| { + for input in &inputs { + assert_shwild_not_matches!(black_box(pattern), black_box(input)); + } + }) + }); + } + + pub fn assert_shwild_not_matches_against_range_continuum_crosscase(c : &mut Criterion) { + let pattern = constants::patterns::RANGE_CONTINUUM_CROSSCASE; + + let inputs = [ + "", + " ", + "aa", + "_", + ]; + + c.bench_function("`assert_shwild_not_matches!()` - range continuum (crosscase)", |b| { + b.iter(|| { + for input in &inputs { + assert_shwild_not_matches!(black_box(pattern), black_box(input)); + } + }) + }); + } + + pub fn assert_shwild_not_matches_test_against_pattern_WindowsPath(c : &mut Criterion) { + let pattern = constants::patterns::WINDOWS_PATH; + let inputs = constants::windows_path_inputs::NOT_MATCHING; + + c.bench_function("`assert_shwild_not_matches!()` - Windows Path", |b| { + b.iter(|| { + for input in &inputs { + assert_shwild_not_matches!(black_box(pattern), black_box(input)); + } + }) + }); + } +} + + +criterion_group!( + shwild_matches_benches, + macros_benches::shwild_matches_input_empty, + macros_benches::shwild_matches_input_literal_small, + macros_benches::shwild_matches_input_literal_medium, + macros_benches::shwild_matches_input_literal_large, + macros_benches::shwild_matches_test_against_pattern_WindowsPath, + macros_benches::shwild_matches_against_nrange_continuum_simple, + macros_benches::shwild_matches_against_nrange_continuum_reverse, + macros_benches::shwild_matches_against_nrange_continuum_crosscase, + macros_benches::shwild_matches_against_range_continuum_simple, + macros_benches::shwild_matches_against_range_continuum_reverse, + macros_benches::shwild_matches_against_range_continuum_crosscase, +); + +#[cfg(feature = "assertions")] +criterion_group!( + assert_shwild_matches_benches, + assertion_benches::assert_shwild_matches_input_empty, + assertion_benches::assert_shwild_matches_input_literal_small, + assertion_benches::assert_shwild_matches_input_literal_medium, + assertion_benches::assert_shwild_matches_input_literal_large, + assertion_benches::assert_shwild_matches_test_against_pattern_WindowsPath, + assertion_benches::assert_shwild_matches_against_nrange_continuum_simple, + assertion_benches::assert_shwild_matches_against_nrange_continuum_reverse, + assertion_benches::assert_shwild_matches_against_nrange_continuum_crosscase, + assertion_benches::assert_shwild_matches_against_range_continuum_simple, + assertion_benches::assert_shwild_matches_against_range_continuum_reverse, + assertion_benches::assert_shwild_matches_against_range_continuum_crosscase, +); + +#[cfg(feature = "assertions")] +criterion_group!( + assert_shwild_not_matches_benches, + assertion_benches::assert_shwild_not_matches_input_empty, + assertion_benches::assert_shwild_not_matches_input_literal_small, + assertion_benches::assert_shwild_not_matches_input_literal_medium, + assertion_benches::assert_shwild_not_matches_input_literal_large, + assertion_benches::assert_shwild_not_matches_test_against_pattern_WindowsPath, + assertion_benches::assert_shwild_not_matches_against_nrange_continuum_simple, + assertion_benches::assert_shwild_not_matches_against_nrange_continuum_reverse, + assertion_benches::assert_shwild_not_matches_against_nrange_continuum_crosscase, + assertion_benches::assert_shwild_not_matches_against_range_continuum_simple, + assertion_benches::assert_shwild_not_matches_against_range_continuum_reverse, + assertion_benches::assert_shwild_not_matches_against_range_continuum_crosscase, +); + +#[cfg(feature = "assertions")] +criterion_main!( + shwild_matches_benches, + assert_shwild_matches_benches, + assert_shwild_not_matches_benches, +); + +#[cfg(not(feature = "assertions"))] +criterion_main!(shwild_matches_benches); From f8645fe2274cd95161727ba83d404d127d729cd9 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Wed, 8 Jul 2026 09:02:32 +1000 Subject: [PATCH 13/16] feature: added `"flexible-flags-type"` feature --- .github/workflows/ci.yml | 3 ++ CHANGES.md | 6 ++- Cargo.toml | 10 ++++ src/lib.rs | 102 ++++++++++++++++++++++++++++++++++++--- 4 files changed, 112 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 26ed2a1..36e491c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,6 +42,9 @@ jobs: - name: cargo test ("test-regex") run: cargo test --no-default-features --features test-regex --locked + - name: cargo test ("full") + run: cargo test --no-default-features --features full --locked + - name: cargo clippy run: cargo clippy --all-targets --locked -- -D warnings diff --git a/CHANGES.md b/CHANGES.md index 3ab917b..1c6d566 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,9 +4,11 @@ ## 0.2.0 - 8th July 2026 * added `assert_shwild_matches!()` and `assert_shwild_not_matches!()` test assertion macros, available with the `"assertions"` feature (enabled by default); -* added optional dependency on [**base-traits**](https://github.com/synesissoftware/base-traits) (via `"assertions"`; minimal features: `implement-AsI64-for-built_ins`); +* added `"flexible-flags-type"` feature — optional [**base-traits**](https://github.com/synesissoftware/base-traits) dependency (`implement-AsI64-for-built_ins`) allowing macro `flags` parameters to be any type implementing `AsI64`; when disabled, `flags` must be `i64`; +* `"assertions"` no longer implies **base-traits**; use `"full"` to enable assertions, flexible flags, and lookup ranges together; +* extended `shwild_matches!()` 3-parameter form with the same flexible-`flags` behaviour; +* added **macros** benchmark (`benches/macros.rs`); * crate-level and macro `///` documentation for the assertion macros; -* added macro benchmarks * **README.md** macros and dependencies sections updated; diff --git a/Cargo.toml b/Cargo.toml index 8f745e0..8f97573 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -91,6 +91,12 @@ default = [ "lookup-ranges", ] +full = [ + "assertions", + "flexible-flags-type", + "lookup-ranges", +] + # General features: # # - "_NEVER_TO_BE_ENABLED" - this is a placeholder feature and must NEVER be specified; @@ -103,10 +109,14 @@ null-feature = [] # Crate-specific features: # # - "assertions" - enable assertions; +# - "flexible-flags-type" - allows flags parameters to be any type for which `base_traits::I64` is implemented; # - "lookup-ranges" - enable lookup ranges; # - "test-regex" - enable test regex; assertions = [ +] + +flexible-flags-type = [ "dep:base-traits", ] diff --git a/src/lib.rs b/src/lib.rs index 24a43a1..d6f6628 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1945,9 +1945,18 @@ macro_rules! shwild_matches { ($pattern:expr, $input:expr $(,)?) => { $crate::matches($pattern, $input, 0) }; - ($pattern:expr, $input:expr, $flags:expr $(,)?) => { - $crate::matches($pattern, $input, $flags) - }; + ($pattern:expr, $input:expr, $flags:expr $(,)?) => {{ + #[cfg(feature = "flexible-flags-type")] + let flags = { + let flags : &dyn base_traits::AsI64 = &$flags; + + flags.as_i64() + }; + #[cfg(not(feature = "flexible-flags-type"))] + let flags : i64 = $flags; + + $crate::matches($pattern, $input, flags) + }}; } /// Defines the macro `assert_shwild_matches!()`. @@ -1971,8 +1980,14 @@ macro_rules! assert_shwild_matches { ($expected_pattern:expr, $actual:expr, $flags:expr) => { let expected_pattern : &str = &$expected_pattern; let actual = &$actual; - let flags : &dyn base_traits::AsI64 = &$flags; - let flags = flags.as_i64(); + #[cfg(feature = "flexible-flags-type")] + let flags = { + let flags : &dyn base_traits::AsI64 = &$flags; + + flags.as_i64() + }; + #[cfg(not(feature = "flexible-flags-type"))] + let flags : i64 = $flags; match $crate::matches(expected_pattern, actual, flags) { Err(e) => { @@ -2013,8 +2028,14 @@ macro_rules! assert_shwild_not_matches { ($expected_pattern:expr, $actual:expr, $flags:expr) => { let expected_pattern : &str = &$expected_pattern; let actual = &$actual; - let flags : &dyn base_traits::AsI64 = &$flags; - let flags = flags.as_i64(); + #[cfg(feature = "flexible-flags-type")] + let flags = { + let flags : &dyn base_traits::AsI64 = &$flags; + + flags.as_i64() + }; + #[cfg(not(feature = "flexible-flags-type"))] + let flags : i64 = $flags; match $crate::matches(expected_pattern, actual, flags) { Err(e) => { @@ -3561,6 +3582,73 @@ mod tests { assert_shwild_not_matches!("[a-d]", "D", IGNORE_CASE); } } + + + #[cfg(feature = "flexible-flags-type")] + mod TEST_FLEXIBLE_FLAGS_TYPE { + #![allow(non_snake_case)] + + use super::*; + + use base_traits::AsI64; + + + struct Flags(i64); + + impl AsI64 for Flags { + fn as_i64(&self) -> i64 { + self.0 + } + } + + + #[test] + fn TEST_shwild_matches_WITH__AsI64__IGNORE_CASE_1() { + assert_eq!(Ok(true), shwild_matches!("[a-d]", "A", IGNORE_CASE)); + assert_eq!(Ok(false), shwild_matches!("[a-d]", "A", 0i64)); + } + + #[test] + fn TEST_shwild_matches_WITH__AsI64__i64_1() { + let flags = IGNORE_CASE; + + assert_eq!(Ok(true), shwild_matches!("[a-d]", "B", flags)); + assert_eq!(Ok(false), shwild_matches!("[a-d]", "B", 0i64)); + } + + #[test] + fn TEST_shwild_matches_WITH__AsI64__Flags_1() { + let flags = Flags(IGNORE_CASE); + + assert_eq!(Ok(true), shwild_matches!("[a-d]", "C", flags)); + assert_eq!(Ok(false), shwild_matches!("[a-d]", "C", Flags(0))); + } + + #[cfg(feature = "assertions")] + mod TEST_ASSERTION_MATCHES { + #![allow(non_snake_case)] + + use super::*; + + + #[test] + fn TEST_assert_shwild_matches_WITH__AsI64__Flags_1() { + assert_shwild_matches!("[a-d]", "D", Flags(IGNORE_CASE)); + assert_shwild_matches!("[a-d]", "d", Flags(0)); + } + + #[test] + fn TEST_assert_shwild_not_matches_WITH__AsI64__Flags_1() { + assert_shwild_not_matches!("[a-d]", "e", Flags(0)); + assert_shwild_not_matches!("[a-d]", "D", Flags(0)); + } + + #[test] + fn TEST_assert_shwild_not_matches_WITH__AsI64__IGNORE_CASE_1() { + assert_shwild_not_matches!("[a-d]", "e", IGNORE_CASE); + } + } + } } From 9321ffa3973bdc0c369d3b43e24ca67c051650d8 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Wed, 8 Jul 2026 09:14:44 +1000 Subject: [PATCH 14/16] fmt --- benches/macros.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/benches/macros.rs b/benches/macros.rs index ada4487..49c910b 100644 --- a/benches/macros.rs +++ b/benches/macros.rs @@ -138,6 +138,7 @@ mod macros_benches { let flags = 0; let inputs = [ + // insert list: "", " ", "a", @@ -164,6 +165,7 @@ mod macros_benches { let flags = 0; let inputs = [ + // insert list: "", " ", "a", @@ -190,6 +192,7 @@ mod macros_benches { let flags = 0; let inputs = [ + // insert list: "", " ", "a", @@ -216,6 +219,7 @@ mod macros_benches { let flags = 0; let inputs = [ + // insert list: "", " ", "a", @@ -242,6 +246,7 @@ mod macros_benches { let flags = 0; let inputs = [ + // insert list: "", " ", "a", @@ -268,6 +273,7 @@ mod macros_benches { let flags = 0; let inputs = [ + // insert list: "", " ", "a", @@ -363,6 +369,7 @@ mod assertion_benches { let pattern = constants::patterns::NRANGE_CONTINUUM_SIMPLE; let inputs = [ + // insert list: " ", "_", ]; @@ -380,6 +387,7 @@ mod assertion_benches { let pattern = constants::patterns::NRANGE_CONTINUUM_REVERSE; let inputs = [ + // insert list: " ", "_", ]; @@ -397,6 +405,7 @@ mod assertion_benches { let pattern = constants::patterns::NRANGE_CONTINUUM_CROSSCASE; let inputs = [ + // insert list: " ", "_", ]; @@ -414,6 +423,7 @@ mod assertion_benches { let pattern = constants::patterns::RANGE_CONTINUUM_SIMPLE; let inputs = [ + // insert list: "a", "b", "c", @@ -433,6 +443,7 @@ mod assertion_benches { let pattern = constants::patterns::RANGE_CONTINUUM_REVERSE; let inputs = [ + // insert list: "a", "b", "c", @@ -452,6 +463,7 @@ mod assertion_benches { let pattern = constants::patterns::RANGE_CONTINUUM_CROSSCASE; let inputs = [ + // insert list: "a", "b", "c", @@ -532,6 +544,7 @@ mod assertion_benches { let pattern = constants::patterns::NRANGE_CONTINUUM_SIMPLE; let inputs = [ + // insert list: "a", "b", "c", @@ -553,6 +566,7 @@ mod assertion_benches { let pattern = constants::patterns::NRANGE_CONTINUUM_REVERSE; let inputs = [ + // insert list: "a", "b", "c", @@ -574,6 +588,7 @@ mod assertion_benches { let pattern = constants::patterns::NRANGE_CONTINUUM_CROSSCASE; let inputs = [ + // insert list: "a", "b", "c", @@ -595,6 +610,7 @@ mod assertion_benches { let pattern = constants::patterns::RANGE_CONTINUUM_SIMPLE; let inputs = [ + // insert list: "", " ", "aa", @@ -614,6 +630,7 @@ mod assertion_benches { let pattern = constants::patterns::RANGE_CONTINUUM_REVERSE; let inputs = [ + // insert list: "", " ", "aa", @@ -633,6 +650,7 @@ mod assertion_benches { let pattern = constants::patterns::RANGE_CONTINUUM_CROSSCASE; let inputs = [ + // insert list: "", " ", "aa", From d0d6e9a21400f677a47415151f0b76e31149bb2d Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Thu, 9 Jul 2026 16:46:41 +1000 Subject: [PATCH 15/16] squash-commit --- CHANGES.md | 9 ++++++++- Cargo.lock | 12 ++++++------ NEWS.md | 4 +++- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 52df80f..cde73ee 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,6 +12,13 @@ * **README.md** macros and dependencies sections updated; +## 0.1.6 - 10th July 2026 + +* preparatory changes; +* consistency fixes; +* added **benches/macros.rs**; + + ## 0.1.5 - 9th July 2026 * bunch of boilerplate improvements: .gitattributes; .vimrc; EXAMPLES.md; VS Code settings; ignores; license; rustfmt.toml; @@ -21,7 +28,7 @@ * added **CHANGES.md** (back-filled) and **NEWS.md**; * added **EXAMPLES.md** and per-example documentation; -* **README.md** badges, dependency links, related projects, and `null-feature` documentation; +* **README.md** badges, dependency links, related projects, and `"null-feature"` documentation; * added crate-level `//!` documentation; * added CI (`.github/workflows/ci.yml`) and quality scripts (`scripts/fmt`, checkers); * added `rust-version` (MSRV 1.79); diff --git a/Cargo.lock b/Cargo.lock index a594224..6690ee4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -218,9 +218,9 @@ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "memchr" -version = "2.8.2" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "num-traits" @@ -458,18 +458,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.53" +version = "0.8.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75726053136156d419e285b9b7eddaaea9e3fea6ce32eed44a89901f0bd98de1" +checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.53" +version = "0.8.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4714fd92cf900833d49538023a9b3915155210801d1c1169eba513b2addefd71" +checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" dependencies = [ "proc-macro2", "quote", diff --git a/NEWS.md b/NEWS.md index a5a2a54..07e1a19 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,7 +2,9 @@ | Date | News Item | | --------------------- | ----------------------------------------- | -| 8th July 2026 | shwild.Rust 0.2.0 released | +| 10th July 2026 | shwild.Rust 0.2.0 released | +| 9th July 2026 | shwild.Rust 0.1.6 released | +| 9th July 2026 | shwild.Rust 0.1.5 released | | 7th July 2026 | shwild.Rust 0.1.4 released | | 28th March 2025 | shwild.Rust 0.1.3 released | | 3rd November 2024 | shwild.Rust 0.1.2 released | From a31e5e52dbf23c0d53a51e4158d499e78a60f70c Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Thu, 9 Jul 2026 16:54:43 +1000 Subject: [PATCH 16/16] tidying --- CHANGES.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index cde73ee..8c3d927 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,18 +1,17 @@ # shwild.Rust - CHANGES -## 0.2.0 - 8th July 2026 +## 0.2.0 - 10th July 2026 * added `assert_shwild_matches!()` and `assert_shwild_not_matches!()` test assertion macros, available with the `"assertions"` feature (enabled by default); * added `"flexible-flags-type"` feature — optional [**base-traits**](https://github.com/synesissoftware/base-traits) dependency (`implement-AsI64-for-built_ins`) allowing macro `flags` parameters to be any type implementing `AsI64`; when disabled, `flags` must be `i64`; * `"assertions"` no longer implies **base-traits**; use `"full"` to enable assertions, flexible flags, and lookup ranges together; * extended `shwild_matches!()` 3-parameter form with the same flexible-`flags` behaviour; -* added **macros** benchmark (`benches/macros.rs`); * crate-level and macro `///` documentation for the assertion macros; * **README.md** macros and dependencies sections updated; -## 0.1.6 - 10th July 2026 +## 0.1.6 - 9th July 2026 * preparatory changes; * consistency fixes;