diff --git a/Cargo.lock b/Cargo.lock index 3e8990d..ef47de6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -121,21 +121,6 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" -[[package]] -name = "bisync" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5020822f6d6f23196ccaf55e228db36f9de1cf788052b37992e17cbc96ec41a7" -dependencies = [ - "bisync_macros", -] - -[[package]] -name = "bisync_macros" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d21f40d350a700f6aa107e45fb26448cf489d34794b2ba4522181dc9f1173af6" - [[package]] name = "bitflags" version = "1.3.2" @@ -519,9 +504,9 @@ dependencies = [ [[package]] name = "gix" -version = "0.86.0" +version = "0.87.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb3790fd8981cba7949f1ba924ef865d902df731627bc5998d14164063892fce" +checksum = "fdefc1465d8631807deaf504dacdeff628b978de515653b47976ca7c28ab2a7a" dependencies = [ "gix-actor", "gix-attributes", @@ -541,12 +526,14 @@ dependencies = [ "gix-ignore", "gix-index", "gix-lock", + "gix-note", "gix-object", "gix-odb", "gix-pack", "gix-path", "gix-pathspec", "gix-protocol", + "gix-quote", "gix-ref", "gix-refspec", "gix-revision", @@ -570,9 +557,9 @@ dependencies = [ [[package]] name = "gix-actor" -version = "0.41.2" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33f9308ad6fd35b2a865cbe4117ac61b2be59e4a9ef1621c7a9794f7c8e52c5b" +checksum = "e37efa99929ac62f980fb1a7dcd09dea85b3aa6ead6ba0498362add3f4e698de" dependencies = [ "bstr", "gix-date", @@ -581,9 +568,9 @@ dependencies = [ [[package]] name = "gix-attributes" -version = "0.34.0" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31c593692ebdc1e38858d9a2b56f6a594c501e24a38971fe6685571f5a07be0" +checksum = "76b83782ac69ae28921a6e8fbcf2e24069613f1518030b97ae622b079abffa54" dependencies = [ "bstr", "gix-features", @@ -598,40 +585,39 @@ dependencies = [ [[package]] name = "gix-bitmap" -version = "0.3.3" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd1d118d0f5d88b96e6f6e13b566475fef4797ead4a02c26fed36c1375066f7" +checksum = "f17013c7ef5cb95ebb4bf4978201e6a8a42ffea9d666fd5388f1b7e742307d80" dependencies = [ "gix-error", ] [[package]] name = "gix-chunk" -version = "0.7.3" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a871e5cab12ba568845714473505deefffb3c04eb47f4708ce344cd459c1cc" +checksum = "00e32f938b3745ac93d73bc7490b6a15a661b2fc81973bfe90e275cc53c908e1" dependencies = [ "gix-error", ] [[package]] name = "gix-command" -version = "0.9.1" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00706d4fef135ef4b01680d5218c6ee40cda8baf697b864296cbc887d19118f6" +checksum = "aa00adbd2c77f584cfe41f348772d2f131af1fa519a7f6b98638f1144ead96a4" dependencies = [ "bstr", "gix-path", "gix-quote", "gix-trace", - "shell-words", ] [[package]] name = "gix-commitgraph" -version = "0.38.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2cd7f054ae2727223fe46dd39c012f066b12f532962d336d29ee193261787da" +checksum = "6b93c9fb1f5be01bba54a7a3b7455d359efc68555539c75ef74b8021bb6db497" dependencies = [ "bstr", "gix-chunk", @@ -643,9 +629,9 @@ dependencies = [ [[package]] name = "gix-config" -version = "0.59.0" +version = "0.60.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "103d11bef95c467577ecfa8b7b86a22e65af3507b2c9bfa3809a4afbae7df301" +checksum = "f973c28c0a4871a7926fc90c8377d4458fd6cbb19692f56582b4d2022313ae90" dependencies = [ "bstr", "gix-config-value", @@ -662,9 +648,9 @@ dependencies = [ [[package]] name = "gix-config-value" -version = "0.19.0" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f813e312a3f7f327187823cd4c754a3e4d948c98907d11e8f37554a2b6b8059" +checksum = "6f6af5321bfd3711a279d6b244d58532ba1cfabf9eb6374791f19929d8970082" dependencies = [ "bitflags 2.13.1", "bstr", @@ -675,9 +661,9 @@ dependencies = [ [[package]] name = "gix-date" -version = "0.15.6" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e47b9e8cdc688296609b706428de570f88b1e0eed7156dde7b4a89d26fa4567" +checksum = "e63d9aa18f29facd571c8953e66224ee0075b9e16622024794555ed4acceea85" dependencies = [ "bstr", "gix-error", @@ -687,9 +673,9 @@ dependencies = [ [[package]] name = "gix-diff" -version = "0.66.0" +version = "0.67.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fee7d89a3c507491cdfc57a1d1e0e300214720b4f7709ebc253e422f99822bfc" +checksum = "1b1689ff5ddeee4acfb2a43e875a1072a76d208624b15a9065c938b39cb0da2a" dependencies = [ "bstr", "gix-command", @@ -708,9 +694,9 @@ dependencies = [ [[package]] name = "gix-discover" -version = "0.54.0" +version = "0.55.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f517766fa1101dfe2606c1a19a8ffa699099030995a9194445446dfe261bdf" +checksum = "ec32a30ec2934735599c2545f30067e80bd255fd3454b52a5d59bc02b52874a3" dependencies = [ "bstr", "dunce", @@ -723,18 +709,18 @@ dependencies = [ [[package]] name = "gix-error" -version = "0.2.5" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a9292309fd944e71b2a3c96d3c03a6feb8852db646febdde7cbb9f79cb5f329" +checksum = "4a1ba536602db507119eb5f3cdf80bef3ca5a5ec0dfc6e9c7f46dc70ade7c112" dependencies = [ "bstr", ] [[package]] name = "gix-features" -version = "0.49.0" +version = "0.49.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20aa09e83a48dc02c5f5f08578aa79d3ab1bab4618b8c362f88684645a02bdcc" +checksum = "39c0e59d9d253dcccc38c3a46b91bfb9b46bd63eed54fe1a719e12194884d52a" dependencies = [ "bytes", "crc32fast", @@ -751,9 +737,9 @@ dependencies = [ [[package]] name = "gix-filter" -version = "0.33.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e7b5dbf524d97e839f642930c76d7f011c0791e7d11d8148989ac5af7c76aa8" +checksum = "9c30b28d957e2e6f1e1bbfbfd26b8e0bb0aab68d8a5e730fb4fd3049943575e3" dependencies = [ "bstr", "encoding_rs", @@ -772,9 +758,9 @@ dependencies = [ [[package]] name = "gix-fs" -version = "0.22.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "865cf13fcaf5455220546cb9607c416bd1be9a6caafd143655a362fdeab64e80" +checksum = "ebcfa9fd253f25350a3b21b3dd74034a446098e373c6123d4cee3519894f12ef" dependencies = [ "bstr", "gix-features", @@ -785,9 +771,9 @@ dependencies = [ [[package]] name = "gix-glob" -version = "0.27.0" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "421e92a711554fa5827d1b0599d3389acdd0f6729e97a8c5a57d79af1e50bf36" +checksum = "b417cf515fd8c91468b578071f76d6cba716f8a1eccd853906bff4908b2c1413" dependencies = [ "bitflags 2.13.1", "bstr", @@ -797,9 +783,9 @@ dependencies = [ [[package]] name = "gix-hash" -version = "0.26.0" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13adaa73415fd6c902310923f68d0b98e8cecf14b33ea58c02cc387cee56f54e" +checksum = "380b9c423a54f0821064b954b5a5ab25c660812f6a91da03c3f1cb4b10762aa5" dependencies = [ "faster-hex", "gix-features", @@ -820,9 +806,9 @@ dependencies = [ [[package]] name = "gix-ignore" -version = "0.22.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12cff8e8aa125e39377456073e63df3334d9e5741372ddcc226198015076dda2" +checksum = "65859a2f7de5e159d4344a5486ebf53b6bac22d5ec6afa836e47c10ae88a7f0f" dependencies = [ "bstr", "gix-glob", @@ -833,19 +819,19 @@ dependencies = [ [[package]] name = "gix-imara-diff" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a791e6620676a875f362f3156ed213e73ca099a09bf992c18812abe65cc37b1" +checksum = "1c91d8cffac8849493a82233811bd02b2b183b8cf39bf704de0fa0841b737595" dependencies = [ "bstr", - "hashbrown 0.16.1", + "hashbrown 0.17.1", ] [[package]] name = "gix-index" -version = "0.54.0" +version = "0.55.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5009c4e7e9f9b4cfaaab1153e49133eb04d79c015b5702d6c3d2ab94271a89c6" +checksum = "632e16cb48b0e88a747e106924cb2765194105d8070a7a961b0d080ed806c632" dependencies = [ "bitflags 2.13.1", "bstr", @@ -880,18 +866,43 @@ dependencies = [ "thiserror", ] +[[package]] +name = "gix-macros" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3836b4b051393464a753c5a08fe19c7ce0d8b77574a92bd558972941d4553cb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "gix-note" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9af6bbdb3f3c62b4920407c479af1cc0beb27fd75a762782d48baed53d330958" +dependencies = [ + "gix-error", + "gix-hash", + "gix-hashtable", + "gix-object", +] + [[package]] name = "gix-object" -version = "0.63.0" +version = "0.64.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e48c235e7f886eb819fc878af75be889333dd3c38bee02ed7af48ae2cf596c4" +checksum = "56fef799ca40cdeab4de5a3e74a696d8fa9b9c6264592646bf022e026f13ce2c" dependencies = [ "bstr", "gix-actor", + "gix-command", "gix-date", "gix-features", "gix-hash", "gix-hashtable", + "gix-tempfile", "gix-utils", "gix-validate", "itoa", @@ -901,9 +912,9 @@ dependencies = [ [[package]] name = "gix-odb" -version = "0.83.0" +version = "0.84.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dd494ffb5037e62b8220109e894d2861ff2150a2cacbfccdba57ae1ebab2b96" +checksum = "cf45d195b71cb6363886e7b466821bf4dffd7aba1b6b814ff75488e0061d72a7" dependencies = [ "arc-swap", "gix-features", @@ -923,11 +934,12 @@ dependencies = [ [[package]] name = "gix-pack" -version = "0.73.0" +version = "0.74.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d5446127b269706e85998065267ddd2ccc3550179da6780b22fe496175ccb20" +checksum = "3d199d515cbdcf05f531f3859be9771fc12acac7b7fe0f5b581b2c1ab1c3a61e" dependencies = [ "clru", + "crossbeam-deque", "gix-chunk", "gix-error", "gix-features", @@ -944,9 +956,9 @@ dependencies = [ [[package]] name = "gix-packetline" -version = "0.22.0" +version = "0.22.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3766025c72319c4accdd854a18e6f0dd176c8eb0f3bc8a60a7765be2b50cabf2" +checksum = "792bd92bea390087e6223b18d4d023decc36467de31e6b066b69b94beea1b9e2" dependencies = [ "bstr", "faster-hex", @@ -956,9 +968,9 @@ dependencies = [ [[package]] name = "gix-path" -version = "0.12.3" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ed3e8d7a82e886e17a72e03d4ba0c13db6f2219b6cd4e2900b4cae426ec20c9" +checksum = "38fc6f029ea67de83cbcbd33fd98c05a48360d2932b39d9fdacbc6eae802d475" dependencies = [ "bstr", "gix-trace", @@ -968,9 +980,9 @@ dependencies = [ [[package]] name = "gix-pathspec" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f6fa5f8007f008187c3f60b4373209ca83d1cc947f35ede03e16cd15a4d137" +checksum = "c92e44c63ba53bb55aa88ee48847664beae9446417621582fb14dfc69b2f8c72" dependencies = [ "bitflags 2.13.1", "bstr", @@ -983,15 +995,15 @@ dependencies = [ [[package]] name = "gix-protocol" -version = "0.64.0" +version = "0.65.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dede40e89c1e90f548415f50636bb051f6d9c60f68b8b710bc07825722d19588" +checksum = "68878c37ae168b474c762d70adc8a8e0f9323ed0d80444a672e2eb561fa16691" dependencies = [ - "bisync", "bstr", "gix-date", "gix-features", "gix-hash", + "gix-macros", "gix-ref", "gix-shallow", "gix-transport", @@ -1002,9 +1014,9 @@ dependencies = [ [[package]] name = "gix-quote" -version = "0.7.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6e541fc33cc2b783b7979040d445a0c86a2eca747c8faea4ca84230d06ae6ef" +checksum = "f74795eb76eab8313063849f9cbd2bbcdc6e4692f71c1d7d0244ab11cd777329" dependencies = [ "bstr", "gix-error", @@ -1013,9 +1025,9 @@ dependencies = [ [[package]] name = "gix-ref" -version = "0.66.0" +version = "0.67.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeb0c90a8f6202ceaaa22996cbf837c943ccb2d8af9ff3490f0758305e6b7883" +checksum = "8328387e7ab354e1dc3afb63e6b4d1866f82d7ce035222bc9d5baaf36bc88020" dependencies = [ "gix-actor", "gix-features", @@ -1033,15 +1045,12 @@ dependencies = [ [[package]] name = "gix-refspec" -version = "0.44.0" +version = "0.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7406282cc0259b51f6aee299ca3d31279a020530363152a2e6c96e8a7f7bbc83" +checksum = "7de280d46e8fd9e4d3e7e2ab4276b965e377ba68b2b9a8fce56371015e8bb059" dependencies = [ "bstr", - "gix-error", - "gix-glob", "gix-hash", - "gix-revision", "gix-validate", "smallvec", "thiserror", @@ -1049,9 +1058,9 @@ dependencies = [ [[package]] name = "gix-revision" -version = "0.48.0" +version = "0.49.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e55e09d4a1ecf2beecc8c09cafcad37979e805b31f588b0e957e191df5783681" +checksum = "4577b864c3e134697e91564553e43230c2e401bb1bcc51cfa998edca21c95078" dependencies = [ "bitflags 2.13.1", "bstr", @@ -1068,9 +1077,9 @@ dependencies = [ [[package]] name = "gix-revwalk" -version = "0.34.0" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c113c0a53294dc6280ffc06cbcc4f50f820397e97d6a00b429a44b8db26e29" +checksum = "248823eefe405e2c0754b74f6f43ab6faab68670cdbf2d6e114cb0471f766450" dependencies = [ "gix-commitgraph", "gix-date", @@ -1109,9 +1118,9 @@ dependencies = [ [[package]] name = "gix-submodule" -version = "0.33.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd98077a56d08886112e6b08dc94076d03539f4bc0b9d7880e4be2b8a640d8c" +checksum = "da85564d6725c483b8d95d7b31d1db0b78c29e462a2b481949e2f18e1ed55a6a" dependencies = [ "bstr", "gix-config", @@ -1143,9 +1152,9 @@ checksum = "be3eb81d9dc914335923e50d52829c551feefd6a72d176c4130c546b67a60814" [[package]] name = "gix-transport" -version = "0.58.0" +version = "0.59.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c1bcf30081eb8ab04540a5795c67a2fcb22cb2e976e8b1a4ea657b1ed61469" +checksum = "b3f41a64939953ff49117df4eb91334d299abc59ff57a90fb2193d8ffa5886f1" dependencies = [ "bstr", "gix-command", @@ -1160,9 +1169,9 @@ dependencies = [ [[package]] name = "gix-traverse" -version = "0.60.0" +version = "0.61.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008c5cd879e46e86b5c2469e633611978b18775d53d05668d691bc13088bd409" +checksum = "9af3503668739f4de5dba57fe15cfd9adc443b08bcbbf195e5de16b648872245" dependencies = [ "bitflags 2.13.1", "gix-commitgraph", @@ -1177,9 +1186,9 @@ dependencies = [ [[package]] name = "gix-url" -version = "0.37.0" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d10e53b8eae21ee601687f47bbbd6cb2ed7162cb4c1cafdd422fb7ec64cbee" +checksum = "5bda80ccb4bc81fb9fb07a975916eb0c7a889bcbcb0c8a239c3f82a9cc2abdda" dependencies = [ "bstr", "gix-path", @@ -1190,9 +1199,9 @@ dependencies = [ [[package]] name = "gix-utils" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1795bd2a970ca8b2185318c2abb97d955c71992f1cf28de73ad3b593a9f3ce8" +checksum = "0da1c46491b49458a446cc76f0085860f8164c2290742e0aa8c653ce67240a97" dependencies = [ "bstr", "fastrand", @@ -1202,18 +1211,18 @@ dependencies = [ [[package]] name = "gix-validate" -version = "0.11.3" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a034e84d1e04e1b1f20f51f12491da230b6ac8b925d0c8e1b89bcd87a7c5ccc" +checksum = "4dae8780f63ed8a803b8bdabbd7aa5f5c5d74592c8b50eed875c1bb4f6545a6a" dependencies = [ "bstr", ] [[package]] name = "gix-worktree" -version = "0.55.0" +version = "0.56.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31eb8e675122e83585e461fe28f68ff8c5ed55b49017b697e7e76423ff973424" +checksum = "3ed36b627476e2072c129900a17c977a38052b5497e27308159bc881fbf4eecf" dependencies = [ "bstr", "gix-attributes", @@ -1230,9 +1239,9 @@ dependencies = [ [[package]] name = "gix-worktree-stream" -version = "0.35.0" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b088c8724e7be120c4798dd86925cf05332c9d356a463542578600c50c7a549" +checksum = "de5ec0ccbab39c9994656ea031ded8ffd90b370a3cc8e360fbe3c4ebe7c64964" dependencies = [ "gix-attributes", "gix-error", @@ -1842,12 +1851,6 @@ dependencies = [ "sha1", ] -[[package]] -name = "shell-words" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" - [[package]] name = "shlex" version = "2.0.1" diff --git a/Cargo.toml b/Cargo.toml index b106b78..8e8b78e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,7 @@ clap = { version = "4.6.3", features = ["derive"] } colored = "3.1.1" dunce = "1.0.5" fixedbitset = "0.5.7" -gix = { version = "0.86.0", default-features = false, features = ["blob-diff", "max-performance-safe", "revision", "sha1"] } +gix = { version = "0.87.1", default-features = false, features = ["blob-diff", "max-performance-safe", "revision", "sha1"] } globset = "0.4.19" ignore = "0.4.31" lasso = { version = "0.7.3", features = ["multi-threaded"] } diff --git a/README.md b/README.md index cb28e23..a2379b5 100644 --- a/README.md +++ b/README.md @@ -440,6 +440,7 @@ walking up from the analysis root to the nearest directory containing one { "aggressive": true, "roots": ["MyApp.Program.Main"], + "entryPoints": ["Jobs/", "Api/Handlers/**/*.cs"], "libraryProjects": ["MyLib"], "ignore": ["Migrations/**", "Generated/", "**/*.designer.cs"], "deadCode": { @@ -471,7 +472,11 @@ matches the whole directory, so `"Generated/"` needs no `**`). `aggressive`, `roots`, and `libraryProjects` set defaults for the matching CLI flags: an explicit `--aggressive`, `--root`, or `--library` always wins, otherwise the config's value applies, otherwise the built-in default (`false` / no extra -roots / no extra library projects). The same `ignore` list also applies to +roots / no extra library projects). `entryPoints` globs are the file-level +counterpart to `roots`: every declaration in a matching file is treated as +an entry point, so the file and everything it references count as used — +unlike an `ignore` glob, which only drops the findings in the matching file +itself. The same `ignore` list also applies to `roe dupes`, since a duplicate spans multiple files and doesn't map cleanly onto a single-line inline suppression comment. Each command's section takes an `ignore` list of its own with the same rules, unioned with the top-level diff --git a/docs/commands/dead-code.mdx b/docs/commands/dead-code.mdx index 5d95648..39b2dc4 100644 --- a/docs/commands/dead-code.mdx +++ b/docs/commands/dead-code.mdx @@ -51,8 +51,12 @@ roe dead-code --library MyLib ``` Both can also be set persistently in a [config file](/configuration). A -config's `deadCode.ignore` glob list drops every dead-code finding in -matching files without affecting the other commands — see +config's `entryPoints` glob list roots whole files rather than single +symbols — every declaration in a matching file counts as used, along with +everything it references — see +[Entry-point files](/configuration#entry-point-files). And a config's +`deadCode.ignore` glob list drops every dead-code finding in matching files +without affecting the other commands — see [Suppressing findings](/suppressing-findings). ## What counts as an entry point diff --git a/docs/concepts/entry-points.mdx b/docs/concepts/entry-points.mdx index 6d0079e..f2f57d4 100644 --- a/docs/concepts/entry-points.mdx +++ b/docs/concepts/entry-points.mdx @@ -46,6 +46,23 @@ roe dead-code --root App.Jobs.NightlyCleanupJob } ``` +When a whole file is consumed in ways roe can't see — a plugin host loading +it by path, an external tool reading it directly — name the file with the +`entryPoints` config field instead of listing every symbol in it. Patterns +are globs resolved relative to the config file's directory (a trailing `/` +matches the whole directory); every declaration in a matching file becomes +a root, so the file and everything it references count as used: + +```json roe.json +{ + "entryPoints": ["Plugins/", "Jobs/**/*.cs"] +} +``` + +A pattern that matches no file at all is reported as a note, so a stale +path can't silently drop the protection it promised. See +[Configuration](/configuration#entry-point-files) for the full rules. + ## How detection works Under the hood, `roe dead-code` runs a five-step pipeline: diff --git a/docs/configuration.mdx b/docs/configuration.mdx index cd6817e..9192325 100644 --- a/docs/configuration.mdx +++ b/docs/configuration.mdx @@ -25,6 +25,7 @@ error. { "aggressive": true, "roots": ["MyApp.Program.Main"], + "entryPoints": ["Jobs/", "Api/Handlers/**/*.cs"], "libraryProjects": ["MyLib"], "ignore": ["Migrations/**", "Generated/", "**/*.designer.cs"], "deadCode": { @@ -55,6 +56,7 @@ error. | --- | --- | --- | | `aggressive` | boolean | Also flag enum members and public settable auto-properties. Default for the `--aggressive` flag. | | `roots` | string[] | Fully-qualified symbol names to treat as extra entry-point roots. Default for `--root`. | +| `entryPoints` | string[] | Glob patterns naming files to treat as entry points: every declaration in a matching file is a root, so the file and everything it references count as used. | | `libraryProjects` | string[] | Project names to always treat in library mode (public API counts as used). Default for `--library`. | | `ignore` | string[] | Glob patterns; every finding in a matching file is dropped. Applies to all three commands; each command's section can add its own. | | `deadCode` | object | Extra `ignore` globs applied only to [`roe dead-code`](/commands/dead-code). | @@ -110,6 +112,31 @@ passing `--baseline` is also what makes a bare `roe` and its own. A path that doesn't exist is a hard error, not a silent full-backlog run. +## Entry-point files + +`entryPoints` is the file-level counterpart to `roots`: where `roots` names +one symbol by its fully-qualified name, an entry-point glob keeps a whole +file's contents alive. Every declaration in a matching file becomes a root, +so the file is never flagged and everything it references counts as used — +unlike an `ignore` glob, which only drops the findings in the matching file +itself. + +Use it for files that are consumed in ways roe can't see: a plugin host +loading them by path, string-based reflection, or an external tool reading +the file directly. + +```json roe.json +{ + "entryPoints": ["Jobs/", "Api/Handlers/**/*.cs"] +} +``` + +Patterns resolve relative to the config file's own directory with the same +rules as `ignore` globs — a trailing `/` matches the whole directory, and +`..` is unsupported. A pattern that matches no file at all is reported as a +note in the output, so a stale path can't silently drop the protection it +promised. + ## Ignore globs `ignore` patterns are resolved relative to the config file's own directory. @@ -168,9 +195,9 @@ Dupes settings follow the same per-field order: `--min-tokens 100` overrides carries a value, so unlike `--aggressive` and `--exclude-tests` it has no on-only nuance — `--mode exact` overrides a config's `"semantic"` cleanly. -Ignore lists sit outside this order entirely: they have no CLI flag, and a -command's own `ignore` list is unioned with the top-level one rather than -replacing it. +Ignore lists and `entryPoints` sit outside this order entirely: neither has +a CLI flag, and a command's own `ignore` list is unioned with the top-level +one rather than replacing it. To confirm a config-only `excludeTests` or `ignore` actually applied, read the `roe health` footer: the scanned counts narrow by whatever was excluded, and diff --git a/skills/roe/SKILL.md b/skills/roe/SKILL.md index 6713e27..821c1d6 100644 --- a/skills/roe/SKILL.md +++ b/skills/roe/SKILL.md @@ -154,8 +154,9 @@ The `kind` values (identical to the inline-suppression rule names): Prefer fixing real findings over suppressing. When a finding genuinely is a false positive (e.g. code invoked via string-based reflection), first -consider whether `--root`/`roots` describes the situation better — a root -documents *why* the code is alive, a suppression just silences the report. +consider whether `--root`/`roots` (or `entryPoints` for a whole file) +describes the situation better — a root documents *why* the code is alive, +a suppression just silences the report. Inline comments work eslint-style, checked after the first `//` (so `///` doc-comments work too): @@ -188,6 +189,7 @@ fields are a hard error, so typos fail loudly. { "aggressive": false, "roots": ["App.Jobs.NightlyCleanupJob"], + "entryPoints": ["Plugins/", "Jobs/**/*.cs"], "libraryProjects": ["App.Sdk"], "ignore": ["Migrations/**", "Generated/"], "deadCode": { @@ -228,8 +230,16 @@ Precedence is CLI flag → config → built-in default, with gotchas: - `--root` and `--library` **replace** the config's `roots`/`libraryProjects` lists entirely rather than merging with them. - The `dupes` settings are plain per-field overrides (`--mode exact` beats a - config `"semantic"`), and ignore lists have no CLI flag at all — scoped - lists only ever add to the top-level one. + config `"semantic"`), and ignore lists and `entryPoints` have no CLI flag + at all — scoped ignore lists only ever add to the top-level one. + +`entryPoints` is the file-level counterpart to `roots`: globs (resolved +relative to the config file's directory, trailing `/` means the whole +subtree) whose matching files are treated as entry points — every +declaration in them is a root, so the file and everything it references +count as used. Prefer it over `deadCode.ignore` for plugin/reflection-loaded +files: an ignore only hides the file's own findings, an entry point also +keeps alive what the file uses. ## Adopting roe on a legacy codebase diff --git a/src/commands/dead_code.rs b/src/commands/dead_code.rs index 49076a7..6e44c8a 100644 --- a/src/commands/dead_code.rs +++ b/src/commands/dead_code.rs @@ -15,14 +15,18 @@ pub struct Analysis { /// The full dead-code pipeline: discover → extract → symbol table → kill /// list → entry points → graph → reachability → detectors → inline -/// suppressions. +/// suppressions. `entry_points` globs resolve against the workspace root; +/// the config-aware path through [`execute`] resolves them against the +/// config file's own directory instead. pub fn analyze( root: &Path, aggressive: bool, manual_roots: &[String], library_projects: &[String], + entry_points: &[String], ) -> anyhow::Result { let extracted = commands::Extracted::build(root)?; + let entry_point_dir = extracted.workspace.root.clone(); analyze_extracted( &extracted, @@ -30,6 +34,8 @@ pub fn analyze( aggressive, manual_roots, library_projects, + entry_points, + &entry_point_dir, ) } @@ -42,6 +48,8 @@ pub(crate) fn analyze_extracted( aggressive: bool, manual_roots: &[String], library_projects: &[String], + entry_points: &[String], + entry_point_dir: &Path, ) -> anyhow::Result { let mut workspace = extracted.workspace.clone(); let rodeo = &extracted.rodeo; @@ -51,7 +59,7 @@ pub(crate) fn analyze_extracted( rules::apply_kill_list(&mut resolution, &workspace, rodeo, aggressive); - let notes = entry_points::mark_roots( + let mut notes = entry_points::mark_roots( &mut resolution, &workspace, facts, @@ -60,6 +68,14 @@ pub(crate) fn analyze_extracted( rodeo, ); + let entry_point_globs = + config::build_entry_point_globs(entry_point_dir, entry_points, &mut workspace.warnings); + notes.extend(entry_points::mark_entry_point_files( + &mut resolution, + &workspace, + &entry_point_globs, + )); + let symbol_graph = graph::build_graph(&mut resolution, &workspace, facts, rodeo); let roots: Vec = resolution .symbols @@ -109,12 +125,25 @@ pub(crate) fn execute_extracted( &args.library_projects, ); + // `entryPoints` has no CLI flag (like `ignore`), so it comes straight + // from the config, and its globs resolve against the config file's own + // directory the way `ignore` globs do. + let (entry_points, entry_point_dir) = match context.config.as_ref() { + Some(resolved) => ( + resolved.config.entry_points.clone().unwrap_or_default(), + resolved.dir.clone(), + ), + None => (Vec::new(), context.root.clone()), + }; + let mut analysis = analyze_extracted( extracted, extracted.started, effective.aggressive, &effective.roots, &effective.library_projects, + &entry_points, + &entry_point_dir, )?; analysis .workspace diff --git a/src/config.rs b/src/config.rs index 843de6a..a962dce 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,7 +1,7 @@ use std::path::{Path, PathBuf}; use anyhow::{Context, bail}; -use globset::{Glob, GlobSet, GlobSetBuilder}; +use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder}; use serde::Deserialize; use crate::cli::DupeMode; @@ -13,6 +13,14 @@ use crate::cli::DupeMode; pub struct RoeConfig { pub aggressive: Option, pub roots: Option>, + /// Glob patterns (relative to this config file's directory, resolved + /// with the same rules as `ignore`) naming files to treat as entry + /// points: every declaration in a matching file is a root, so the file + /// and everything it references count as used. For files consumed in + /// ways roe can't see — plugin hosts, reflection loaders, source + /// generators reading the file by path. + #[serde(rename = "entryPoints")] + pub entry_points: Option>, /// Project names to always treat in library mode (public API is used), /// regardless of executables elsewhere in the workspace. #[serde(rename = "libraryProjects")] @@ -403,21 +411,9 @@ pub(crate) fn build_ignore_globset( let mut builder = GlobSetBuilder::new(); let mut any = false; for pattern in patterns { - if pattern.contains("..") { - warnings.push(format!("unsupported ignore glob with '..': {pattern}")); - continue; - } - let expanded = match pattern.strip_suffix('/') { - Some(dir) => format!("{dir}/**"), - None => pattern.clone(), - }; - let absolute = format!("{}/{}", config_dir.display(), expanded); - match Glob::new(&absolute) { - Ok(glob) => { - builder.add(glob); - any = true; - } - Err(error) => warnings.push(format!("invalid ignore glob {pattern}: {error}")), + if let Some(glob) = build_config_glob(config_dir, pattern, "ignore", warnings) { + builder.add(glob); + any = true; } } if !any { @@ -426,6 +422,69 @@ pub(crate) fn build_ignore_globset( builder.build().ok() } +/// A single compiled `entryPoints` pattern. Kept per-pattern rather than +/// merged into one `GlobSet` so a pattern that matches no file at all can be +/// reported on its own. +pub struct EntryPointGlob { + pub matcher: GlobMatcher, + pub pattern: String, +} + +/// Compiles `entryPoints` glob patterns against `config_dir` (the config +/// file's own directory) with the same resolution rules `ignore` globs use. +/// A pattern that doesn't compile is skipped with a warning. +pub fn build_entry_point_globs( + config_dir: &Path, + patterns: &[String], + warnings: &mut Vec, +) -> Vec { + patterns + .iter() + .filter_map(|pattern| { + build_config_glob(config_dir, pattern, "entryPoints", warnings).map(|glob| { + EntryPointGlob { + matcher: glob.compile_matcher(), + pattern: pattern.clone(), + } + }) + }) + .collect() +} + +/// Expands one config-relative glob pattern into an absolute `Glob` with the +/// rules every config glob list shares: patterns resolve against the config +/// file's own directory, a trailing slash on a pattern reads as "this whole +/// directory" without the user having to spell out `**` themselves, and `..` +/// is unsupported. `label` names the config field so a warning points at the +/// list that holds the bad pattern. +fn build_config_glob( + config_dir: &Path, + pattern: &str, + label: &str, + warnings: &mut Vec, +) -> Option { + if pattern.contains("..") { + warnings.push(format!("unsupported {label} glob with '..': {pattern}")); + + return None; + } + + let expanded = match pattern.strip_suffix('/') { + Some(directory) => format!("{directory}/**"), + None => pattern.to_string(), + }; + let absolute = format!("{}/{}", config_dir.display(), expanded); + + match Glob::new(&absolute) { + Ok(glob) => Some(glob), + Err(error) => { + warnings.push(format!("invalid {label} glob {pattern}: {error}")); + + None + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -890,6 +949,50 @@ mod tests { assert!(result.expect("discover should succeed").is_none()); } + #[test] + fn parses_entry_points() { + let config: RoeConfig = + serde_json::from_str(r#"{"entryPoints": ["Plugins/"]}"#).expect("valid json"); + assert_eq!(config.entry_points, Some(vec!["Plugins/".to_string()])); + + let config: RoeConfig = + serde_yaml_ng::from_str("entryPoints:\n - \"Jobs/**/*.cs\"\n").expect("valid yaml"); + assert_eq!(config.entry_points, Some(vec!["Jobs/**/*.cs".to_string()])); + } + + #[test] + fn entry_point_glob_trailing_slash_matches_whole_directory() { + let mut warnings = Vec::new(); + let globs = + build_entry_point_globs(Path::new("/repo"), &["Plugins/".to_string()], &mut warnings); + + assert_eq!(globs.len(), 1); + assert_eq!(globs[0].pattern, "Plugins/"); + assert!( + globs[0] + .matcher + .is_match(Path::new("/repo/Plugins/Nested/File.cs")) + ); + assert!(!globs[0].matcher.is_match(Path::new("/repo/Other/File.cs"))); + assert!(warnings.is_empty()); + } + + #[test] + fn entry_point_glob_with_dot_dot_is_skipped_with_a_warning() { + let mut warnings = Vec::new(); + let globs = build_entry_point_globs( + Path::new("/repo"), + &["../outside/**".to_string()], + &mut warnings, + ); + + assert!(globs.is_empty()); + assert_eq!( + warnings, + vec!["unsupported entryPoints glob with '..': ../outside/**".to_string()] + ); + } + #[test] fn ignore_glob_trailing_slash_matches_whole_directory() { let mut warnings = Vec::new(); diff --git a/src/entry_points.rs b/src/entry_points.rs index f80ab92..ee08efd 100644 --- a/src/entry_points.rs +++ b/src/entry_points.rs @@ -1,3 +1,4 @@ +use crate::config::EntryPointGlob; use crate::extract::Interner; use crate::extract::FileFacts; @@ -194,6 +195,46 @@ pub fn mark_roots( notes } +/// Root every declaration in the files the config's `entryPoints` globs +/// match, so each matching file and everything it references count as used. +/// This is the file-level counterpart to `roots`: `roots` names one symbol, +/// an entry-point file keeps its whole contents alive. Returns notes to +/// surface in the report (a pattern that matches no file at all). +pub fn mark_entry_point_files( + resolution: &mut Resolution, + workspace: &Workspace, + entry_point_globs: &[EntryPointGlob], +) -> Vec { + let mut notes = Vec::new(); + let mut matched_files = vec![false; workspace.files.len()]; + + for glob in entry_point_globs { + let mut matched = false; + + for file in &workspace.files { + if glob.matcher.is_match(&file.path) { + matched_files[file.id.index()] = true; + matched = true; + } + } + + if !matched { + notes.push(format!( + "entryPoints {}: no matching file found", + glob.pattern + )); + } + } + + for symbol in &mut resolution.symbols { + if matched_files[symbol.file.index()] { + symbol.flags |= SymbolFlags::ROOT; + } + } + + notes +} + fn is_controller(symbol: &crate::resolve::Symbol, rodeo: &Interner) -> bool { if !symbol.kind.is_type() { return false; diff --git a/tests/cli.rs b/tests/cli.rs index 75cc18e..c182f93 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -108,6 +108,36 @@ fn config_ignore_yaml_filters_out_ignored_folder() { assert!(!stdout.contains("Ignored")); } +#[test] +fn config_entry_points_json_roots_matching_files_and_what_they_reference() { + // Plugins/CustomPlugin.cs is only alive through the config's + // `entryPoints` glob, and PluginHelper is only alive through the plugin + // — proving an entry-point file keeps its references alive, which a + // plain `ignore` glob would not. + let output = roe() + .args(["dead-code", &fixture("config_entry_points_json")]) + .output() + .expect("command runs"); + assert_eq!(output.status.code(), Some(1)); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("StillDead.cs")); + assert!(!stdout.contains("Plugins")); + assert!(!stdout.contains("PluginHelper")); +} + +#[test] +fn config_entry_points_yaml_roots_matching_files_and_what_they_reference() { + let output = roe() + .args(["dead-code", &fixture("config_entry_points_yaml")]) + .output() + .expect("command runs"); + assert_eq!(output.status.code(), Some(1)); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("StillDead.cs")); + assert!(!stdout.contains("Plugins")); + assert!(!stdout.contains("PluginHelper")); +} + #[test] fn dead_code_scoped_ignore_json_drops_findings_in_matching_files() { let output = roe() diff --git a/tests/dead_code.rs b/tests/dead_code.rs index 740afc0..78dc4e7 100644 --- a/tests/dead_code.rs +++ b/tests/dead_code.rs @@ -11,7 +11,8 @@ fn fixture(name: &str) -> PathBuf { /// Sorted (kind, name) pairs for stable assertions. fn findings(name: &str, aggressive: bool) -> Vec<(FindingKind, String)> { - let analysis = analyze(&fixture(name), aggressive, &[], &[]).expect("analysis should succeed"); + let analysis = + analyze(&fixture(name), aggressive, &[], &[], &[]).expect("analysis should succeed"); let mut pairs: Vec<(FindingKind, String)> = analysis .result .findings @@ -131,6 +132,7 @@ fn library_projects_flag_protects_a_named_project_despite_a_sibling_executable() false, &[], &["Lib".to_string()], + &[], ) .expect("analysis should succeed"); assert!(analysis.result.findings.is_empty()); @@ -211,6 +213,7 @@ fn manual_roots_rescue_symbols() { false, &["ConsoleApp.ConsoleGreeter.UnusedHelper".to_string()], &[], + &[], ) .expect("analysis should succeed"); let names: Vec<&str> = analysis @@ -222,9 +225,54 @@ fn manual_roots_rescue_symbols() { assert!(!names.contains(&"ConsoleApp.ConsoleGreeter.UnusedHelper")); } +#[test] +fn entry_point_files_root_every_declaration_they_contain() { + let analysis = analyze( + &fixture("console_app"), + false, + &[], + &[], + &["ConsoleApp/DeadClass.cs".to_string()], + ) + .expect("analysis should succeed"); + let mut pairs: Vec<(FindingKind, String)> = analysis + .result + .findings + .iter() + .map(|f| (f.kind, f.name.clone())) + .collect(); + pairs.sort_by(|a, b| a.1.cmp(&b.1)); + + assert_findings( + pairs, + vec![member("ConsoleApp.ConsoleGreeter.UnusedHelper")], + ); +} + +#[test] +fn an_entry_point_glob_matching_no_file_surfaces_a_note() { + let analysis = analyze( + &fixture("console_app"), + false, + &[], + &[], + &["Plugins/**".to_string()], + ) + .expect("analysis should succeed"); + + assert!( + analysis + .result + .notes + .contains(&"entryPoints Plugins/**: no matching file found".to_string()), + "got notes {:?}", + analysis.result.notes + ); +} + #[test] fn nonexistent_path_errors() { - assert!(analyze(&fixture("does_not_exist"), false, &[], &[]).is_err()); + assert!(analyze(&fixture("does_not_exist"), false, &[], &[], &[]).is_err()); } #[test] diff --git a/tests/fixtures/config_entry_points_json/PluginHelper.cs b/tests/fixtures/config_entry_points_json/PluginHelper.cs new file mode 100644 index 0000000..fd5904c --- /dev/null +++ b/tests/fixtures/config_entry_points_json/PluginHelper.cs @@ -0,0 +1,8 @@ +namespace ConfigEntryPoints; + +internal class PluginHelper +{ + public void Help() + { + } +} diff --git a/tests/fixtures/config_entry_points_json/Plugins/CustomPlugin.cs b/tests/fixtures/config_entry_points_json/Plugins/CustomPlugin.cs new file mode 100644 index 0000000..0089c8c --- /dev/null +++ b/tests/fixtures/config_entry_points_json/Plugins/CustomPlugin.cs @@ -0,0 +1,10 @@ +namespace ConfigEntryPoints; + +internal class CustomPlugin +{ + public void Run() + { + var helper = new PluginHelper(); + helper.Help(); + } +} diff --git a/tests/fixtures/config_entry_points_json/Program.cs b/tests/fixtures/config_entry_points_json/Program.cs new file mode 100644 index 0000000..596b0a0 --- /dev/null +++ b/tests/fixtures/config_entry_points_json/Program.cs @@ -0,0 +1,8 @@ +namespace ConfigEntryPoints; + +public static class Program +{ + public static void Main() + { + } +} diff --git a/tests/fixtures/config_entry_points_json/StillDead.cs b/tests/fixtures/config_entry_points_json/StillDead.cs new file mode 100644 index 0000000..70df89d --- /dev/null +++ b/tests/fixtures/config_entry_points_json/StillDead.cs @@ -0,0 +1,8 @@ +namespace ConfigEntryPoints; + +internal class StillDead +{ + public void DoNothing() + { + } +} diff --git a/tests/fixtures/config_entry_points_json/roe.json b/tests/fixtures/config_entry_points_json/roe.json new file mode 100644 index 0000000..0a7483a --- /dev/null +++ b/tests/fixtures/config_entry_points_json/roe.json @@ -0,0 +1,3 @@ +{ + "entryPoints": ["Plugins/"] +} diff --git a/tests/fixtures/config_entry_points_yaml/PluginHelper.cs b/tests/fixtures/config_entry_points_yaml/PluginHelper.cs new file mode 100644 index 0000000..fd5904c --- /dev/null +++ b/tests/fixtures/config_entry_points_yaml/PluginHelper.cs @@ -0,0 +1,8 @@ +namespace ConfigEntryPoints; + +internal class PluginHelper +{ + public void Help() + { + } +} diff --git a/tests/fixtures/config_entry_points_yaml/Plugins/CustomPlugin.cs b/tests/fixtures/config_entry_points_yaml/Plugins/CustomPlugin.cs new file mode 100644 index 0000000..0089c8c --- /dev/null +++ b/tests/fixtures/config_entry_points_yaml/Plugins/CustomPlugin.cs @@ -0,0 +1,10 @@ +namespace ConfigEntryPoints; + +internal class CustomPlugin +{ + public void Run() + { + var helper = new PluginHelper(); + helper.Help(); + } +} diff --git a/tests/fixtures/config_entry_points_yaml/Program.cs b/tests/fixtures/config_entry_points_yaml/Program.cs new file mode 100644 index 0000000..596b0a0 --- /dev/null +++ b/tests/fixtures/config_entry_points_yaml/Program.cs @@ -0,0 +1,8 @@ +namespace ConfigEntryPoints; + +public static class Program +{ + public static void Main() + { + } +} diff --git a/tests/fixtures/config_entry_points_yaml/StillDead.cs b/tests/fixtures/config_entry_points_yaml/StillDead.cs new file mode 100644 index 0000000..70df89d --- /dev/null +++ b/tests/fixtures/config_entry_points_yaml/StillDead.cs @@ -0,0 +1,8 @@ +namespace ConfigEntryPoints; + +internal class StillDead +{ + public void DoNothing() + { + } +} diff --git a/tests/fixtures/config_entry_points_yaml/roe.yaml b/tests/fixtures/config_entry_points_yaml/roe.yaml new file mode 100644 index 0000000..1c9076d --- /dev/null +++ b/tests/fixtures/config_entry_points_yaml/roe.yaml @@ -0,0 +1,2 @@ +entryPoints: + - "Plugins/"