xtask/tasks/fmt/lints/
unused_deps.rs1use super::Lint;
36use super::LintCtx;
37use super::Lintable;
38use grep_regex::RegexMatcher;
39use std::collections::BTreeMap;
40use std::collections::BTreeSet;
41use toml_edit::DocumentMut;
42
43pub struct UnusedDeps {
44 workspace_deps: BTreeSet<String>,
46 workspace_found_deps: BTreeSet<String>,
48
49 crate_deps: BTreeSet<String>,
51 crate_found_deps: BTreeSet<String>,
53
54 dep_matchers: BTreeMap<String, RegexMatcher>,
58
59 only_diffed: bool,
62
63 searcher: grep_searcher::Searcher,
65}
66
67impl Lint for UnusedDeps {
68 fn new(ctx: &LintCtx) -> Self {
69 UnusedDeps {
70 workspace_deps: BTreeSet::new(),
71 workspace_found_deps: BTreeSet::new(),
72 crate_deps: BTreeSet::new(),
73 crate_found_deps: BTreeSet::new(),
74 dep_matchers: BTreeMap::new(),
75 only_diffed: ctx.only_diffed,
76 searcher: grep_searcher::SearcherBuilder::new()
77 .line_number(false)
78 .build(),
79 }
80 }
81
82 fn enter_workspace(&mut self, content: &Lintable<DocumentMut>) {
83 if let Some(deps) = content
85 .get("workspace")
86 .and_then(|w| w.get("dependencies"))
87 .and_then(|d| d.as_table_like())
88 {
89 for (dep_name, _) in deps.iter() {
91 self.workspace_deps.insert(dep_name.to_string());
92 self.dep_matchers
93 .insert(dep_name.to_string(), compile_matcher(dep_name));
94 }
95 }
96 }
97
98 fn enter_crate(&mut self, content: &Lintable<DocumentMut>) {
99 const DEP_TABLE_NAMES: &[&str] =
100 &["dependencies", "build-dependencies", "dev-dependencies"];
101
102 self.crate_deps.clear();
103 self.crate_found_deps.clear();
104
105 for dep_table in DEP_TABLE_NAMES
106 .iter()
107 .flat_map(|s| content.get(s).map(|d| d.as_table_like().unwrap()))
108 {
109 for (dep_name, _) in dep_table.iter() {
110 self.crate_deps.insert(dep_name.to_string());
111 if !self.dep_matchers.contains_key(dep_name) {
112 self.dep_matchers
113 .insert(dep_name.to_string(), compile_matcher(dep_name));
114 }
115 }
116 }
117
118 if let Some(target) = content.get("target").map(|t| t.as_table_like().unwrap()) {
120 for target_table in target.iter().map(|(_, t)| t.as_table_like().unwrap()) {
121 for dep_table in DEP_TABLE_NAMES
122 .iter()
123 .flat_map(|s| target_table.get(s).map(|d| d.as_table_like().unwrap()))
124 {
125 for (dep_name, _) in dep_table.iter() {
126 self.crate_deps.insert(dep_name.to_string());
127 if !self.dep_matchers.contains_key(dep_name) {
128 self.dep_matchers
129 .insert(dep_name.to_string(), compile_matcher(dep_name));
130 }
131 }
132 }
133 }
134 }
135 }
136
137 fn visit_file(&mut self, content: &mut Lintable<String>) {
138 if self.crate_found_deps.len() == self.crate_deps.len() {
139 return;
140 }
141
142 let unfound = self.crate_deps.difference(&self.crate_found_deps);
143 let mut found = Vec::new();
144 for looking in unfound {
145 let needle = looking.replace('-', "_");
146 if !content.contains(&needle) {
148 continue;
149 }
150 let mut sink = StopAfterFirstMatch::new();
152 self.searcher
153 .search_slice(
154 &self.dep_matchers[&**looking],
155 content.as_bytes(),
156 &mut sink,
157 )
158 .unwrap();
159 if sink.found {
160 found.push(looking.clone());
161 }
162 }
163
164 self.crate_found_deps.extend(found.iter().cloned());
165 self.workspace_found_deps.extend(found);
166 }
167
168 fn exit_crate(&mut self, content: &mut Lintable<DocumentMut>) {
169 exit_toml(content, &self.crate_deps, &self.crate_found_deps, false);
170 }
171
172 fn exit_workspace(&mut self, content: &mut Lintable<DocumentMut>) {
173 if self.only_diffed {
177 return;
178 }
179
180 exit_toml(
181 content,
182 &self.workspace_deps,
183 &self.workspace_found_deps,
184 true,
185 );
186 }
187}
188
189fn exit_toml(
190 content: &mut Lintable<DocumentMut>,
191 deps: &BTreeSet<String>,
192 found_deps: &BTreeSet<String>,
193 is_workspace: bool,
194) {
195 let top_key = if is_workspace { "workspace" } else { "package" };
196 let ignored: BTreeSet<_> = content
198 .get(top_key)
199 .and_then(|p| p.get("metadata"))
200 .and_then(|m| m.get("xtask"))
201 .and_then(|x| x.get("unused-deps"))
202 .and_then(|u| u.get("ignored"))
203 .map(|arr| {
204 arr.as_array()
205 .unwrap()
206 .iter()
207 .map(|v| v.as_str().unwrap().to_string())
208 .collect()
209 })
210 .unwrap_or_default();
211
212 for ign in &ignored {
213 if !deps.contains(ign) {
214 let msg = format!("{ign} is ignored, but it's not being depended on");
215 content.fix(&msg, |doc| remove_from_ignored(doc, ign, is_workspace));
216 }
217 }
218
219 for dep in deps {
220 let found = found_deps.contains(&**dep);
221 let ignored = ignored.contains(&**dep);
222
223 match (found, ignored) {
224 (false, false) => {
225 content.fix(&format!("{dep} is unused"), |doc| {
226 remove_from_deps(doc, dep, is_workspace)
227 });
228 }
229 (true, true) => {
230 content.fix(&format!("{dep} is ignored, but being used"), |doc| {
231 remove_from_ignored(doc, dep, is_workspace)
232 });
233 }
234 _ => {}
235 }
236 }
237}
238
239fn compile_matcher(dep: &str) -> RegexMatcher {
240 let name = dep.replace('-', "_");
241 let regex =
254 format!(r#"use (::)?{name}(::|;| as)|(?:[^:]|^|\W::)\b{name}::|extern crate {name}( |;)"#);
255 RegexMatcher::new_line_matcher(®ex).unwrap()
256}
257
258fn remove_from_deps(doc: &mut DocumentMut, dep_name: &str, is_workspace: bool) {
260 const DEP_TABLE_NAMES: &[&str] = &["dependencies", "build-dependencies", "dev-dependencies"];
261
262 if is_workspace {
263 let deps = doc["workspace"]["dependencies"]
265 .as_table_like_mut()
266 .unwrap();
267 deps.remove(dep_name);
268 } else {
269 for table_name in DEP_TABLE_NAMES {
271 if let Some(deps) = doc.get_mut(table_name).and_then(|d| d.as_table_like_mut()) {
272 deps.remove(dep_name);
273 }
274 }
275
276 if let Some(target) = doc.get_mut("target").and_then(|t| t.as_table_like_mut()) {
278 let keys: Vec<String> = target.iter().map(|(k, _)| k.to_string()).collect();
279 for key in keys {
280 if let Some(target_table) = target.get_mut(&key).and_then(|t| t.as_table_like_mut())
281 {
282 for table_name in DEP_TABLE_NAMES {
283 if let Some(deps) = target_table
284 .get_mut(table_name)
285 .and_then(|d| d.as_table_like_mut())
286 {
287 deps.remove(dep_name);
288 }
289 }
290 }
291 }
292 }
293
294 if let Some(features) = doc.get_mut("features").and_then(|f| f.as_table_like_mut()) {
296 let dep_enable = format!("dep:{dep_name}");
297 let dep_feature_prefix = format!("{dep_name}/");
298 let feature_keys: Vec<String> = features.iter().map(|(k, _)| k.to_string()).collect();
299 for feature_key in feature_keys {
300 if let Some(arr) = features
301 .get_mut(&feature_key)
302 .and_then(|v| v.as_array_mut())
303 {
304 arr.retain(|v| {
305 if let Some(s) = v.as_str() {
306 s != dep_enable && !s.starts_with(&dep_feature_prefix)
307 } else {
308 true
309 }
310 });
311 }
312 }
313 }
314 }
315}
316
317fn remove_from_ignored(doc: &mut DocumentMut, dep_name: &str, is_workspace: bool) {
319 let top_key = if is_workspace { "workspace" } else { "package" };
320 let ignored_array = doc[top_key]["metadata"]["xtask"]["unused-deps"]["ignored"]
321 .as_array_mut()
322 .unwrap();
323 ignored_array.retain(|v| v.as_str() != Some(dep_name));
324}
325
326struct StopAfterFirstMatch {
327 found: bool,
328}
329
330impl StopAfterFirstMatch {
331 fn new() -> Self {
332 Self { found: false }
333 }
334}
335
336impl grep_searcher::Sink for StopAfterFirstMatch {
337 type Error = Box<dyn std::error::Error>;
338
339 fn matched(
340 &mut self,
341 _searcher: &grep_searcher::Searcher,
342 mat: &grep_searcher::SinkMatch<'_>,
343 ) -> Result<bool, Self::Error> {
344 let mat = mat.bytes().trim_ascii();
345
346 if mat.starts_with(b"//")
347 && !(mat.starts_with(b"/// # use")
348 || mat.starts_with(b"/// use")
349 || mat.starts_with(b"//! # use")
350 || mat.starts_with(b"//! use"))
351 {
352 return Ok(true);
359 }
360
361 self.found = true;
364 Ok(false)
365 }
366}