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
34impl Lint for WorkspacedManifest {
35    fn new(_ctx: &LintCtx) -> Self {
36        WorkspacedManifest {
37            members: Vec::new(),
38            excluded: Vec::new(),
39            dependencies: Vec::new(),
40        }
41    }
42
43    fn enter_workspace(&mut self, content: &Lintable<DocumentMut>) {
44        // Gather the set of crates we expect to see: all members, dependencies, and exclusions
45        self.members = content["workspace"]
46            .get("members")
47            .and_then(|m| m.as_array())
48            .into_iter()
49            .flat_map(|a| a.into_iter())
50            .map(|m| Path::new(m.as_str().unwrap()).join("Cargo.toml"))
51            .collect();
52        self.excluded = content["workspace"]
53            .get("exclude")
54            .and_then(|e| e.as_array())
55            .into_iter()
56            .flat_map(|a| a.into_iter())
57            .map(|e| Path::new(e.as_str().unwrap()).join("Cargo.toml"))
58            .collect();
59        self.dependencies = content["workspace"]
60            .get("dependencies")
61            .and_then(|d| d.as_table())
62            .into_iter()
63            .flat_map(|t| t.into_iter())
64            // We only need to keep local dependencies, external dependencies don't get visited
65            .filter_map(|(_k, v)| {
66                v.get("path")
67                    .map(|p| Path::new(p.as_str().unwrap()).join("Cargo.toml"))
68            })
69            .collect();
70    }
71
72    fn enter_crate(&mut self, content: &Lintable<DocumentMut>) {
73        // Remove this crate from whichever set it appears in, but ensure it only appears in one
74        let mut count = 0;
75        if let Some(member) = self.members.iter().position(|m| content.path() == m) {
76            self.members.remove(member);
77            count += 1;
78        }
79        if let Some(excluded) = self.excluded.iter().position(|e| content.path() == e) {
80            self.excluded.remove(excluded);
81            count += 1;
82        }
83        if let Some(dependency) = self.dependencies.iter().position(|d| content.path() == d) {
84            self.dependencies.remove(dependency);
85            count += 1;
86        }
87
88        if count == 0 {
89            content.unfixable("crate is not a workspace member, dependency, or exclusion");
90        } else if count > 1 {
91            content.unfixable("crate appears in multiple workspace sections");
92        }
93    }
94
95    fn visit_file(&mut self, _content: &mut Lintable<String>) {}
96
97    fn exit_crate(&mut self, content: &mut Lintable<DocumentMut>) {
98        // Verify that all dependencies of this crate are workspaced
99        let mut dep_tables = Vec::new();
100        for (name, v) in content.iter() {
101            match name {
102                "dependencies" | "build-dependencies" | "dev-dependencies" => {
103                    dep_tables.push(v.as_table_like().unwrap())
104                }
105                "target" => {
106                    let flattened = v
107                        .as_table_like()
108                        .unwrap()
109                        .iter()
110                        .flat_map(|(_, v)| v.as_table_like().unwrap().iter());
111
112                    for (k, v) in flattened {
113                        match k {
114                            "dependencies" | "build-dependencies" | "dev-dependencies" => {
115                                dep_tables.push(v.as_table_like().unwrap())
116                            }
117                            _ => {}
118                        }
119                    }
120                }
121                _ => {}
122            }
123        }
124
125        let crate_name = content["package"]["name"].as_str().unwrap();
126        let handle_bad_dep = |dep_name| {
127            let allowed = WORKSPACE_EXCEPTIONS
128                .iter()
129                .find_map(|&(p, crates)| (p == crate_name).then_some(crates))
130                .unwrap_or(&[]);
131
132            if allowed.contains(&dep_name) {
133                log::debug!(
134                    "{} contains non-workspaced dependency {}. Allowed by exception.",
135                    content.path().display(),
136                    dep_name
137                );
138            } else {
139                content.unfixable(&format!("non-workspaced dependency {} found", dep_name));
140            }
141        };
142        let check_table_like = |t: &dyn TableLike, dep_name| {
143            if t.get("workspace").and_then(|x| x.as_bool()) != Some(true) {
144                handle_bad_dep(dep_name);
145            }
146        };
147
148        for table in dep_tables {
149            for (dep_name, value) in table.iter() {
150                match value {
151                    Item::Value(Value::String(_)) => handle_bad_dep(dep_name),
152                    Item::Value(Value::InlineTable(t)) => {
153                        check_table_like(t, dep_name);
154
155                        if t.len() == 1 {
156                            content.unfixable(&format!(
157                                "inline table syntax used for dependency on {} but only one table entry is present, change to the dotted form",
158                                dep_name
159                            ));
160                        }
161                    }
162                    Item::Table(t) => check_table_like(t, dep_name),
163                    _ => unreachable!(),
164                }
165            }
166        }
167    }
168
169    fn exit_workspace(&mut self, content: &mut Lintable<DocumentMut>) {
170        // Any members or dependencies that we expected to see but didn't are errors
171        for member in self.members.iter() {
172            content.unfixable(&format!(
173                "workspace member {} does not exist",
174                member.display()
175            ));
176        }
177        // Dependencies that we didn't see may be from other workspaces, as is done in the internal repo, so they're allowed
178        // Exclusions that we didn't see may be nested workspaces, which don't get visited, so they're allowed
179    }
180}