Skip to main content

xtask/tasks/fmt/lints/
workspaced.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Checks that every crate's Cargo.toml is properly workspaced.
5
6use super::Lint;
7use super::LintCtx;
8use super::Lintable;
9use std::path::Path;
10use std::path::PathBuf;
11use toml_edit::DocumentMut;
12use toml_edit::Item;
13use toml_edit::TableLike;
14use toml_edit::Value;
15
16/// List of exceptions to using workspace package declarations.
17static WORKSPACE_EXCEPTIONS: &[(&str, &[&str])] = &[
18    // Allow disk_blob to use tokio for now, but no one else.
19    //
20    // disk_blob eventually will remove its tokio dependency.
21    ("disk_blob", &["tokio"]),
22    // Allow mesh_rpc to use tokio, since h2 depends on it for the tokio IO
23    // trait definitions. Hopefully this can be resolved upstream once async IO
24    // trait "vocabulary types" move to a common crate.
25    ("mesh_rpc", &["tokio"]),
26];
27
28pub struct WorkspacedManifest {
29    members: Vec<PathBuf>,
30    excluded: Vec<PathBuf>,
31    dependencies: Vec<PathBuf>,
32
33    only_diffed: bool,
34}
35
36impl Lint for WorkspacedManifest {
37    fn new(ctx: &LintCtx) -> Self {
38        WorkspacedManifest {
39            members: Vec::new(),
40            excluded: Vec::new(),
41            dependencies: Vec::new(),
42
43            only_diffed: ctx.only_diffed,
44        }
45    }
46
47    fn enter_workspace(&mut self, content: &Lintable<DocumentMut>) {
48        // Gather the set of crates we expect to see: all members, dependencies, and exclusions
49        self.members = content["workspace"]
50            .get("members")
51            .and_then(|m| m.as_array())
52            .into_iter()
53            .flat_map(|a| a.into_iter())
54            .map(|m| Path::new(m.as_str().unwrap()).join("Cargo.toml"))
55            .collect();
56        self.excluded = content["workspace"]
57            .get("exclude")
58            .and_then(|e| e.as_array())
59            .into_iter()
60            .flat_map(|a| a.into_iter())
61            .map(|e| Path::new(e.as_str().unwrap()).join("Cargo.toml"))
62            .collect();
63        self.dependencies = content["workspace"]
64            .get("dependencies")
65            .and_then(|d| d.as_table())
66            .into_iter()
67            .flat_map(|t| t.into_iter())
68            // We only need to keep local dependencies, external dependencies don't get visited
69            .filter_map(|(_k, v)| {
70                v.get("path")
71                    .map(|p| Path::new(p.as_str().unwrap()).join("Cargo.toml"))
72            })
73            .collect();
74    }
75
76    fn enter_crate(&mut self, content: &Lintable<DocumentMut>) {
77        // Remove this crate from whichever set it appears in, but ensure it only appears in one
78        let mut count = 0;
79        if let Some(member) = self.members.iter().position(|m| content.path() == m) {
80            self.members.remove(member);
81            count += 1;
82        }
83        if let Some(excluded) = self.excluded.iter().position(|e| content.path() == e) {
84            self.excluded.remove(excluded);
85            count += 1;
86        }
87        if let Some(dependency) = self.dependencies.iter().position(|d| content.path() == d) {
88            self.dependencies.remove(dependency);
89            count += 1;
90        }
91
92        if count == 0 {
93            content.unfixable("crate is not a workspace member, dependency, or exclusion");
94        } else if count > 1 {
95            content.unfixable("crate appears in multiple workspace sections");
96        }
97    }
98
99    fn visit_file(&mut self, _content: &mut Lintable<String>) {}
100
101    fn exit_crate(&mut self, content: &mut Lintable<DocumentMut>) {
102        // Verify that all dependencies of this crate are workspaced
103        let mut dep_tables = Vec::new();
104        for (name, v) in content.iter() {
105            match name {
106                "dependencies" | "build-dependencies" | "dev-dependencies" => {
107                    dep_tables.push(v.as_table_like().unwrap())
108                }
109                "target" => {
110                    let flattened = v
111                        .as_table_like()
112                        .unwrap()
113                        .iter()
114                        .flat_map(|(_, v)| v.as_table_like().unwrap().iter());
115
116                    for (k, v) in flattened {
117                        match k {
118                            "dependencies" | "build-dependencies" | "dev-dependencies" => {
119                                dep_tables.push(v.as_table_like().unwrap())
120                            }
121                            _ => {}
122                        }
123                    }
124                }
125                _ => {}
126            }
127        }
128
129        let crate_name = content["package"]["name"].as_str().unwrap();
130        let handle_bad_dep = |dep_name| {
131            let allowed = WORKSPACE_EXCEPTIONS
132                .iter()
133                .find_map(|&(p, crates)| (p == crate_name).then_some(crates))
134                .unwrap_or(&[]);
135
136            if allowed.contains(&dep_name) {
137                log::debug!(
138                    "{} contains non-workspaced dependency {}. Allowed by exception.",
139                    content.path().display(),
140                    dep_name
141                );
142            } else {
143                content.unfixable(&format!("non-workspaced dependency {} found", dep_name));
144            }
145        };
146        let check_table_like = |t: &dyn TableLike, dep_name| {
147            if t.get("workspace").and_then(|x| x.as_bool()) != Some(true) {
148                handle_bad_dep(dep_name);
149            }
150        };
151
152        for table in dep_tables {
153            for (dep_name, value) in table.iter() {
154                match value {
155                    Item::Value(Value::String(_)) => handle_bad_dep(dep_name),
156                    Item::Value(Value::InlineTable(t)) => {
157                        check_table_like(t, dep_name);
158
159                        if t.len() == 1 {
160                            content.unfixable(&format!(
161                                "inline table syntax used for dependency on {} but only one table entry is present, change to the dotted form",
162                                dep_name
163                            ));
164                        }
165                    }
166                    Item::Table(t) => check_table_like(t, dep_name),
167                    _ => unreachable!(),
168                }
169            }
170        }
171    }
172
173    fn exit_workspace(&mut self, content: &mut Lintable<DocumentMut>) {
174        // Any workspace members that we expected to see but didn't are errors,
175        // unless we're only checking diffs, in which case we know we'll miss crates.
176        if !self.only_diffed {
177            for member in self.members.iter() {
178                content.unfixable(&format!(
179                    "workspace member {} does not exist",
180                    member.display()
181                ));
182            }
183        }
184        // Dependencies that we didn't see may be from other workspaces, as is done in the internal repo, so they're allowed
185        // Exclusions that we didn't see may be nested workspaces, which don't get visited, so they're allowed
186    }
187}