diff options
author | CosmicHorror <CosmicHorrorDev@pm.me> | 2023-10-28 14:13:54 -0600 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-10-28 15:13:54 -0500 |
commit | a88673b18e56cb056d8dab4f8c7bae60848b8915 (patch) | |
tree | 7562065164122b46beedbe1a652fec07156bf794 | |
parent | a98100fb53d0e73131e0dbe75757e2d139fdc52f (diff) |
Error on `$<num><non_num>` capture replacement names (#258)1.0.0-pre-alpha.51.0.0-alpha.0
* Mostly working capture name validation
* Improve inputs for property tests
* Fix advancing when passing over escaped dollar signs
* Switch to inline snapshot captures
* Cleanup invalid capture error formatting code
-rw-r--r-- | Cargo.lock | 317 | ||||
-rw-r--r-- | Cargo.toml | 5 | ||||
-rw-r--r-- | proptest-regressions/replacer/tests.txt | 8 | ||||
-rw-r--r-- | proptest-regressions/replacer/validate.txt | 10 | ||||
-rw-r--r-- | src/error.rs | 4 | ||||
-rw-r--r-- | src/main.rs | 12 | ||||
-rw-r--r-- | src/replacer/mod.rs (renamed from src/replacer.rs) | 75 | ||||
-rw-r--r-- | src/replacer/tests.rs | 80 | ||||
-rw-r--r-- | src/replacer/validate.rs | 380 | ||||
-rw-r--r-- | tests/cli.rs | 92 |
10 files changed, 901 insertions, 82 deletions
@@ -12,6 +12,16 @@ dependencies = [ ] [[package]] +name = "ansi-to-html" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7bd918cc0ff933f0e6cf48a8f74584818ea43e07d1fba1f9251bb3df2a37ca2" +dependencies = [ + "regex", + "thiserror", +] + +[[package]] name = "ansi_term" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -55,7 +65,7 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" dependencies = [ - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -65,7 +75,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" dependencies = [ "anstyle", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -96,6 +106,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -191,6 +216,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] +name = "console" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "unicode-width", + "windows-sys 0.45.0", +] + +[[package]] name = "crossbeam-deque" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -242,13 +280,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] name = "errno" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -264,6 +308,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] name = "globset" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -317,6 +372,19 @@ dependencies = [ ] [[package]] +name = "insta" +version = "1.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d64600be34b2fcfc267740a243fa7744441bb4947a619ac4e5bb6507f35fbfc" +dependencies = [ + "console", + "lazy_static", + "linked-hash-map", + "similar", + "yaml-rust", +] + +[[package]] name = "is-terminal" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -324,7 +392,7 @@ checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ "hermit-abi", "rustix", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -349,6 +417,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" [[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] name = "linux-raw-sys" version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -385,12 +465,28 @@ dependencies = [ ] [[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] name = "once_cell" version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] name = "predicates" version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -428,6 +524,32 @@ dependencies = [ ] [[package]] +name = "proptest" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c003ac8c77cb07bb74f5f198bce836a689bcd5a42574612bf14d17bfd08c20e" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags 2.4.1", + "lazy_static", + "num-traits", + "rand", + "rand_chacha", + "rand_xorshift", + "regex-syntax 0.7.5", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] name = "quote" version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -437,6 +559,45 @@ dependencies = [ ] [[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core", +] + +[[package]] name = "rayon" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -474,7 +635,7 @@ dependencies = [ "aho-corasick", "memchr", "regex-automata", - "regex-syntax", + "regex-syntax 0.8.2", ] [[package]] @@ -485,11 +646,17 @@ checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.8.2", ] [[package]] name = "regex-syntax" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" + +[[package]] +name = "regex-syntax" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" @@ -510,7 +677,19 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "rusty-fork" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", ] [[package]] @@ -532,17 +711,22 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" name = "sd" version = "0.7.6" dependencies = [ + "ansi-to-html", "ansi_term", "anyhow", "assert_cmd", "clap", "clap_mangen", + "console", "globwalk", "ignore", + "insta", "is-terminal", "memmap2", + "proptest", "rayon", "regex", + "regex-automata", "tempfile", "thiserror", "unescape", @@ -569,6 +753,12 @@ dependencies = [ ] [[package]] +name = "similar" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2aeaf503862c419d66959f5d7ca015337d864e9c49485d771b732e2a20453597" + +[[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -595,7 +785,7 @@ dependencies = [ "fastrand", "redox_syscall", "rustix", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -605,7 +795,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" dependencies = [ "rustix", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -645,6 +835,12 @@ dependencies = [ ] [[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] name = "unescape" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -657,6 +853,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + +[[package]] name = "utf8parse" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -682,6 +884,12 @@ dependencies = [ ] [[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -714,11 +922,35 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", ] [[package]] @@ -727,53 +959,95 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" @@ -787,3 +1061,12 @@ dependencies = [ "clap_mangen", "roff", ] + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] @@ -43,6 +43,11 @@ clap.workspace = true assert_cmd = "2.0.12" anyhow = "1.0.75" clap_mangen = "0.2.14" +proptest = "1.3.1" +console = "0.15.7" +insta = "1.34.0" +ansi-to-html = "0.1.3" +regex-automata = "0.4.3" [profile.release] opt-level = 3 diff --git a/proptest-regressions/replacer/tests.txt b/proptest-regressions/replacer/tests.txt new file mode 100644 index 0000000..f164b58 --- /dev/null +++ b/proptest-regressions/replacer/tests.txt @@ -0,0 +1,8 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 3a23ade8355ca034558ea8635e4ea2ee96ecb38b7b1cb9a854509d7633d45795 # shrinks to s = "" +cc 8c8d1e7497465f26416bddb7607df0de1fce48d098653eeabac0ad2aeba1fa0a # shrinks to s = "$0$0a" diff --git a/proptest-regressions/replacer/validate.txt b/proptest-regressions/replacer/validate.txt new file mode 100644 index 0000000..bbdb3b3 --- /dev/null +++ b/proptest-regressions/replacer/validate.txt @@ -0,0 +1,10 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc cfacd65058c8dae0ac7b91c56b8096c36ef68cb35d67262debebac005ea9c677 # shrinks to s = "" +cc 61e5dc6ce0314cde48b5cbc839fbf46a49fcf8d0ba02cfeecdcbff52fca8c786 # shrinks to s = "$a" +cc 8e5fd9dbb58ae762a751349749320664715056ef63aad58215397e87ee42c722 # shrinks to s = "$$" +cc 37c2e41ceeddbecbc4e574f82b58a4007923027ad1a6756bf2f547aa3f748d13 # shrinks to s = "$$0" diff --git a/src/error.rs b/src/error.rs index e757cf4..517defd 100644 --- a/src/error.rs +++ b/src/error.rs @@ -3,6 +3,8 @@ use std::{ path::PathBuf, }; +use crate::replacer::InvalidReplaceCapture; + #[derive(thiserror::Error)] pub enum Error { #[error("invalid regex {0}")] @@ -15,6 +17,8 @@ pub enum Error { InvalidPath(PathBuf), #[error("failed processing files:\n{0}")] FailedProcessing(FailedJobs), + #[error("{0}")] + InvalidReplaceCapture(#[from] InvalidReplaceCapture), } pub struct FailedJobs(Vec<(PathBuf, Error)>); diff --git a/src/main.rs b/src/main.rs index de24644..c7097b4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,13 +5,23 @@ mod input; pub(crate) mod replacer; pub(crate) mod utils; +use std::process; + pub(crate) use self::input::{App, Source}; +use ansi_term::{Color, Style}; pub(crate) use error::{Error, Result}; use replacer::Replacer; use clap::Parser; -fn main() -> Result<()> { +fn main() { + if let Err(e) = try_main() { + eprintln!("{}: {}", Style::from(Color::Red).bold().paint("error"), e); + process::exit(1); + } +} + +fn try_main() -> Result<()> { let options = cli::Options::parse(); let source = if options.recursive { diff --git a/src/replacer.rs b/src/replacer/mod.rs index f6d5d21..b6e0c0b 100644 --- a/src/replacer.rs +++ b/src/replacer/mod.rs @@ -1,6 +1,14 @@ +use std::{fs, fs::File, io::prelude::*, path::Path}; + use crate::{utils, Error, Result}; + use regex::bytes::Regex; -use std::{fs, fs::File, io::prelude::*, path::Path}; + +#[cfg(test)] +mod tests; +mod validate; + +pub use validate::{validate_replace, InvalidReplaceCapture}; pub(crate) struct Replacer { regex: Regex, @@ -20,6 +28,8 @@ impl Replacer { let (look_for, replace_with) = if is_literal { (regex::escape(&look_for), replace_with.into_bytes()) } else { + validate_replace(&replace_with)?; + ( look_for, utils::unescape(&replace_with) @@ -154,66 +164,3 @@ impl Replacer { Ok(()) } } - -#[cfg(test)] -mod tests { - use super::*; - - fn replace( - look_for: impl Into<String>, - replace_with: impl Into<String>, - literal: bool, - flags: Option<&'static str>, - src: &'static str, - target: &'static str, - ) { - let replacer = Replacer::new( - look_for.into(), - replace_with.into(), - literal, - flags.map(ToOwned::to_owned), - None, - ) - .unwrap(); - assert_eq!( - std::str::from_utf8(&replacer.replace(src.as_bytes())), - Ok(target) - ); - } - - #[test] - fn default_global() { - replace("a", "b", false, None, "aaa", "bbb"); - } - - #[test] - fn escaped_char_preservation() { - replace("a", "b", false, None, "a\\n", "b\\n"); - } - - #[test] - fn case_sensitive_default() { - replace("abc", "x", false, None, "abcABC", "xABC"); - replace("abc", "x", true, None, "abcABC", "xABC"); - } - - #[test] - fn sanity_check_literal_replacements() { - replace("((special[]))", "x", true, None, "((special[]))y", "xy"); - } - - #[test] - fn unescape_regex_replacements() { - replace("test", r"\n", false, None, "testtest", "\n\n"); - } - - #[test] - fn no_unescape_literal_replacements() { - replace("test", r"\n", true, None, "testtest", r"\n\n"); - } - - #[test] - fn full_word_replace() { - replace("abc", "def", false, Some("w"), "abcd abc", "abcd def"); - } -} diff --git a/src/replacer/tests.rs b/src/replacer/tests.rs new file mode 100644 index 0000000..9304a17 --- /dev/null +++ b/src/replacer/tests.rs @@ -0,0 +1,80 @@ +use super::*; + +use proptest::prelude::*; + +proptest! { + #[test] + fn validate_doesnt_panic(s in r"(\PC*\$?){0,5}") { + let _ = validate::validate_replace(&s); + } + + // $ followed by a digit and a non-ident char or an ident char + #[test] + fn validate_ok(s in r"([^\$]*(\$([0-9][^a-zA-Z_0-9\$]|a-zA-Z_))?){0,5}") { + validate::validate_replace(&s).unwrap(); + } + + // Force at least one $ followed by a digit and an ident char + #[test] + fn validate_err(s in r"[^\$]*?\$[0-9][a-zA-Z_]\PC*") { + validate::validate_replace(&s).unwrap_err(); + } +} + +fn replace( + look_for: impl Into<String>, + replace_with: impl Into<String>, + literal: bool, + flags: Option<&'static str>, + src: &'static str, + target: &'static str, +) { + let replacer = Replacer::new( + look_for.into(), + replace_with.into(), + literal, + flags.map(ToOwned::to_owned), + None, + ) + .unwrap(); + assert_eq!( + std::str::from_utf8(&replacer.replace(src.as_bytes())), + Ok(target) + ); +} + +#[test] +fn default_global() { + replace("a", "b", false, None, "aaa", "bbb"); +} + +#[test] +fn escaped_char_preservation() { + replace("a", "b", false, None, "a\\n", "b\\n"); +} + +#[test] +fn case_sensitive_default() { + replace("abc", "x", false, None, "abcABC", "xABC"); + replace("abc", "x", true, None, "abcABC", "xABC"); +} + +#[test] +fn sanity_check_literal_replacements() { + replace("((special[]))", "x", true, None, "((special[]))y", "xy"); +} + +#[test] +fn unescape_regex_replacements() { + replace("test", r"\n", false, None, "testtest", "\n\n"); +} + +#[test] +fn no_unescape_literal_replacements() { + replace("test", r"\n", true, None, "testtest", r"\n\n"); +} + +#[test] +fn full_word_replace() { + replace("abc", "def", false, Some("w"), "abcd abc", "abcd def"); +} diff --git a/src/replacer/validate.rs b/src/replacer/validate.rs new file mode 100644 index 0000000..da5cc71 --- /dev/null +++ b/src/replacer/validate.rs @@ -0,0 +1,380 @@ +use std::{error::Error, fmt, str::CharIndices}; + +use ansi_term::{Color, Style}; + +#[derive(Debug)] +pub struct InvalidReplaceCapture { + original_replace: String, + invalid_ident: Span, + num_leading_digits: usize, +} + +impl Error for InvalidReplaceCapture {} + +// NOTE: This code is much more allocation heavy than it needs to be, but it's +// only displayed as a hard error to the user, so it's not a big deal +impl fmt::Display for InvalidReplaceCapture { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + #[derive(Clone, Copy)] + enum SpecialChar { + Newline, + CarriageReturn, + Tab, + } + + impl SpecialChar { + fn new(c: char) -> Option<Self> { + match c { + '\n' => Some(Self::Newline), + '\r' => Some(Self::CarriageReturn), + '\t' => Some(Self::Tab), + _ => None, + } + } + + /// Renders as the character from the "Control Pictures" block + /// + /// https://en.wikipedia.org/wiki/Control_Pictures + fn render(self) -> char { + match self { + Self::Newline => '␊', + Self::CarriageReturn => '␍', + Self::Tab => '␉', + } + } + } + + let Self { + original_replace, + invalid_ident, + num_leading_digits, + } = self; + + // Build up the error to show the user + let mut formatted = String::new(); + let mut arrows_start = Span::start_at(0); + let special = Style::new().bold(); + let error = Style::from(Color::Red).bold(); + for (byte_index, c) in original_replace.char_indices() { + let (prefix, suffix, text) = match SpecialChar::new(c) { + Some(c) => { + (Some(special.prefix()), Some(special.suffix()), c.render()) + } + None => { + let (prefix, suffix) = if byte_index == invalid_ident.start + { + (Some(error.prefix()), None) + } else if byte_index + == invalid_ident.end.checked_sub(1).unwrap() + { + (None, Some(error.suffix())) + } else { + (None, None) + }; + (prefix, suffix, c) + } + }; + + if let Some(prefix) = prefix { + formatted.push_str(&prefix.to_string()); + } + formatted.push(text); + if let Some(suffix) = suffix { + formatted.push_str(&suffix.to_string()); + } + + if byte_index < invalid_ident.start { + // Assumes that characters have a base display width of 1. While + // that's not technically true, it's near impossible to do right + // since the specifics on text rendering is up to the user's + // terminal/font. This _does_ rely on variable-width characters + // like \n, \r, and \t getting converting to single character + // representations above + arrows_start.start += 1; + } + } + + // This relies on all non-curly-braced capture chars being 1 byte + let arrows_span = arrows_start.end_offset(invalid_ident.len()); + let mut arrows = " ".repeat(arrows_span.start); + arrows.push_str(&format!( + "{}", + Style::new().bold().paint("^".repeat(arrows_span.len())) + )); + + let ident = invalid_ident.slice(original_replace); + let (number, the_rest) = ident.split_at(*num_leading_digits); + let disambiguous = format!("${{{number}}}{the_rest}"); + let error_message = format!( + "The numbered capture group `{}` in the replacement text is ambiguous.", + Style::new().bold().paint(format!("${}", number).to_string()) + ); + let hint_message = format!( + "{}: Use curly braces to disambiguate it `{}`.", + Style::from(Color::Blue).bold().paint("hint"), + Style::new().bold().paint(disambiguous) + ); + + writeln!(f, "{}", error_message)?; + writeln!(f, "{}", hint_message)?; + writeln!(f, "{}", formatted)?; + write!(f, "{}", arrows) + } +} + +pub fn validate_replace(s: &str) -> Result<(), InvalidReplaceCapture> { + for ident in ReplaceCaptureIter::new(s) { + let mut char_it = ident.name.char_indices(); + let (_, c) = char_it.next().unwrap(); + if c.is_ascii_digit() { + for (i, c) in char_it { + if !c.is_ascii_digit() { + return Err(InvalidReplaceCapture { + original_replace: s.to_owned(), + invalid_ident: ident.span, + num_leading_digits: i, + }); + } + } + } + } + + Ok(()) +} + +#[derive(Clone, Copy, Debug)] +struct Span { + start: usize, + end: usize, +} + +impl Span { + fn start_at(start: usize) -> SpanOpen { + SpanOpen { start } + } + + fn new(start: usize, end: usize) -> Self { + // `<` instead of `<=` because `Span` is exclusive on the upper bound + assert!(start < end); + Self { start, end } + } + + fn slice(self, s: &str) -> &str { + &s[self.start..self.end] + } + + fn len(self) -> usize { + self.end - self.start + } +} + +#[derive(Clone, Copy)] +struct SpanOpen { + start: usize, +} + +impl SpanOpen { + fn end_at(self, end: usize) -> Span { + let Self { start } = self; + Span::new(start, end) + } + + fn end_offset(self, offset: usize) -> Span { + assert_ne!(offset, 0); + let Self { start } = self; + self.end_at(start + offset) + } +} + +#[derive(Debug)] +struct Capture<'rep> { + name: &'rep str, + span: Span, +} + +impl<'rep> Capture<'rep> { + fn new(name: &'rep str, span: Span) -> Self { + Self { name, span } + } +} + +/// An iterator over the capture idents in an interpolated replacement string +/// +/// This cod |