Skip to main content

wowlab_tidy/languages/toml/rules/cargo/
edition.rs

1#[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
112/// Literal edition as a number; `edition.workspace = true` yields `None`.
113fn 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}