From 1b7b834c28ea8aa04d13ce52951730facd49c722 Mon Sep 17 00:00:00 2001 From: Yoann Blot Date: Wed, 25 Mar 2026 16:21:09 +0100 Subject: [PATCH] feat: allow configure individually each rule --- Cargo.lock | 811 +----------------- Cargo.toml | 11 +- build.rs | 90 ++ src/config.rs | 8 + src/main.rs | 31 +- src/resolver.rs | 134 +++ src/rules/mod.rs | 22 +- src/rules/quality/add_final_keyword.rs | 113 +-- tests/fixtures/resolver/config/src/app.php | 2 +- tests/fixtures/resolver/config/tests/unit.php | 2 +- tests/fixtures/resolver/dir_with_php/a.php | 2 +- tests/fixtures/resolver/dir_with_php/b.php | 2 +- tests/fixtures/resolver/nested/root.php | 2 +- tests/fixtures/resolver/nested/sub/nested.php | 2 +- tests/resolver_test.rs | 38 + 15 files changed, 379 insertions(+), 891 deletions(-) create mode 100644 build.rs diff --git a/Cargo.lock b/Cargo.lock index 321f76b..823d9e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,16 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "Inflector" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" -dependencies = [ - "lazy_static", - "regex", -] - [[package]] name = "aho-corasick" version = "1.1.4" @@ -21,66 +11,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "anyhow" -version = "1.0.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" - -[[package]] -name = "ariadne" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8454c8a44ce2cb9cc7e7fae67fc6128465b343b92c6631e94beca3c8d1524ea5" -dependencies = [ - "unicode-width", - "yansi", -] - -[[package]] -name = "arrayref" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" - -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" - -[[package]] -name = "blake3" -version = "1.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" -dependencies = [ - "arrayref", - "arrayvec", - "cc", - "cfg-if", - "constant_time_eq", - "cpufeatures", -] - [[package]] name = "bstr" version = "1.12.1" @@ -91,57 +21,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bumpalo" -version = "3.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" -dependencies = [ - "serde", -] - -[[package]] -name = "cc" -version = "1.2.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "codespan-reporting" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" -dependencies = [ - "serde", - "termcolor", - "unicode-width", -] - -[[package]] -name = "constant_time_eq" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -167,12 +46,6 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" -[[package]] -name = "dyn-clone" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" - [[package]] name = "either" version = "1.15.0" @@ -185,33 +58,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - -[[package]] -name = "fsevent-sys" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" -dependencies = [ - "libc", -] - [[package]] name = "glob" version = "0.3.3" @@ -238,10 +84,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] -name = "heck" -version = "0.5.0" +name = "ignore" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] [[package]] name = "indexmap" @@ -253,58 +109,6 @@ dependencies = [ "hashbrown", ] -[[package]] -name = "inotify" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd5b3eaf1a28b758ac0faa5a4254e8ab2705605496f1b1f3fbbc3988ad73d199" -dependencies = [ - "bitflags 2.11.0", - "inotify-sys", - "libc", -] - -[[package]] -name = "inotify-sys" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" -dependencies = [ - "libc", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "kqueue" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" -dependencies = [ - "kqueue-sys", - "libc", -] - -[[package]] -name = "kqueue-sys" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" -dependencies = [ - "bitflags 1.3.2", - "libc", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - [[package]] name = "libc" version = "0.2.183" @@ -317,214 +121,27 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" -[[package]] -name = "mago-database" -version = "1.15.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c7b10a23e6d5f1589de8809052b48d6523475cb7a4ab7fc1da783959f501074" -dependencies = [ - "foldhash", - "glob", - "globset", - "memchr", - "notify", - "rayon", - "serde", - "simdutf8", - "tracing", - "walkdir", -] - -[[package]] -name = "mago-php-version" -version = "1.15.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "735a537984399a6ef6b8284e156e72ad02d63921f46b97e241ee71cee2d2854b" -dependencies = [ - "schemars", - "serde", -] - -[[package]] -name = "mago-reporting" -version = "1.15.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b1f84031541e2848bfd59cfa2347260e799ba9a18ee7b732111170738b6593" -dependencies = [ - "ariadne", - "blake3", - "codespan-reporting", - "foldhash", - "mago-database", - "mago-span", - "mago-text-edit", - "schemars", - "serde", - "serde-sarif", - "serde_json", - "strum 0.28.0", - "termcolor", -] - -[[package]] -name = "mago-span" -version = "1.15.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9fab570c46da07b81629b2126e61dd984e565af9f9cb5909c21f431c9dd3a4" -dependencies = [ - "mago-database", - "serde", -] - -[[package]] -name = "mago-syntax" -version = "1.15.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec09761f36922845e0796a2be1428b79eb3c14af16ea1acd828b9e3ae78c77ff" -dependencies = [ - "bumpalo", - "mago-database", - "mago-php-version", - "mago-reporting", - "mago-span", - "mago-syntax-core", - "memchr", - "ordered-float", - "paste", - "serde", - "strum 0.28.0", -] - -[[package]] -name = "mago-syntax-core" -version = "1.15.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91dc4c4d95f0f536351bc703645763b75a6b7a802c5ff57a867f22491581190" -dependencies = [ - "bumpalo", - "mago-database", - "mago-span", - "memchr", -] - -[[package]] -name = "mago-text-edit" -version = "1.15.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e43928360868a349a6d7a1ab70cc1e8ad9e269c64b3c516c040ae44a993588e8" -dependencies = [ - "serde", - "strum 0.28.0", - "tracing", -] - [[package]] name = "memchr" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" -[[package]] -name = "mio" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" -dependencies = [ - "libc", - "log", - "wasi", - "windows-sys 0.61.2", -] - -[[package]] -name = "notify" -version = "8.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" -dependencies = [ - "bitflags 2.11.0", - "fsevent-sys", - "inotify", - "kqueue", - "libc", - "log", - "mio", - "notify-types", - "walkdir", - "windows-sys 0.60.2", -] - -[[package]] -name = "notify-types" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" -dependencies = [ - "bitflags 2.11.0", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "ordered-float" -version = "5.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4779c6901a562440c3786d08192c6fbda7c1c2060edd10006b05ee35d10f2d" -dependencies = [ - "num-traits", - "rand", - "serde", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - [[package]] name = "php-refactor" -version = "0.1.0" +version = "0.1.1" dependencies = [ - "bumpalo", + "glob", + "globset", + "ignore", "libc", - "mago-database", - "mago-span", - "mago-syntax", + "rayon", + "regex", "serde", "toml", "walkdir", ] -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn 2.0.117", -] - [[package]] name = "proc-macro2" version = "1.0.106" @@ -543,25 +160,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "rand_core", - "serde", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "serde", -] - [[package]] name = "rayon" version = "1.11.0" @@ -582,26 +180,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "ref-cast" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "regex" version = "1.12.3" @@ -640,58 +218,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "schemafy_core" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bec29dddcfe60f92f3c0d422707b8b56473983ef0481df8d5236ed3ab8fdf24" -dependencies = [ - "serde", - "serde_json", -] - -[[package]] -name = "schemafy_lib" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af3d87f1df246a9b7e2bfd1f4ee5f88e48b11ef9cfc62e63f0dead255b1a6f5f" -dependencies = [ - "Inflector", - "proc-macro2", - "quote", - "schemafy_core", - "serde", - "serde_derive", - "serde_json", - "syn 1.0.109", - "uriparse", -] - -[[package]] -name = "schemars" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" -dependencies = [ - "dyn-clone", - "ref-cast", - "schemars_derive", - "serde", - "serde_json", -] - -[[package]] -name = "schemars_derive" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" -dependencies = [ - "proc-macro2", - "quote", - "serde_derive_internals", - "syn 2.0.117", -] - [[package]] name = "serde" version = "1.0.228" @@ -702,26 +228,6 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "serde-sarif" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a053c46f18a8043570d4e32fefc4c6377f82bf29ec310a33e93f273048e3b0be" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "schemafy_lib", - "serde", - "serde_json", - "strum 0.27.2", - "strum_macros 0.27.2", - "syn 2.0.117", - "thiserror", - "typed-builder", -] - [[package]] name = "serde_core" version = "1.0.228" @@ -739,32 +245,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", -] - -[[package]] -name = "serde_derive_internals" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "serde_json" -version = "1.0.149" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" -dependencies = [ - "indexmap", - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", + "syn", ] [[package]] @@ -776,68 +257,6 @@ dependencies = [ "serde", ] -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "simdutf8" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" - -[[package]] -name = "strum" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" - -[[package]] -name = "strum" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" -dependencies = [ - "strum_macros 0.28.0", -] - -[[package]] -name = "strum_macros" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "strum_macros" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - [[package]] name = "syn" version = "2.0.117" @@ -849,35 +268,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "toml" version = "0.8.23" @@ -919,79 +309,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", -] - -[[package]] -name = "typed-builder" -version = "0.21.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fef81aec2ca29576f9f6ae8755108640d0a86dd3161b2e8bca6cfa554e98f77d" -dependencies = [ - "typed-builder-macro", -] - -[[package]] -name = "typed-builder-macro" -version = "0.21.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ecb9ecf7799210407c14a8cfdfe0173365780968dc57973ed082211958e0b18" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" -[[package]] -name = "unicode-width" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" - -[[package]] -name = "uriparse" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0200d0fc04d809396c2ad43f3c95da3582a2556eba8d453c1087f4120ee352ff" -dependencies = [ - "fnv", - "lazy_static", -] - [[package]] name = "walkdir" version = "2.5.0" @@ -1002,19 +325,13 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - [[package]] name = "winapi-util" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -1023,15 +340,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets", -] - [[package]] name = "windows-sys" version = "0.61.2" @@ -1041,71 +349,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - [[package]] name = "winnow" version = "0.7.15" @@ -1114,15 +357,3 @@ checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" dependencies = [ "memchr", ] - -[[package]] -name = "yansi" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" - -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index b002eff..fb346c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "php-refactor" -version = "0.1.0" +version = "0.1.1" edition = "2024" [[bin]] @@ -13,10 +13,11 @@ path = "src/lib.rs" [dependencies] libc = "0.2" -mago-syntax = "1.15.3" -mago-span = "1.15.3" -mago-database = "1.15.3" -bumpalo = "3" serde = { version = "1", features = ["derive"] } toml = "0.8" walkdir = "2" +glob = "0.3" +rayon = "1" +regex = "1" +ignore = "0.4" +globset = "0.4" diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..8a1f8a6 --- /dev/null +++ b/build.rs @@ -0,0 +1,90 @@ +use std::fs; +use std::path::Path; + +fn main() { + // Scan src/rules/ directory to auto-discover rule modules + let rules_dir = "src/rules"; + let mut rules_code = String::from("// Auto-generated rule registry\n\n"); + + // Track rules for all_rules() and all_source_transforms() + let mut all_rules_entries = Vec::new(); + let mut all_transforms_entries = Vec::new(); + + // Scan categories (subdirectories of src/rules/) + if let Ok(entries) = fs::read_dir(rules_dir) { + for entry in entries.flatten() { + let path = entry.path(); + + // Skip mod.rs and other files, only process directories + if !path.is_dir() { + continue; + } + + let category = path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("") + .to_string(); + + if category == "quality" { + // Scan quality subdirectory for rule modules + if let Ok(rule_entries) = fs::read_dir(&path) { + for rule_entry in rule_entries.flatten() { + let rule_path = rule_entry.path(); + + // Look for .rs files that are rule modules + if rule_path.extension().map(|e| e == "rs").unwrap_or(false) { + let rule_name = + rule_path.file_stem().and_then(|n| n.to_str()).unwrap_or(""); + + // Skip mod.rs + if rule_name == "mod" { + continue; + } + + let rule_key = format!("{}/{}", category, rule_name); + let module_path = format!("{}::{}", category, rule_name); + + all_rules_entries.push(format!( + " (\"{}\", {}::apply),", + rule_key, module_path + )); + all_transforms_entries.push(format!( + " (\"{}\", {}::apply_to_source),", + rule_key, module_path + )); + } + } + } + } + } + } + + // Generate all_rules() function + rules_code.push_str("pub fn all_rules() -> Vec<(&'static str, RuleFn)> {\n"); + rules_code.push_str(" vec![\n"); + for entry in all_rules_entries { + rules_code.push_str(&entry); + rules_code.push('\n'); + } + rules_code.push_str(" ]\n"); + rules_code.push_str("}\n\n"); + + // Generate all_source_transforms() function + rules_code + .push_str("pub fn all_source_transforms() -> Vec<(&'static str, SourceTransformFn)> {\n"); + rules_code.push_str(" vec![\n"); + for entry in all_transforms_entries { + rules_code.push_str(&entry); + rules_code.push('\n'); + } + rules_code.push_str(" ]\n"); + rules_code.push_str("}\n"); + + // Write generated code to a file in OUT_DIR + let out_dir = std::env::var("OUT_DIR").unwrap(); + let dest_path = Path::new(&out_dir).join("generated_rules.rs"); + fs::write(&dest_path, rules_code).unwrap(); + + println!("cargo:rerun-if-changed=src/rules/"); +} diff --git a/src/config.rs b/src/config.rs index 82807f1..3435ed0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,9 +1,12 @@ use serde::Deserialize; +use std::collections::HashMap; use std::fs; #[derive(Deserialize)] pub struct Config { pub source: SourceConfig, + #[serde(flatten)] + pub rules: HashMap>, } #[derive(Deserialize)] @@ -11,6 +14,11 @@ pub struct SourceConfig { pub paths: Vec, } +#[derive(Deserialize, Clone)] +pub struct RuleConfig { + pub paths: Vec, +} + pub fn load(path: &str) -> Result> { let contents = fs::read_to_string(path)?; let config = toml::from_str(&contents)?; diff --git a/src/main.rs b/src/main.rs index fa58706..abaf12c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,10 +19,35 @@ fn main() { let mut total_changed = 0; let mut total_analyzed = 0; - for (_, rule_fn) in rules::all_rules() { - let result = rule_fn(&path); + // Load config once instead of per-rule + let config = if path.ends_with(".toml") { + php_refactor::config::load(&path).ok() + } else { + None + }; + + for (rule_key, rule_fn) in rules::all_rules() { + let collect_start = Instant::now(); + let files = + php_refactor::resolver::resolve_for_rule_with_config(&path, rule_key, config.as_ref()); + let collect_ms = collect_start.elapsed().as_secs_f64() * 1000.0; + + let process_start = Instant::now(); + let result = rule_fn(&files); + let process_ms = process_start.elapsed().as_secs_f64() * 1000.0; + + eprintln!( + "[INFO] {}: collected {} in {:.2}ms → matched {}, changed {}, processed in {:.2}ms", + rule_key, + files.len(), + collect_ms, + result.files_matched, + result.files_changed, + process_ms + ); + total_changed += result.files_changed; - total_analyzed = result.files_analyzed; + total_analyzed += result.files_analyzed; } let total_elapsed = total_start.elapsed(); diff --git a/src/resolver.rs b/src/resolver.rs index bb67c95..62c110d 100644 --- a/src/resolver.rs +++ b/src/resolver.rs @@ -1,6 +1,52 @@ +use globset::{Glob, GlobSetBuilder}; +use ignore::{WalkBuilder, WalkState}; +use std::collections::HashSet; use std::path::{Path, PathBuf}; +use std::sync::mpsc::channel; use walkdir::WalkDir; +/// Resolve the effective files for a rule with an already-loaded config. +/// Avoids re-loading and re-parsing the config per rule. +pub fn resolve_for_rule_with_config( + path: &str, + rule_key: &str, + config: Option<&crate::config::Config>, +) -> Vec { + if let Some(cfg) = config { + // Split rule_key (e.g., "quality/add_final_keyword") into category and name + let parts: Vec<&str> = rule_key.split('/').collect(); + if parts.len() == 2 { + let category = parts[0]; + let name = parts[1]; + + // Check if this rule has a specific path configuration + if let Some(rule_config) = cfg.rules.get(category).and_then(|c| c.get(name)) { + return collect_php_files_from_globs(&rule_config.paths); + } + } + } + + // Fall back to standard path expansion + collect_php_files(path).unwrap_or_default() +} + +/// Resolve the effective files for a rule based on CLI path and rule key. +/// +/// If path is a .toml config file and the rule has per-rule paths configured, +/// use those paths (with glob expansion). Otherwise, use standard path expansion. +/// +/// This function loads the config on each call. For better performance with multiple rules, +/// use `resolve_for_rule_with_config` and load the config once. +pub fn resolve_for_rule(path: &str, rule_key: &str) -> Vec { + let config = if path.ends_with(".toml") { + crate::config::load(path).ok() + } else { + None + }; + + resolve_for_rule_with_config(path, rule_key, config.as_ref()) +} + /// Expand a path argument into a list of PHP files to process. /// - `.toml` file → load config, walk configured paths /// - directory → walk and collect .php files @@ -22,6 +68,71 @@ pub fn collect_php_files(path: &str) -> Result, Box Vec { + // 1. Compile all patterns into one GlobSet (fast DFA matching, all patterns in one pass). + // Deduplicate base directories to avoid walking the same tree multiple times. + let mut gsbuilder = GlobSetBuilder::new(); + let mut bases: HashSet = HashSet::new(); + + for p in patterns { + let effective = if p.contains('/') { + p.clone() + } else { + format!("**/{}", p) + }; + bases.insert(extract_base_dir(&effective)); + if let Ok(g) = Glob::new(&effective) { + gsbuilder.add(g); + } + } + + let Ok(globset) = gsbuilder.build() else { + return vec![]; + }; + + // 2. One WalkBuilder with all base directories as roots. + let mut bases_iter = bases.into_iter(); + let Some(first) = bases_iter.next() else { + return vec![]; + }; + + let mut builder = WalkBuilder::new(&first); + for base in bases_iter { + builder.add(&base); + } + + // 3. One WalkParallel run: all roots walked in parallel, globset matching all patterns. + let (tx, rx) = channel(); + builder.standard_filters(true).build_parallel().run(|| { + let gs = globset.clone(); + let tx = tx.clone(); + Box::new(move |result| { + if let Ok(entry) = result { + let path = entry.path(); + if entry.file_type().map(|ft| ft.is_file()).unwrap_or(false) + && path.extension().and_then(|s| s.to_str()) == Some("php") + && gs.is_match(path) + { + let _ = tx.send(path.to_path_buf()); + } + } + WalkState::Continue + }) + }); + drop(tx); + + let mut result: Vec = rx.into_iter().collect(); + result.sort(); + result +} + /// Load a TOML config and collect all .php files from configured paths. fn collect_from_config(config_path: &str) -> Result, Box> { let cfg = crate::config::load(config_path)?; @@ -43,3 +154,26 @@ fn collect_from_directory(dir_path: &str) -> Vec { .map(|e| e.path().to_path_buf()) .collect() } + +/// Extract the longest non-wildcard prefix directory from a glob pattern. +/// This anchors walkdir to the smallest useful subtree, avoiding unnecessary traversal +/// of unrelated directories (e.g., vendor/ when pattern is src/**/*.php). +/// +/// Examples: +/// - "src/Interface/**/*Request.php" → PathBuf("src/Interface") +/// - "src/**/*.php" → PathBuf("src") +/// - "**/*.php" → PathBuf(".") +fn extract_base_dir(pattern: &str) -> PathBuf { + let parts: Vec<&str> = pattern.split('/').collect(); + let base_parts: Vec<&str> = parts + .iter() + .take_while(|p| !p.contains('*') && !p.contains('?') && !p.contains('{')) + .copied() + .collect(); + + if base_parts.is_empty() { + PathBuf::from(".") + } else { + PathBuf::from(base_parts.join("/")) + } +} diff --git a/src/rules/mod.rs b/src/rules/mod.rs index 97ee597..10ee84e 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -4,27 +4,15 @@ pub mod quality; #[derive(Debug, Clone, Copy)] pub struct RuleResult { pub files_changed: usize, + pub files_matched: usize, pub files_analyzed: usize, } -/// File-aware rule: takes a path (file, config, or directory) and returns stats. -pub type RuleFn = fn(&str) -> RuleResult; +/// File-aware rule: takes a list of files to process and returns stats. +pub type RuleFn = fn(&[std::path::PathBuf]) -> RuleResult; /// Pure source transformation: takes source code, returns modified source or None. pub type SourceTransformFn = fn(&str) -> Option; -/// Rules registered for execution via main.rs -pub fn all_rules() -> Vec<(&'static str, RuleFn)> { - vec![( - "quality/add_final_keyword", - quality::add_final_keyword::apply, - )] -} - -/// Pure source transforms used by tests -pub fn all_source_transforms() -> Vec<(&'static str, SourceTransformFn)> { - vec![( - "quality/add_final_keyword", - quality::add_final_keyword::apply_to_source, - )] -} +// Include auto-generated rule registry (discovered from src/rules/ at build time) +include!(concat!(env!("OUT_DIR"), "/generated_rules.rs")); diff --git a/src/rules/quality/add_final_keyword.rs b/src/rules/quality/add_final_keyword.rs index c495d73..d6992c8 100644 --- a/src/rules/quality/add_final_keyword.rs +++ b/src/rules/quality/add_final_keyword.rs @@ -1,90 +1,63 @@ -use bumpalo::Bump; -use mago_database::file::FileId; -use mago_syntax::ast::{Modifier, Sequence, Statement}; -use mago_syntax::parser::parse_file_content; +use rayon::prelude::*; +use regex::Regex; use std::fs; - -/// File-aware entry point: takes a path and returns stats. -pub fn apply(path: &str) -> crate::rules::RuleResult { - let files = match crate::resolver::collect_php_files(path) { - Ok(f) => f, - Err(_) => { - return crate::rules::RuleResult { - files_changed: 0, - files_analyzed: 0, - }; - } - }; - - let mut files_changed = 0; - let total_analyzed = files.len(); - - for file_path in files { - let Ok(original) = fs::read_to_string(&file_path) else { - continue; +use std::path::PathBuf; +use std::sync::LazyLock; +use std::sync::atomic::{AtomicUsize, Ordering}; + +static RE: LazyLock = LazyLock::new(|| { + // Matches "class" keyword at the start of a line (after optional whitespace). + // Captures: (1) leading whitespace, (2) optional "readonly " modifier. + // Naturally excludes: abstract class, final class, interface, trait, enum, ::class, $class, etc. + Regex::new(r"(?m)^(\s*)(readonly\s+)?class\s").unwrap() +}); + +/// File-aware entry point: applies the rule to the given set of files in parallel. +pub fn apply(files: &[PathBuf]) -> crate::rules::RuleResult { + let files_matched = AtomicUsize::new(0); + let files_changed = AtomicUsize::new(0); + + files.par_iter().for_each(|file_path| { + let Ok(original) = fs::read_to_string(file_path) else { + return; }; if let Some(modified) = apply_to_source(&original) { - files_changed += usize::from(fs::write(&file_path, &modified).is_ok()); + files_matched.fetch_add(1, Ordering::Relaxed); + if fs::write(file_path, &modified).is_ok() { + files_changed.fetch_add(1, Ordering::Relaxed); + } } - } + }); crate::rules::RuleResult { - files_changed, - files_analyzed: total_analyzed, + files_changed: files_changed.load(Ordering::Relaxed), + files_matched: files_matched.load(Ordering::Relaxed), + files_analyzed: files.len(), } } /// Pure source transformation: used by tests. pub fn apply_to_source(source: &str) -> Option { - let arena = Bump::new(); - let file_id = FileId::zero(); - let program = parse_file_content(&arena, file_id, source); - - let mut insertions: Vec = Vec::new(); - collect_insertions(&program.statements, &mut insertions); - - if insertions.is_empty() { + // Early exit: skip expensive regex if source has no class declaration + if !source.contains("class ") { return None; } - // Apply insertions back-to-front to preserve offsets - let mut result = source.to_string(); - insertions.sort_unstable_by(|a, b| b.cmp(a)); - for offset in insertions { - result.insert_str(offset, "final "); + // Check if pattern exists before attempting replacement + if !RE.is_match(source) { + return None; } - Some(result) -} + let result = RE.replace_all(source, |caps: ®ex::Captures| { + let indent = &caps[1]; + let readonly = caps.get(2).map(|m| m.as_str()).unwrap_or(""); + format!("{}final {}class ", indent, readonly) + }); -fn collect_insertions<'a>( - statements: &'a Sequence<'a, Statement<'a>>, - insertions: &mut Vec, -) { - for stmt in statements { - match stmt { - Statement::Class(class) => { - let mods = &class.modifiers; - if mods.contains_final() || mods.contains_abstract() { - continue; - } - // Insert before `readonly` if present, else before `class` keyword - let offset = if mods.contains_readonly() { - match mods.get_readonly() { - Some(Modifier::Readonly(kw)) => kw.span.start.offset as usize, - _ => unreachable!(), - } - } else { - class.class.span.start.offset as usize - }; - insertions.push(offset); - } - Statement::Interface(_) => {} // skip - Statement::Namespace(ns) => { - collect_insertions(ns.statements(), insertions); - } - _ => {} - } + if result == source { + None + } else { + Some(result.into_owned()) } } diff --git a/tests/fixtures/resolver/config/src/app.php b/tests/fixtures/resolver/config/src/app.php index 29fe1c5..7227e90 100644 --- a/tests/fixtures/resolver/config/src/app.php +++ b/tests/fixtures/resolver/config/src/app.php @@ -1,3 +1,3 @@