1use std::collections::HashMap;
36use std::sync::OnceLock;
37
38use proc_macro::TokenStream;
39use proc_macro2::{Delimiter, TokenStream as TokenStream2, TokenTree};
40use quote::quote;
41use semver::{Version, VersionReq};
42use syn::LitStr;
43use syn::parse::{Parse, ParseStream};
44
45struct Package {
47 name: String,
48 version: Version,
49 deps: Vec<String>,
51}
52
53struct LockFile {
55 packages: Vec<Package>,
56}
57
58impl LockFile {
59 fn parse(content: &str) -> Self {
60 let mut packages = Vec::new();
61 let mut current_name: Option<String> = None;
62 let mut current_version: Option<Version> = None;
63 let mut current_deps: Vec<String> = Vec::new();
64 let mut in_deps = false;
65
66 let flush = |name: &mut Option<String>,
67 version: &mut Option<Version>,
68 deps: &mut Vec<String>,
69 packages: &mut Vec<Package>| {
70 if let (Some(name), Some(version)) = (name.take(), version.take()) {
71 packages.push(Package {
72 name,
73 version,
74 deps: std::mem::take(deps),
75 });
76 }
77 };
78
79 for line in content.lines() {
80 let trimmed = line.trim();
81
82 if trimmed == "[[package]]" {
83 flush(
84 &mut current_name,
85 &mut current_version,
86 &mut current_deps,
87 &mut packages,
88 );
89 in_deps = false;
90 continue;
91 }
92
93 if let Some(rest) = trimmed.strip_prefix("name = ") {
94 current_name = Some(rest.trim_matches('"').to_string());
95 in_deps = false;
96 } else if let Some(rest) = trimmed.strip_prefix("version = ") {
97 let ver_str = rest.trim_matches('"');
98 current_version = Version::parse(ver_str).ok();
99 in_deps = false;
100 } else if trimmed == "dependencies = [" {
101 in_deps = true;
102 } else if in_deps && trimmed == "]" {
103 in_deps = false;
104 } else if in_deps {
105 let dep = trimmed.trim_matches(['"', ',', ' '].as_slice());
107 if !dep.is_empty() {
108 current_deps.push(dep.to_string());
109 }
110 }
111 }
112
113 flush(
114 &mut current_name,
115 &mut current_version,
116 &mut current_deps,
117 &mut packages,
118 );
119
120 LockFile { packages }
121 }
122
123 fn resolve_deps_for(&self, pkg_name: &str) -> Vec<(String, Version)> {
128 let mut by_name: HashMap<&str, Vec<&Package>> = HashMap::new();
130 for pkg in &self.packages {
131 by_name.entry(&pkg.name).or_default().push(pkg);
132 }
133
134 let current = match by_name.get(pkg_name).and_then(|pkgs| pkgs.first()) {
136 Some(pkg) => pkg,
137 None => return Vec::new(),
138 };
139
140 let mut result = Vec::new();
141 for dep_entry in ¤t.deps {
142 let mut parts = dep_entry.splitn(2, ' ');
144 let dep_name = parts.next().unwrap();
145 let dep_version = parts.next();
146
147 if let Some(candidates) = by_name.get(dep_name) {
148 let resolved = if let Some(ver_str) = dep_version {
149 candidates.iter().find(|p| p.version.to_string() == ver_str)
151 } else if candidates.len() == 1 {
152 Some(&candidates[0])
154 } else {
155 candidates.first()
158 };
159
160 if let Some(pkg) = resolved {
161 result.push((dep_name.to_string(), pkg.version.clone()));
162 }
163 }
164 }
165
166 result
167 }
168}
169
170static LOCK_CACHE: OnceLock<LockFile> = OnceLock::new();
173
174fn lockfile() -> &'static LockFile {
175 LOCK_CACHE.get_or_init(load_lockfile)
176}
177
178fn load_lockfile() -> LockFile {
179 let manifest_dir = match std::env::var("CARGO_MANIFEST_DIR") {
180 Ok(d) => d,
181 Err(_) => {
182 return LockFile { packages: Vec::new() };
183 }
184 };
185
186 let mut dir = std::path::PathBuf::from(manifest_dir);
187 loop {
188 let candidate = dir.join("Cargo.lock");
189 if let Ok(content) = std::fs::read_to_string(&candidate) {
190 return LockFile::parse(&content);
191 }
192 if !dir.pop() {
193 return LockFile { packages: Vec::new() };
194 }
195 }
196}
197
198fn dep_matches(name: &str, req: &VersionReq) -> bool {
199 let pkg_name = std::env::var("CARGO_PKG_NAME").unwrap_or_default();
200 lockfile()
201 .resolve_deps_for(&pkg_name)
202 .iter()
203 .any(|(n, v)| n == name && req.matches(v))
204}
205
206struct Args {
207 name: String,
208 req: VersionReq,
209}
210
211impl Parse for Args {
212 fn parse(input: ParseStream) -> syn::Result<Self> {
213 let mut name = String::new();
214 loop {
215 if input.is_empty() {
216 return Err(input.error("expected `=` after crate name"));
217 }
218 let tt: TokenTree = input.parse()?;
219 if let TokenTree::Punct(ref p) = tt
220 && p.as_char() == '='
221 {
222 break;
223 }
224 name.push_str(&tt.to_string());
225 }
226 if name.is_empty() {
227 return Err(input.error("expected a crate name"));
228 }
229
230 let lit: LitStr = input.parse()?;
231 let req = VersionReq::parse(&lit.value())
232 .map_err(|e| syn::Error::new(lit.span(), format!("invalid version requirement: {e}")))?;
233 Ok(Args { name, req })
234 }
235}
236
237fn flatten_transparent_group(ts: TokenStream2) -> TokenStream2 {
238 let mut output = TokenStream2::new();
239 for tt in ts {
240 match tt {
241 TokenTree::Group(ref g) if g.delimiter() == Delimiter::None => {
242 output.extend(flatten_transparent_group(g.stream()));
243 }
244 other => output.extend(std::iter::once(other)),
245 }
246 }
247 output
248}
249
250#[proc_macro_attribute]
272pub fn cfg_version(args: TokenStream, input: TokenStream) -> TokenStream {
273 let args = match syn::parse2::<Args>(flatten_transparent_group(args.into())) {
274 Ok(args) => args,
275 Err(e) => return e.to_compile_error().into(),
276 };
277
278 let keep = dep_matches(&args.name, &args.req);
279
280 if keep {
281 let input: TokenStream2 = input.into();
282 quote! {
283 #[allow(clippy::incompatible_msrv)]
284 #input
285 }
286 .into()
287 } else {
288 TokenStream::new()
289 }
290}