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