1use std::{collections::BTreeMap, ops::Range, path::Path, sync::Arc};
64
65use cargo_metadata::{
66 Metadata, Resolve,
67 semver::{Prerelease, Version, VersionReq},
68};
69use clippy_utils::{
70 diagnostics::{span_lint, span_lint_and_then},
71 paths::find_crates,
72};
73use rustc_hir::def_id::LOCAL_CRATE;
74use rustc_lint::LateContext;
75use rustc_span::{BytePos, Pos, SourceFile, Span, SyntaxContext};
76use serde::Deserialize;
77use toml::Spanned;
78
79use crate::{declare_bevy_lint, sym};
80
81declare_bevy_lint! {
82 pub(crate) DUPLICATE_BEVY_DEPENDENCIES,
83 super::Nursery,
84 "multiple versions of the `bevy` crate found",
85 @crate_level_only = true,
86}
87
88#[derive(Deserialize, Debug)]
89struct CargoToml {
90 dependencies: BTreeMap<Spanned<String>, Spanned<toml::Value>>,
91}
92
93fn toml_span(range: Range<usize>, file: &SourceFile) -> Span {
94 Span::new(
95 file.start_pos + BytePos::from_usize(range.start),
96 file.start_pos + BytePos::from_usize(range.end),
97 SyntaxContext::root(),
98 None,
99 )
100}
101
102pub(crate) fn check(cx: &LateContext<'_>, metadata: &Metadata) {
103 if find_crates(cx.tcx, sym::bevy).len() == 1 {
105 return;
106 }
107
108 if let Ok(file) = cx.tcx.sess.source_map().load_file(Path::new("Cargo.toml"))
111 && let Some(src) = file.src.as_deref()
112 && let Ok(cargo_toml) = toml::from_str::<CargoToml>(src)
115 {
116 let local_name = cx.tcx.crate_name(LOCAL_CRATE);
117
118 let mut bevy_dependents = BTreeMap::default();
120 for package in &metadata.packages {
121 for dependency in &package.dependencies {
122 if dependency.name.as_str() == "bevy"
123 && package.name.as_str() != local_name.as_str()
124 {
125 bevy_dependents.insert(package.name.as_str(), dependency.req.clone());
126 }
127 }
128 }
129
130 match cargo_toml.dependencies.get("bevy") {
135 Some(bevy_cargo) => {
136 lint_with_target_version(cx, &cargo_toml, &file, bevy_cargo, &bevy_dependents);
137 }
138
139 None => {
140 if let Some(resolve) = &metadata.resolve {
141 minimal_lint(cx, &bevy_dependents, resolve);
142 }
143 }
144 }
145 }
146}
147
148fn lint_with_target_version(
149 cx: &LateContext<'_>,
150 cargo_toml: &CargoToml,
151 file: &Arc<SourceFile>,
152 bevy_cargo: &Spanned<toml::Value>,
153 bevy_dependents: &BTreeMap<&str, VersionReq>,
154) {
155 let bevy_cargo_toml_span = toml_span(bevy_cargo.span(), file);
156
157 let Ok(target_version) = get_version_from_toml(bevy_cargo.as_ref()) else {
162 cx.tcx.dcx().span_warn(
163 bevy_cargo_toml_span,
164 "specified version format is not supported, use a fixed version or disable `bevy::duplicate_bevy_dependencies`",
165 );
166 return;
167 };
168
169 let mismatching_dependencies = bevy_dependents
170 .iter()
171 .filter(|dependency| !dependency.1.matches(&target_version));
172
173 for mismatching_dependency in mismatching_dependencies {
174 if let Some(cargo_toml_reference) = cargo_toml.dependencies.get(*mismatching_dependency.0) {
175 span_lint_and_then(
176 cx,
177 DUPLICATE_BEVY_DEPENDENCIES,
178 toml_span(cargo_toml_reference.span(), file),
179 DUPLICATE_BEVY_DEPENDENCIES.desc,
180 |diag| {
181 diag.span_help(
182 bevy_cargo_toml_span,
183 format!("expected all crates to use `bevy` {target_version}, but `{}` uses `bevy` {}", mismatching_dependency.0, mismatching_dependency.1),
184 );
185 },
186 );
187 }
188 }
189}
190
191fn minimal_lint(
192 cx: &LateContext<'_>,
193 bevy_dependents: &BTreeMap<&str, VersionReq>,
194 resolved: &Resolve,
195) {
196 let mut resolved_bevy_versions: Vec<&str> = resolved
200 .nodes
201 .iter()
202 .filter_map(|node| {
203 if node.id.repr.starts_with("file:///") {
205 return node.id.repr.split('#').nth(1).map(|version| vec![version]);
206 }
207 if let Some((id, _)) = node.id.repr.split_once('@')
209 && bevy_dependents
210 .keys()
211 .any(|crate_name| id.ends_with(crate_name))
212 {
213 return Some(
214 node.dependencies
215 .iter()
216 .filter_map(|dep| dep.repr.split_once('@'))
217 .filter_map(|(name, version)| (name.contains("bevy")).then_some(version))
218 .collect(),
219 );
220 }
221
222 None
223 })
224 .flatten()
225 .collect();
226
227 resolved_bevy_versions.sort_unstable();
228 resolved_bevy_versions.dedup();
229
230 if resolved_bevy_versions.len() > 1 {
231 span_lint(
232 cx,
233 DUPLICATE_BEVY_DEPENDENCIES,
234 rustc_span::DUMMY_SP,
235 "found multiple versions of bevy",
236 );
237 }
238}
239
240fn get_version_from_toml(table: &toml::Value) -> anyhow::Result<Version> {
248 match table {
249 toml::Value::String(version) => parse_version(version),
250 toml::Value::Table(table) => table
251 .get("version")
252 .and_then(toml::Value::as_str)
253 .ok_or_else(|| anyhow::anyhow!("The 'version' field is required."))
254 .map(parse_version)?,
255 _ => Err(anyhow::anyhow!(
256 "Unexpected TOML format: expected a toml-string '<crate> = <version>' or a toml-table with '<crate> = {{ version = <version> }} '"
257 )),
258 }
259}
260
261fn parse_version(version: &str) -> anyhow::Result<Version> {
265 let mut iter = version.split('-');
267
268 let semver_parts = iter
271 .next()
272 .ok_or(anyhow::anyhow!("A version string is required"))?
273 .split('.')
274 .collect::<Vec<&str>>();
275
276 if !semver_parts
278 .iter()
279 .all(|part| part.chars().all(|c| c.is_ascii_digit()))
280 {
281 return Err(anyhow::anyhow!("Version ranges are not yet supported"));
282 }
283
284 let pre = iter.next();
285
286 let major = semver_parts
287 .first()
288 .and_then(|v| v.parse::<u64>().ok())
289 .unwrap_or(0);
290
291 let minor = semver_parts
292 .get(1)
293 .and_then(|v| v.parse::<u64>().ok())
294 .unwrap_or(0);
295
296 let patch = semver_parts
297 .get(2)
298 .and_then(|v| v.parse::<u64>().ok())
299 .unwrap_or(0);
300
301 let mut version = Version::new(major, minor, patch);
302
303 if let Some(pre) = pre {
304 version.pre = Prerelease::new(pre).unwrap();
305 }
306 Ok(version)
307}
308
309#[cfg(test)]
310mod tests {
311 use cargo_metadata::semver::{Prerelease, Version};
312
313 use super::parse_version;
314
315 #[test]
316 fn parse_version_req() {
317 assert_eq!(Version::new(0, 16, 0), parse_version("0.16").unwrap());
318 assert_eq!(Version::new(0, 16, 1), parse_version("0.16.1").unwrap());
319 assert_eq!(Version::new(1, 16, 10), parse_version("1.16.10").unwrap());
320 let mut version_with_pre = Version::new(0, 16, 0);
321 version_with_pre.pre = Prerelease::new("rc.1").unwrap();
322 assert_eq!(version_with_pre, parse_version("0.16.0-rc.1").unwrap());
323 assert!(parse_version("0.*").is_err());
325 }
326}