diff --git a/Cargo.lock b/Cargo.lock index f23bae4..1b8ac78 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,6 +20,15 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +[[package]] +name = "borsh" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" +dependencies = [ + "cfg_aliases", +] + [[package]] name = "bpaf" version = "0.9.20" @@ -69,11 +78,10 @@ dependencies = [ "cargo_toml", "insta", "mimalloc-safe", - "proc-macro2", + "ra_ap_syntax", "rayon", - "rustc-hash", + "rustc-hash 2.1.1", "serde_json", - "syn", "tempfile", "toml_edit", "walkdir", @@ -135,6 +143,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "cmake" version = "0.1.54" @@ -156,6 +170,21 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "countme" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -192,6 +221,12 @@ dependencies = [ "syn", ] +[[package]] +name = "drop_bomb" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bda8e21c04aca2ae33ffc2fd8c23134f3cac46db123ba97bd9d3f3b8a4a85e1" + [[package]] name = "either" version = "1.15.0" @@ -264,6 +299,12 @@ dependencies = [ "wasip2", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.16.1" @@ -379,7 +420,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.1", ] [[package]] @@ -393,12 +434,27 @@ dependencies = [ "similar", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jod-thread" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a037eddb7d28de1d0fc42411f501b53b75838d313908078d6698d064f3029b24" + [[package]] name = "libc" version = "0.2.177" @@ -434,6 +490,15 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "mimalloc-safe" version = "0.1.55" @@ -443,6 +508,15 @@ dependencies = [ "libmimalloc-sys2", ] +[[package]] +name = "miow" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "536bfad37a309d62069485248eeaba1e8d9853aaf951caaeaed0585a95346f08" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -473,6 +547,12 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + [[package]] name = "potential_utf" version = "0.1.4" @@ -506,6 +586,71 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "ra-ap-rustc_lexer" +version = "0.137.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac80365383a3c749f38af567fdcfaeff3fa6ea5df3846852abbce73e943921b9" +dependencies = [ + "memchr", + "unicode-properties", + "unicode-xid", +] + +[[package]] +name = "ra_ap_edition" +version = "0.0.307" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "827b826e17647842049aa07bb4f75d78516ddcb32d651307de6b2891d9349396" + +[[package]] +name = "ra_ap_parser" +version = "0.0.307" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2876e07c278c85cb458f0fe1a9679a48d561a95868e67b465fb328458506867" +dependencies = [ + "drop_bomb", + "ra-ap-rustc_lexer", + "ra_ap_edition", + "rustc-literal-escaper", + "tracing", + "winnow", +] + +[[package]] +name = "ra_ap_stdx" +version = "0.0.307" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd94073132b765585a905d68352e5a9a0513d66e0da7186892cebc033fd347ee" +dependencies = [ + "crossbeam-channel", + "crossbeam-utils", + "itertools", + "jod-thread", + "libc", + "miow", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "ra_ap_syntax" +version = "0.0.307" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17d3d6c19beb665511d026e513134b053282ec38e45d74f6a7a8287f79de90bc" +dependencies = [ + "either", + "itertools", + "ra_ap_parser", + "ra_ap_stdx", + "rowan", + "rustc-hash 2.1.1", + "rustc-literal-escaper", + "smol_str", + "tracing", + "triomphe", +] + [[package]] name = "rayon" version = "1.11.0" @@ -526,12 +671,37 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rowan" +version = "0.15.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a58fa8a7ccff2aec4f39cc45bf5f985cec7125ab271cf681c279fd00192b49" +dependencies = [ + "countme", + "hashbrown 0.14.5", + "memoffset", + "rustc-hash 1.1.0", + "text-size", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc-hash" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[package]] +name = "rustc-literal-escaper" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab03008eb631b703dd16978282ae36c73282e7922fe101a4bd072a40ecea7b8b" + [[package]] name = "rustix" version = "1.1.2" @@ -662,6 +832,16 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "smol_str" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3498b0a27f93ef1402f20eefacfaa1691272ac4eca1cdc8c596cb0a245d6cbf5" +dependencies = [ + "borsh", + "serde_core", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -703,6 +883,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "text-size" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233" + [[package]] name = "thiserror" version = "2.0.17" @@ -785,6 +971,31 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", +] + +[[package]] +name = "triomphe" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd69c5aa8f924c7519d6372789a74eac5b94fb0f8fcf0d4a97eb0bfc3e785f39" + [[package]] name = "typeid" version = "1.0.3" @@ -797,6 +1008,12 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + [[package]] name = "unicode-xid" version = "0.2.6" @@ -861,7 +1078,16 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", ] [[package]] @@ -879,14 +1105,31 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "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", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[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 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -895,48 +1138,96 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[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.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[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.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[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.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[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.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[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.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[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.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[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.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[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.13" diff --git a/Cargo.toml b/Cargo.toml index e371b65..dca5e86 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,12 +23,7 @@ walkdir = "2.5.0" cargo_metadata = "0.23.0" cargo_toml = "0.22.1" bpaf = { version = "0.9.19", features = ["derive", "batteries"] } -proc-macro2 = { version = "1.0.94", features = ["span-locations"] } -syn = { version = "2.0.100", features = [ - "full", - "visit", - "extra-traits", # add "extra-traits" to debug syn ast -] } +ra_ap_syntax = "0.0.307" rayon = "1.10.0" toml_edit = { version = "0.23.0", features = ["parse"] } anyhow = "1.0.97" diff --git a/README.md b/README.md index 7bcf826..02f9aed 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ cargo shear --fix > [!IMPORTANT] > `cargo shear` cannot detect "hidden" imports from macro expansions without the `--expand` flag (nightly only). -> This is because `cargo shear` uses `syn` to parse files and does not expand macros by default. +> This is because `cargo shear` uses rust-analyzer's parser to parse files and does not expand macros by default. To expand macros: @@ -97,7 +97,7 @@ GitHub Actions Job Example: 1. use the `cargo_metadata` crate to list all dependencies specified in `[workspace.dependencies]` and `[dependencies]` 2. iterate through all package targets (`lib`, `bin`, `example`, `test` and `bench`) to locate all Rust files -3. use `syn` to parse these Rust files and extract imports +3. use rust-analyzer's parser (`ra_ap_syntax`) to parse these Rust files and extract imports - alternatively, use the `--expand` option with `cargo expand` to first expand macros and then parse the expanded code (though this is significantly slower). 4. find the difference between the imports and the package dependencies diff --git a/src/dependency_analyzer.rs b/src/dependency_analyzer.rs index b97153d..9519119 100644 --- a/src/dependency_analyzer.rs +++ b/src/dependency_analyzer.rs @@ -3,7 +3,7 @@ //! This module is responsible for analyzing Rust source code to determine //! which dependencies are actually used. It supports two modes: //! -//! 1. **Normal mode**: Parses Rust source files directly using `syn` +//! 1. **Normal mode**: Parses Rust source files directly using `ra_ap_syntax` //! 2. **Expand mode**: Uses `cargo expand` to expand macros for more accurate detection //! //! The analyzer walks through all source files in a package, collects import @@ -12,7 +12,6 @@ use std::{ env, ffi::OsString, - fmt::Write, path::{Path, PathBuf}, process::Command, }; @@ -156,18 +155,7 @@ impl DependencyAnalyzer { )); } - let imports = collect_imports(&output_str).map_err(|err| { - let location = err.span().start(); - let snippet = Self::extract_code_snippet(&output_str, location.line); - - anyhow!( - "Syntax error in {} at line {}:{}:\n{err}\n{snippet}", - target.name, - location.line, - location.column - ) - })?; - + let imports = collect_imports(&output_str); Self::categorize_imports(&mut categorized, target_kind, imports); } @@ -221,40 +209,7 @@ impl DependencyAnalyzer { /// Parse a Rust source file and collect all import names. fn process_rust_source(path: &Path) -> Result> { let source_text = std::fs::read_to_string(path)?; - collect_imports(&source_text).map_err(|err| { - let location = err.span().start(); - let snippet = Self::extract_code_snippet(&source_text, location.line); - - anyhow!( - "Syntax error in {} at line {}:{}:\n{err}\n{snippet}", - path.display(), - location.line, - location.column - ) - }) - } - - /// Extracts a snippet of code around the specified line number. - fn extract_code_snippet(source: &str, location: usize) -> String { - let lines: Vec<&str> = source.lines().collect(); - let total = lines.len(); - - if location == 0 || location > total { - return String::new(); - } - - // Try and show 3 lines of context before/after the location - let start = location.saturating_sub(4); - let end = (location + 3).min(total); - - let mut snippet = String::from("\n"); - for (index, line) in lines.iter().enumerate().skip(start).take(end - start) { - let line_num = index + 1; - let marker = if line_num == location { ">" } else { " " }; - let _ = writeln!(snippet, "{marker} {line_num:4} | {line}"); - } - - snippet + Ok(collect_imports(&source_text)) } /// Collect import names for dependencies referenced in features. diff --git a/src/import_collector.rs b/src/import_collector.rs index 9c45d03..6d6b977 100644 --- a/src/import_collector.rs +++ b/src/import_collector.rs @@ -1,6 +1,6 @@ //! Import statement collector for cargo-shear. //! -//! This module parses Rust source code using `syn` to extract all import +//! This module parses Rust source code using `ra_ap_syntax` to extract all import //! statements and references to external crates. It handles various forms //! of imports including: //! @@ -10,8 +10,11 @@ //! - Macro invocations //! - Attribute references (e.g., `#[derive(...)]`) +use ra_ap_syntax::{ + AstNode, Edition, SourceFile, SyntaxKind, SyntaxNode, WalkEvent, + ast::{Attr, ExternCrate, MacroCall, MacroRules, Path, TokenTree, Use, UseTree}, +}; use rustc_hash::FxHashSet; -use syn::{self, ext::IdentExt, spanned::Spanned}; /// Collect all import statements and crate references from Rust source code. /// @@ -25,16 +28,13 @@ use syn::{self, ext::IdentExt, spanned::Spanned}; /// # Returns /// /// A set of crate names that are referenced in the source code -pub fn collect_imports(source_text: &str) -> syn::Result> { +pub fn collect_imports(source_text: &str) -> FxHashSet { collect_imports_internal(source_text, true) } -fn collect_imports_internal( - source_text: &str, - include_doc_code: bool, -) -> syn::Result> { - let syntax = syn::parse_str::(source_text)?; - let mut deps = collect_from_syntax(&syntax, include_doc_code); +fn collect_imports_internal(source_text: &str, include_doc_code: bool) -> FxHashSet { + let syntax = SourceFile::parse(source_text, Edition::CURRENT); + let mut deps = collect_from_syntax(&syntax.tree(), include_doc_code); if include_doc_code { for block in gather_doc_blocks(source_text) { @@ -48,10 +48,10 @@ fn collect_imports_internal( } } - Ok(deps) + deps } -fn collect_from_syntax(syntax: &syn::File, include_doc_code: bool) -> FxHashSet { +fn collect_from_syntax(syntax: &SourceFile, include_doc_code: bool) -> FxHashSet { let mut collector = ImportCollector::new(include_doc_code); collector.visit(syntax); collector.deps @@ -59,14 +59,19 @@ fn collect_from_syntax(syntax: &syn::File, include_doc_code: bool) -> FxHashSet< fn collect_imports_from_snippet(code: &str) -> Option> { // Try parsing as a complete file first - if let Ok(syntax) = syn::parse_file(code) { - return Some(collect_from_syntax(&syntax, false)); + let syntax = SourceFile::parse(code, Edition::CURRENT); + if syntax.errors().is_empty() { + return Some(collect_from_syntax(&syntax.tree(), false)); } // If that fails, wrap in a main function (like doc tests do) let wrapped = format!("fn main() {{\n{code}\n}}"); - let syntax = syn::parse_file(&wrapped).ok()?; - Some(collect_from_syntax(&syntax, false)) + let syntax = SourceFile::parse(&wrapped, Edition::CURRENT); + if syntax.errors().is_empty() { + return Some(collect_from_syntax(&syntax.tree(), false)); + } + + None } struct ImportCollector { @@ -79,69 +84,84 @@ impl ImportCollector { Self { deps: FxHashSet::default(), include_doc_code } } - fn visit(&mut self, syntax: &syn::File) { - use syn::visit::Visit; - self.visit_file(syntax); + fn visit(&mut self, syntax: &SourceFile) { + for event in syntax.syntax().preorder() { + let WalkEvent::Enter(node) = event else { continue }; + + #[expect( + clippy::wildcard_enum_match_arm, + reason = "Hundreds of variants, using '_' is the best option" + )] + match node.kind() { + SyntaxKind::USE => self.visit_use(node), + SyntaxKind::EXTERN_CRATE => self.visit_extern_crate(node), + SyntaxKind::PATH => self.visit_path(node), + SyntaxKind::MACRO_CALL => self.visit_macro_call(node), + SyntaxKind::MACRO_RULES => self.visit_macro_rules(node), + SyntaxKind::ATTR => self.visit_attribute(node), + _ => {} + } + } } fn is_known_import(s: &str) -> bool { matches!(s, "crate" | "super" | "self" | "std") } - fn add_import(&mut self, s: String) { - if !Self::is_known_import(&s) { - self.deps.insert(s); + fn add_import(&mut self, s: &str) { + if !Self::is_known_import(s) { + // Handle raw identifiers + let clean = s.strip_prefix("r#").unwrap_or(s); + self.deps.insert(clean.to_owned()); } } - fn unraw_string(ident: &syn::Ident) -> String { - ident.unraw().to_string() - } - - fn add_ident(&mut self, ident: &syn::Ident) { - self.add_import(Self::unraw_string(ident)); - } + fn collect_use_tree(&mut self, tree: &UseTree) { + // Path imports + // - `use foo::bar` + // - `use foo::{bar, baz}` + // - `use foo as bar` + if let Some(path) = tree.path() { + // Extract the first segment + if let Some(first_segment) = path.segments().next() + && let Some(name_ref) = first_segment.name_ref() + { + self.add_import(name_ref.text().as_ref()); + } + } - fn collect_use_tree(&mut self, i: &syn::UseTree) { - use syn::UseTree; - match i { - UseTree::Path(use_path) => self.add_ident(&use_path.ident), - UseTree::Name(use_name) => self.add_ident(&use_name.ident), - UseTree::Rename(use_rename) => self.add_ident(&use_rename.ident), - UseTree::Glob(_) => {} - UseTree::Group(use_group) => { - for use_tree in &use_group.items { - self.collect_use_tree(use_tree); - } + // Group imports + // - `use {foo, bar}` + // - `use foo::{bar, baz}` + if let Some(use_tree_list) = tree.use_tree_list() + && tree.path().is_none() + { + for subtree in use_tree_list.use_trees() { + self.collect_use_tree(&subtree); } } } // `foo::bar` in expressions - fn collect_path(&mut self, path: &syn::Path, is_module: bool) { - if path.segments.len() <= 1 && !is_module { + fn collect_path(&mut self, path: &Path, is_module: bool) { + if path.segments().count() <= 1 && !is_module { // Avoid collecting single-segment paths unless they explicitly point to a module, which might be a crate. // This prevents false positives from free functions and other local items. return; } - let Some(path_segment) = path.segments.first() else { return }; - let ident = Self::unraw_string(&path_segment.ident); + let Some(path_segment) = path.segments().next() else { return }; + let Some(name_ref) = path_segment.name_ref() else { return }; + let ident = name_ref.text(); if ident.chars().next().is_some_and(char::is_uppercase) { return; } - self.add_import(ident); - } - - // `let _: ` - fn collect_type_path(&mut self, type_path: &syn::TypePath) { - let path = &type_path.path; - self.collect_path(path, false); + self.add_import(ident.as_ref()); } // `println!("{}", foo::bar);` // ^^^^^^^^ search for the `::` pattern - fn collect_tokens(&mut self, tokens: &proc_macro2::TokenStream) { - let Some(source_text) = tokens.span().source_text() else { return }; + fn collect_tokens(&mut self, node: &SyntaxNode) { + let source_text = node.text().to_string(); let idents = source_text .match_indices("::") @@ -209,75 +229,87 @@ impl ImportCollector { } // #[serde(with = "foo")] - fn collect_known_attribute(&mut self, attr: &syn::Attribute) { + fn collect_serde_attribute(&mut self, token_tree: &TokenTree) { // Many serde attributes are already caught by `collect_tokens` because they use the `::` pattern. // However, the `with` and `crate` attributes are special cases since they directly reference modules or crates. - if attr.path().is_ident("serde") { - attr.parse_nested_meta(|meta| { - // #[serde(with = "foo")] - // #[serde(crate = "foo")] - if meta.path.is_ident("with") || meta.path.is_ident("crate") { - let _eq = meta.input.parse::()?; - let lit = meta.input.parse::()?; - let path = syn::parse_str(&lit.value())?; - self.collect_path(&path, true); - } - // ignore unknown args - Ok(()) - }) - // Ignore invalid serde attributes. - .ok(); + let text = token_tree.syntax().text().to_string(); + + // #[serde(with = "foo")] + // #[serde(crate = "foo")] + for pattern in ["with = \"", "crate = \""] { + let Some(rest) = text.split_once(pattern).map(|(_, after)| after) else { + continue; + }; + + let Some((path, _)) = rest.split_once('"') else { continue }; + + // Extract first segment + if let Some(import) = path.split("::").next() { + self.add_import(import); + } } } -} -impl<'a> syn::visit::Visit<'a> for ImportCollector { - fn visit_path(&mut self, i: &'a syn::Path) { - self.collect_path(i, false); - syn::visit::visit_path(self, i); + fn visit_path(&mut self, node: SyntaxNode) { + let Some(path) = Path::cast(node) else { return }; + self.collect_path(&path, false); } - /// A use declaration: `use std::collections::HashMap`. - fn visit_item_use(&mut self, i: &'a syn::ItemUse) { - self.collect_use_tree(&i.tree); + /// A use declaration: `use std::collections::HashMap` + fn visit_use(&mut self, node: SyntaxNode) { + let Some(use_item) = Use::cast(node) else { return }; + let Some(use_tree) = use_item.use_tree() else { return }; + self.collect_use_tree(&use_tree); } - /// A path like `std::slice::Iter`, optionally qualified with a self-type as in as `SomeTrait>::Associated`. - fn visit_type_path(&mut self, i: &'a syn::TypePath) { - self.collect_type_path(i); - syn::visit::visit_type_path(self, i); + /// An extern crate item: `extern crate serde` + fn visit_extern_crate(&mut self, node: SyntaxNode) { + let Some(extern_crate) = ExternCrate::cast(node) else { return }; + let Some(name_ref) = extern_crate.name_ref() else { return }; + self.add_import(name_ref.text().as_ref()); } - /// A structured list within an attribute, like derive(Copy, Clone). - fn visit_meta_list(&mut self, m: &'a syn::MetaList) { - self.collect_path(&m.path, false); - self.collect_tokens(&m.tokens); - } + /// A macro invocation: `println!("hello")` + fn visit_macro_call(&mut self, node: SyntaxNode) { + let Some(macro_call) = MacroCall::cast(node) else { return }; - /// An extern crate item: extern crate serde. - fn visit_item_extern_crate(&mut self, i: &'a syn::ItemExternCrate) { - self.add_ident(&i.ident); - } + if let Some(path) = macro_call.path() { + self.collect_path(&path, false); + } - fn visit_macro(&mut self, m: &'a syn::Macro) { - self.collect_path(&m.path, false); - self.collect_tokens(&m.tokens); + if let Some(token_tree) = macro_call.token_tree() { + self.collect_tokens(token_tree.syntax()); + } } - fn visit_item(&mut self, i: &'a syn::Item) { - // For tokens not interpreted by Syn. - if let syn::Item::Verbatim(tokens) = i { - self.collect_tokens(tokens); - } - syn::visit::visit_item(self, i); + /// A `macro_rules` definition: `macro_rules! foo { ... }` + fn visit_macro_rules(&mut self, node: SyntaxNode) { + let Some(macro_rules) = MacroRules::cast(node) else { return }; + let Some(token_tree) = macro_rules.token_tree() else { return }; + self.collect_tokens(token_tree.syntax()); } - fn visit_attribute(&mut self, attr: &'a syn::Attribute) { - if !self.include_doc_code && attr.path().is_ident("doc") { + /// An attribute: `#[derive(Debug)]` + fn visit_attribute(&mut self, node: SyntaxNode) { + let Some(attr) = Attr::cast(node) else { return }; + + if !self.include_doc_code + && let Some(path) = attr.path() + && path.to_string() == "doc" + { return; } - self.collect_known_attribute(attr); - syn::visit::visit_attribute(self, attr); + + if let Some(token_tree) = attr.token_tree() { + self.collect_tokens(token_tree.syntax()); + + // Handle known attributes + if let Some(path) = attr.path() + && path.to_string() == "serde" + { + self.collect_serde_attribute(&token_tree); + } + } } } @@ -476,7 +508,7 @@ mod tests { fn demo() {} "#; - let deps = collect_imports(source).expect("failed to collect imports from doc block"); + let deps = collect_imports(source); assert!(deps.contains("url"), "doc-test rust blocks should count as dependency usage"); } @@ -493,7 +525,7 @@ mod tests { fn example() {} "; - let deps = collect_imports(source).expect("failed to collect imports"); + let deps = collect_imports(source); assert!(deps.contains("async_trait"), "should detect async_trait"); } @@ -512,8 +544,7 @@ mod tests { fn example() {} "#; - let deps = collect_imports(source) - .expect("failed to collect imports from statement-based doctest"); + let deps = collect_imports(source); assert!( deps.contains("serde_json"), "statement-based doctests should be wrapped and parsed correctly" diff --git a/src/tests.rs b/src/tests.rs index 4914191..a1a0677 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -6,7 +6,7 @@ use crate::{CargoShear, CargoShearOptions, default_path, import_collector::colle #[track_caller] fn test(source_text: &str) { - let deps = collect_imports(source_text).unwrap(); + let deps = collect_imports(source_text); let expected = FxHashSet::from_iter(["foo".to_owned()]); assert_eq!(deps, expected, "{source_text}"); } @@ -100,31 +100,31 @@ fn test_lib() { #[test] fn empty_source() { - let deps = collect_imports("").unwrap(); + let deps = collect_imports(""); assert!(deps.is_empty()); } #[test] fn comment_only() { - let deps = collect_imports("// this is a comment").unwrap(); + let deps = collect_imports("// this is a comment"); assert!(deps.is_empty()); } #[test] fn std_imports_not_collected() { - let deps = collect_imports("use std::collections::HashMap;").unwrap(); + let deps = collect_imports("use std::collections::HashMap;"); assert!(deps.is_empty()); } #[test] fn self_super_crate_not_collected() { - let deps = collect_imports("use self::module;").unwrap(); + let deps = collect_imports("use self::module;"); assert!(deps.is_empty()); - let deps = collect_imports("use super::module;").unwrap(); + let deps = collect_imports("use super::module;"); assert!(deps.is_empty()); - let deps = collect_imports("use crate::module;").unwrap(); + let deps = collect_imports("use crate::module;"); assert!(deps.is_empty()); } @@ -137,7 +137,7 @@ fn multiple_imports_same_crate() { foo::qux(); } "; - let deps = collect_imports(source).unwrap(); + let deps = collect_imports(source); let expected = FxHashSet::from_iter(["foo".to_owned()]); assert_eq!(deps, expected); } @@ -239,28 +239,22 @@ fn lifetimes_and_generics() { #[test] fn malformed_syntax_recovery() { // Test that we can handle some malformed syntax gracefully - let result = collect_imports("use foo::;"); // Incomplete use statement - // Should either parse successfully or return an error, but not panic - if let Ok(deps) = result { - // If it parses, foo should be collected - assert!(deps.contains("foo") || deps.is_empty()); - } else { - // Parsing error is acceptable for malformed syntax - } + let deps = collect_imports("use foo::;"); + assert!(deps.contains("foo")); } #[test] fn very_long_path() { let long_path = "foo::".repeat(100) + "bar"; let source = format!("use {long_path};"); - let deps = collect_imports(&source).unwrap(); + let deps = collect_imports(&source); let expected = FxHashSet::from_iter(["foo".to_owned()]); assert_eq!(deps, expected); } #[test] fn unicode_identifiers() { - let deps = collect_imports("use foo::数据;").unwrap(); + let deps = collect_imports("use foo::数据;"); let expected = FxHashSet::from_iter(["foo".to_owned()]); assert_eq!(deps, expected); } @@ -283,7 +277,7 @@ fn raw_string_inside_macro() { #[test] fn glob_imports() { - let deps = collect_imports("use foo::*;").unwrap(); + let deps = collect_imports("use foo::*;"); let expected = FxHashSet::from_iter(["foo".to_owned()]); assert_eq!(deps, expected); } @@ -386,19 +380,19 @@ fn cargo_shear_options_creation() { #[test] fn invalid_rust_syntax() { - let result = collect_imports("this is not rust code ^^^"); - assert!(result.is_err(), "Should fail to parse invalid Rust syntax"); + let deps = collect_imports("this is not rust code ^^^"); + assert!(deps.is_empty(), "Should gracefully handle invalid Rust syntax"); } #[test] fn empty_file_handling() { - let deps = collect_imports("").unwrap(); + let deps = collect_imports(""); assert!(deps.is_empty(), "Empty file should result in no dependencies"); } #[test] fn whitespace_only() { - let deps = collect_imports(" \n \t \n ").unwrap(); + let deps = collect_imports(" \n \t \n "); assert!(deps.is_empty(), "Whitespace-only file should result in no dependencies"); } @@ -411,7 +405,7 @@ fn mixed_valid_invalid_imports() { use self::local; // should be ignored (self) use foo::baz::qux; // valid, same crate as first "; - let deps = collect_imports(source).unwrap(); + let deps = collect_imports(source); let expected = FxHashSet::from_iter(["foo".to_owned()]); assert_eq!(deps, expected); } @@ -420,13 +414,13 @@ fn mixed_valid_invalid_imports() { #[track_caller] fn test_no_deps(source_text: &str) { - let deps = collect_imports(source_text).unwrap(); + let deps = collect_imports(source_text); assert!(deps.is_empty(), "Expected no dependencies for: {source_text}"); } #[track_caller] fn test_multiple_deps(source_text: &str, expected_deps: &[&str]) { - let deps = collect_imports(source_text).unwrap(); + let deps = collect_imports(source_text); let expected = expected_deps.iter().map(|s| (*s).to_owned()).collect::>(); assert_eq!(deps, expected, "Dependencies mismatch for: {source_text}"); } @@ -459,7 +453,7 @@ fn large_file_simulation() { writeln!(source, "fn func{i}() {{ foo::call{i}(); }}").unwrap(); } - let deps = collect_imports(&source).unwrap(); + let deps = collect_imports(&source); let expected = FxHashSet::from_iter(["foo".to_owned()]); assert_eq!(deps, expected); } @@ -468,7 +462,7 @@ fn large_file_simulation() { fn deeply_nested_paths() { let nested_path = (0..20).map(|i| format!("level{i}")).collect::>().join("::"); let source = format!("use foo::{nested_path};"); - let deps = collect_imports(&source).unwrap(); + let deps = collect_imports(&source); let expected = FxHashSet::from_iter(["foo".to_owned()]); assert_eq!(deps, expected); } @@ -500,7 +494,7 @@ fn raw_identifiers() { #[test] fn raw_identifier_crate_name() { - let deps = collect_imports("use r#continue::thing;").unwrap(); + let deps = collect_imports("use r#continue::thing;"); assert!(deps.contains("continue")); } @@ -647,10 +641,16 @@ fn dependency_injection() { #[test] fn incomplete_statements() { - // These should parse or fail gracefully without panicking - let _ = collect_imports("use foo::"); - let _ = collect_imports("fn main() { foo:: }"); - let _ = collect_imports("struct S { field: foo:: }"); + // These should parse without panicking + + let deps = collect_imports("use foo::"); + assert!(deps.contains("foo")); + + let deps = collect_imports("fn main() { foo:: }"); + assert!(deps.is_empty()); + + let deps = collect_imports("struct S { field: foo:: }"); + assert!(deps.is_empty()); } #[test] @@ -689,7 +689,7 @@ fn many_small_imports() { for i in 0..1000 { writeln!(source, "use foo::item{i};").unwrap(); } - let deps = collect_imports(&source).unwrap(); + let deps = collect_imports(&source); let expected = FxHashSet::from_iter(["foo".to_owned()]); assert_eq!(deps, expected); } @@ -700,7 +700,7 @@ fn deeply_nested_modules() { for i in 0..100 { writeln!(source, "mod level{i} {{ use foo::item{i}; }}").unwrap(); } - let deps = collect_imports(&source).unwrap(); + let deps = collect_imports(&source); let expected = FxHashSet::from_iter(["foo".to_owned()]); assert_eq!(deps, expected); } @@ -755,55 +755,52 @@ fn macro_invocation_complex_patterns() { #[test] fn raw_identifier_combinations() { // Raw identifiers in both positions - let deps = collect_imports("use r#foo::r#type;").unwrap(); + let deps = collect_imports("use r#foo::r#type;"); assert!(deps.contains("foo")); - let deps = collect_imports("fn main() { r#foo::r#match(); }").unwrap(); + let deps = collect_imports("fn main() { r#foo::r#match(); }"); assert!(deps.contains("foo")); // Raw identifier only on left - let deps = collect_imports("use r#async::bar;").unwrap(); + let deps = collect_imports("use r#async::bar;"); assert!(deps.contains("async")); - let deps = collect_imports("fn main() { r#type::regular(); }").unwrap(); + let deps = collect_imports("fn main() { r#type::regular(); }"); assert!(deps.contains("type")); // Raw identifier only on right - let deps = collect_imports("use regular::r#await;").unwrap(); + let deps = collect_imports("use regular::r#await;"); assert!(deps.contains("regular")); test("fn main() { foo::r#fn(); }"); // Raw identifiers with whitespace - let deps = collect_imports("use r#foo :: r#bar;").unwrap(); + let deps = collect_imports("use r#foo :: r#bar;"); assert!(deps.contains("foo")); - let deps = collect_imports("fn main() { r#type\t::\tr#match(); }").unwrap(); + let deps = collect_imports("fn main() { r#type\t::\tr#match(); }"); assert!(deps.contains("type")); // Raw identifiers in macros - let deps = collect_imports(r#"fn main() { println!("{}", r#foo::r#bar); }"#).unwrap(); + let deps = collect_imports(r#"fn main() { println!("{}", r#foo::r#bar); }"#); assert!(deps.contains("foo")); - let deps = collect_imports("#[derive(r#foo::r#Trait)] struct S;").unwrap(); + let deps = collect_imports("#[derive(r#foo::r#Trait)] struct S;"); assert!(deps.contains("foo")); } #[test] fn multiple_colon_patterns() { // Three or more colons (should handle gracefully) - let result = collect_imports("fn main() { foo:::bar(); }"); - // Should either parse or fail gracefully - assert!(result.is_ok() || result.is_err()); + let deps = collect_imports("fn main() { foo:::bar(); }"); + assert!(deps.contains("foo")); - let result = collect_imports("fn main() { foo::::bar(); }"); - assert!(result.is_ok() || result.is_err()); + let deps = collect_imports("fn main() { foo::::bar(); }"); + assert!(deps.contains("foo")); // Mixed valid and invalid patterns - let result = collect_imports("fn main() { foo::bar(); baz:::qux(); }"); - if let Ok(deps) = result { - // Should at least capture foo from the valid pattern - assert!(deps.contains("foo") || deps.is_empty()); - } + let deps = collect_imports("fn main() { foo::bar(); baz:::qux(); }"); + let expected = FxHashSet::from_iter(["foo".to_owned(), "baz".to_owned()]); + assert_eq!(deps, expected); } #[test] @@ -882,24 +879,24 @@ fn derive_macro_variations() { #[test] fn edge_case_path_segments() { // Single letter crate names - let deps = collect_imports("use a::b;").unwrap(); + let deps = collect_imports("use a::b;"); assert!(deps.contains("a")); - let deps = collect_imports("fn main() { x::y(); }").unwrap(); + let deps = collect_imports("fn main() { x::y(); }"); assert!(deps.contains("x")); // Underscore in paths - let deps = collect_imports("use foo_bar::baz_qux;").unwrap(); + let deps = collect_imports("use foo_bar::baz_qux;"); assert!(deps.contains("foo_bar")); - let deps = collect_imports("fn main() { snake_case::function_name(); }").unwrap(); + let deps = collect_imports("fn main() { snake_case::function_name(); }"); assert!(deps.contains("snake_case")); // Numbers in identifiers - let deps = collect_imports("use foo2::bar3;").unwrap(); + let deps = collect_imports("use foo2::bar3;"); assert!(deps.contains("foo2")); - let deps = collect_imports("fn main() { crate1::module2::func3(); }").unwrap(); + let deps = collect_imports("fn main() { crate1::module2::func3(); }"); assert!(deps.contains("crate1")); }