wowlab_tidy/languages/toml/rules/cargo/
edition.rs1#[cfg(test)]
2use googletest::prelude::*;
3
4use super::{CARGO_WORKSPACE_REL, cargo_document, is_cargo_member, key_line, nested_table};
5use crate::{Example, TomlCtx, Violation, violation};
6
7const RESOLVER_REQUIRED_EDITION: i64 = 2024;
8const CURRENT_RESOLVER: &str = "3";
9
10#[rustfmt::skip]
11const EXAMPLES: &[Example] = &[
12 Example { label: "workspace on current edition", code: "[workspace]\nmembers = []\n\n[workspace.package]\nedition = \"2024\"\n", pass: false },
13 Example { label: "workspace on old edition", code: "[workspace]\nresolver = \"2\"\nmembers = []\n\n[workspace.package]\nedition = \"2021\"\n", pass: false },
14 Example { label: "redundant resolver on current edition", code: "[workspace]\nresolver = \"3\"\nmembers = []\n\n[workspace.package]\nedition = \"2024\"\n", pass: true },
15 Example { label: "member inherits edition", code: "[package]\nname = \"foo\"\nedition.workspace = true\n", pass: true },
16 Example { label: "member on old edition", code: "[package]\nname = \"foo\"\nedition = \"2021\"\n", pass: false },
17];
18
19crate::toml_rule!(
20 toml_cargo_edition,
21 "Require the workspace and non-inheriting members to target at least the configured Rust edition, with the matching virtual-workspace resolver.",
22 "Virtual workspaces do not infer the resolver from workspace.package.edition, so resolver 3 must be explicit for edition 2024 (M-LATEST-EDITION).",
23 Medium,
24 params {
25 min_edition: i64 = 2024
26 },
27);
28
29fn check_toml_cargo_edition(ctx: &TomlCtx<'_>) -> Vec<Violation> {
30 let Some(document) = cargo_document(ctx) else {
31 return Vec::new();
32 };
33 let min_edition = ctx.file.config.get_i64("toml_cargo_edition", &PARAMS[0]);
34
35 if ctx.file.rel == CARGO_WORKSPACE_REL {
36 return workspace_violations(ctx, &document, min_edition);
37 }
38
39 if is_cargo_member(ctx.file.rel) && document.contains_key("package") {
40 return member_violations(ctx, &document, min_edition);
41 }
42
43 Vec::new()
44}
45
46fn workspace_violations(
47 ctx: &TomlCtx<'_>,
48 document: &toml::Table,
49 min_edition: i64,
50) -> Vec<Violation> {
51 let Some(edition) = nested_table(document, &["workspace", "package"])
52 .and_then(|package| package.get("edition"))
53 .and_then(edition_value)
54 else {
55 return Vec::new();
56 };
57 let mut violations = Vec::new();
58
59 if edition < min_edition {
60 violations.push(violation(
61 ctx.file.rel,
62 key_line(ctx.file.lines, "workspace.package", "edition"),
63 format!(
64 "workspace edition {edition} is below the minimum {min_edition} (M-LATEST-EDITION)"
65 ),
66 ));
67 }
68
69 if edition >= RESOLVER_REQUIRED_EDITION {
70 let resolver = nested_table(document, &["workspace"])
71 .and_then(|workspace| workspace.get("resolver"))
72 .and_then(toml::Value::as_str);
73
74 if resolver != Some(CURRENT_RESOLVER) {
75 violations.push(violation(
76 ctx.file.rel,
77 key_line(ctx.file.lines, "workspace", "resolver"),
78 format!(
79 "virtual workspace on edition {edition} must set `resolver = \"{CURRENT_RESOLVER}\"`"
80 ),
81 ));
82 }
83 }
84
85 violations
86}
87
88fn member_violations(
89 ctx: &TomlCtx<'_>,
90 document: &toml::Table,
91 min_edition: i64,
92) -> Vec<Violation> {
93 let Some(package) = nested_table(document, &["package"]) else {
94 return Vec::new();
95 };
96 let Some(edition) = package.get("edition").and_then(edition_value) else {
97 return Vec::new();
98 };
99 let mut violations = Vec::new();
100
101 if edition < min_edition {
102 violations.push(violation(
103 ctx.file.rel,
104 key_line(ctx.file.lines, "package", "edition"),
105 format!("crate edition {edition} is below the minimum {min_edition}; prefer `edition.workspace = true` (M-LATEST-EDITION)"),
106 ));
107 }
108
109 violations
110}
111
112fn edition_value(value: &toml::Value) -> Option<i64> {
114 match value {
115 toml::Value::String(edition) => edition.parse().ok(),
116 toml::Value::Integer(edition) => Some(*edition),
117 _ => None,
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[gtest]
126 fn examples() -> Result<()> {
127 for example in EXAMPLES {
128 let rel = if example.code.contains("[workspace") {
129 "crates/Cargo.toml"
130 } else {
131 "crates/foo/Cargo.toml"
132 };
133 let violations = crate::test_support::check_source_toml_at(
134 rel,
135 example.code,
136 check_toml_cargo_edition,
137 );
138
139 verify_eq!(violations.is_empty(), example.pass)?;
140 }
141
142 Ok(())
143 }
144
145 #[gtest]
146 fn workspace_resolver_must_match_the_current_edition() -> Result<()> {
147 let current = crate::test_support::check_source_toml_at(
148 "crates/Cargo.toml",
149 "[workspace]\nresolver = \"2\"\nmembers = []\n\n[workspace.package]\nedition = \"2024\"\nversion = \"0.1.0\"\n",
150 check_toml_cargo_edition,
151 );
152
153 verify_eq!(current.len(), 1)?;
154 verify_true!(current[0].message.contains("resolver"))?;
155 verify_eq!(current[0].line, 2)?;
156
157 let old = crate::test_support::check_source_toml_at(
158 "crates/Cargo.toml",
159 "[workspace]\nresolver = \"2\"\nmembers = []\n\n[workspace.package]\nedition = \"2021\"\n",
160 check_toml_cargo_edition,
161 );
162
163 verify_eq!(old.len(), 1)?;
164 verify_true!(old[0].message.contains("edition 2021"))?;
165
166 let matching = crate::test_support::check_source_toml_at(
167 "crates/Cargo.toml",
168 "[workspace]\nresolver = \"3\"\nmembers = []\n\n[workspace.package]\nedition = \"2024\"\n",
169 check_toml_cargo_edition,
170 );
171
172 verify_true!(matching.is_empty())?;
173
174 let missing = crate::test_support::check_source_toml_at(
175 "crates/Cargo.toml",
176 "[workspace]\nmembers = []\n\n[workspace.package]\nedition = \"2024\"\n",
177 check_toml_cargo_edition,
178 );
179
180 verify_eq!(missing.len(), 1)?;
181
182 Ok(())
183 }
184}