1
use crate::config::Config;
2
use crate::linter::{Lint, Linter};
3
use crate::vcs::{Section, VCS};
4
use std::path::PathBuf;
5

6
pub struct Scout<V, C, L>
7
where
8
    V: VCS,
9
    C: Config,
10
    L: Linter,
11
{
12
    vcs: V,
13
    config: C,
14
    linter: L,
15
}
16

17
impl<V, C, L> Scout<V, C, L>
18
where
19
    V: VCS,
20
    C: Config,
21
    L: Linter,
22
{
23 1
    pub fn new(vcs: V, config: C, linter: L) -> Self {
24
        Self {
25
            vcs,
26
            config,
27
            linter,
28
        }
29
    }
30
    #[allow(clippy::missing_errors_doc)]
31 1
    pub fn run(&self) -> Result<Vec<Lint>, crate::error::Error> {
32 1
        let current_dir = std::fs::canonicalize(std::env::current_dir()?)?;
33 1
        let diff_sections = self.vcs.sections(current_dir.clone())?;
34 1
        let mut lints = Vec::new();
35 1
        let config_members = self.config.members();
36 1
        let members = config_members.iter().map(|m| {
37 1
            let mut member = current_dir.clone();
38 1
            member.push(m);
39 1
            member
40
        });
41
        // There's no need to run the linter on members where no changes have been made
42 1
        let relevant_members = members.filter(|m| diff_in_member(m, &diff_sections));
43 1
        for m in relevant_members {
44 1
            lints.extend(self.linter.lints(current_dir.clone().join(m))?);
45
        }
46 1
        println!("[Scout] - checking for intersections");
47 1
        Ok(lints_from_diff(&lints, &diff_sections))
48
    }
49
}
50

51 1
fn diff_in_member(member: &PathBuf, sections: &[Section]) -> bool {
52 1
    if let Some(m) = member.to_str() {
53 1
        for s in sections {
54 1
            if s.file_name.starts_with(&m) {
55 1
                return true;
56
            }
57
        }
58
    }
59 1
    false
60
}
61

62
// Check if lint and git_section have overlapped lines
63 1
fn lines_in_range(lint: &Lint, git_section: &Section) -> bool {
64
    // If git_section.line_start is included in the lint span
65 1
    lint.location.lines[0] <= git_section.line_start && git_section.line_start <= lint.location.lines[1] ||
66
    // If lint.line_start is included in the git_section span
67 1
    git_section.line_start <= lint.location.lines[0] && lint.location.lines[0] <= git_section.line_end
68
}
69

70 1
fn files_match(lint: &Lint, git_section: &Section) -> bool {
71
    // Git diff paths and clippy paths don't get along too well on Windows...
72 1
    lint.location.path.replace("\\", "/") == git_section.file_name.replace("\\", "/")
73
}
74

75 1
fn lints_from_diff(lints: &[Lint], diffs: &[Section]) -> Vec<Lint> {
76 1
    let mut lints_in_diff = Vec::new();
77 1
    for diff in diffs {
78 1
        let diff_lints = lints
79
            .iter()
80 1
            .filter(|lint| files_match(&lint, &diff) && lines_in_range(&lint, &diff));
81 1
        for l in diff_lints {
82 1
            lints_in_diff.push(l.clone());
83
        }
84
    }
85 1
    lints_in_diff
86
}
87

88
#[cfg(test)]
89
mod scout_tests {
90
    use super::{Scout, Section, VCS};
91
    use crate::config::Config;
92
    use crate::error::Error;
93
    use crate::linter::{Lint, Linter, Location};
94
    use crate::utils::get_absolute_file_path;
95
    use std::cell::RefCell;
96
    use std::clone::Clone;
97
    use std::path::{Path, PathBuf};
98
    use std::rc::Rc;
99
    struct TestVCS {
100
        sections: Vec<Section>,
101
        sections_called: RefCell<bool>,
102
    }
103
    impl TestVCS {
104 1
        pub fn new(sections: Vec<Section>) -> Self {
105
            Self {
106
                sections,
107 1
                sections_called: RefCell::new(false),
108
            }
109
        }
110
    }
111
    impl VCS for TestVCS {
112 1
        fn sections<P: AsRef<Path>>(&self, _: P) -> Result<Vec<Section>, Error> {
113 1
            *self.sections_called.borrow_mut() = true;
114 1
            Ok(self.sections.clone())
115
        }
116
    }
117
    struct TestLinter {
118
        // Using a RefCell here because lints
119
        // takes &self and not &mut self.
120
        // We use usize here because we will compare it to a Vec::len()
121
        lints_times_called: Rc<RefCell<usize>>,
122
        lints: Vec<Lint>,
123
    }
124
    impl TestLinter {
125 1
        pub fn new() -> Self {
126
            Self {
127 1
                lints_times_called: Rc::new(RefCell::new(0)),
128 1
                lints: Vec::new(),
129
            }
130
        }
131

132 1
        pub fn with_lints(lints: Vec<Lint>) -> Self {
133
            Self {
134 1
                lints_times_called: Rc::new(RefCell::new(0)),
135
                lints,
136
            }
137
        }
138
    }
139
    impl Linter for TestLinter {
140 1
        fn lints(
141
            &self,
142
            _working_dir: impl Into<PathBuf>,
143
        ) -> Result<Vec<Lint>, crate::error::Error> {
144 1
            *self.lints_times_called.borrow_mut() += 1;
145 1
            Ok(self.lints.clone())
146
        }
147
    }
148
    struct TestConfig {
149
        members: Vec<String>,
150
    }
151
    impl TestConfig {
152 1
        pub fn new(members: Vec<String>) -> Self {
153
            Self { members }
154
        }
155
    }
156
    impl Config for TestConfig {
157 1
        fn members(&self) -> Vec<String> {
158 1
            self.members.clone()
159
        }
160
    }
161

162
    #[test]
163 1
    fn test_scout_no_workspace_no_diff() -> Result<(), crate::error::Error> {
164 1
        let linter = TestLinter::new();
165 1
        let vcs = TestVCS::new(Vec::new());
166
        // No members so we won't have to iterate
167 1
        let config = TestConfig::new(Vec::new());
168 1
        let expected_times_called = 0;
169 1
        let actual_times_called = Rc::clone(&linter.lints_times_called);
170 1
        let scout = Scout::new(vcs, config, linter);
171
        // We don't check for the lints result here.
172
        // It is already tested in the linter tests
173
        // and in intersection tests
174 1
        let _ = scout.run()?;
175 1
        assert_eq!(expected_times_called, *actual_times_called.borrow());
176 1
        Ok(())
177
    }
178

179
    #[test]
180 1
    fn test_scout_no_workspace_one_diff() -> Result<(), crate::error::Error> {
181 1
        let diff = vec![Section {
182 1
            file_name: get_absolute_file_path("foo/bar.rs")?,
183
            line_start: 0,
184
            line_end: 10,
185
        }];
186

187 1
        let lints = vec![
188 1
            Lint {
189 1
                location: Location {
190 1
                    lines: [2, 2],
191 1
                    path: get_absolute_file_path("foo/bar.rs")?,
192
                },
193 1
                message: "Test lint".to_string(),
194
            },
195 1
            Lint {
196 1
                location: Location {
197 1
                    lines: [12, 22],
198 1
                    path: get_absolute_file_path("foo/bar.rs")?,
199
                },
200 1
                message: "This lint is not in diff".to_string(),
201
            },
202
        ];
203

204 1
        let expected_lints_from_diff = vec![Lint {
205 1
            location: Location {
206 1
                lines: [2, 2],
207 1
                path: get_absolute_file_path("foo/bar.rs")?,
208
            },
209 1
            message: "Test lint".to_string(),
210
        }];
211

212 1
        let linter = TestLinter::with_lints(lints);
213 1
        let vcs = TestVCS::new(diff);
214
        // The member matches the file name
215 1
        let config = TestConfig::new(vec!["foo".to_string()]);
216 1
        let expected_times_called = 1;
217 1
        let actual_times_called = Rc::clone(&linter.lints_times_called);
218 1
        let scout = Scout::new(vcs, config, linter);
219
        // We don't check for the lints result here.
220
        // It is already tested in the linter tests
221
        // and in intersection tests
222 1
        let actual_lints_from_diff = scout.run()?;
223 1
        assert_eq!(expected_times_called, *actual_times_called.borrow());
224 1
        assert_eq!(expected_lints_from_diff, actual_lints_from_diff);
225 1
        Ok(())
226
    }
227

228
    #[test]
229 1
    fn test_scout_no_workspace_one_diff_not_relevant_member() -> Result<(), crate::error::Error> {
230 1
        let diff = vec![Section {
231 1
            file_name: get_absolute_file_path("baz/bar.rs")?,
232
            line_start: 0,
233
            line_end: 10,
234
        }];
235 1
        let linter = TestLinter::new();
236 1
        let vcs = TestVCS::new(diff);
237
        // The member does not match the file name
238 1
        let config = TestConfig::new(vec!["foo".to_string()]);
239 1
        let expected_times_called = 0;
240 1
        let actual_times_called = Rc::clone(&linter.lints_times_called);
241 1
        let scout = Scout::new(vcs, config, linter);
242
        // We don't check for the lints result here.
243
        // It is already tested in the linter tests
244
        // and in intersection tests
245 1
        let _ = scout.run()?;
246 1
        assert_eq!(expected_times_called, *actual_times_called.borrow());
247 1
        Ok(())
248
    }
249

250
    #[test]
251 1
    fn test_scout_in_workspace() -> Result<(), crate::error::Error> {
252 1
        let diff = vec![
253 1
            Section {
254 1
                file_name: get_absolute_file_path("member1/bar.rs")?,
255
                line_start: 0,
256
                line_end: 10,
257
            },
258 1
            Section {
259 1
                file_name: get_absolute_file_path("member2/bar.rs")?,
260
                line_start: 0,
261
                line_end: 10,
262
            },
263
        ];
264 1
        let linter = TestLinter::new();
265 1
        let vcs = TestVCS::new(diff);
266
        // The config has members, we will iterate
267 1
        let config = TestConfig::new(vec![
268 1
            "member1".to_string(),
269 1
            "member2".to_string(),
270 1
            "member3".to_string(),
271
        ]);
272
        // We should run the linter on member1 and member2
273 1
        let expected_times_called = 2;
274 1
        let actual_times_called = Rc::clone(&linter.lints_times_called);
275 1
        let scout = Scout::new(vcs, config, linter);
276
        // We don't check for the lints result here.
277
        // It is already tested in the linter tests
278
        // and in intersection tests
279 1
        let _ = scout.run()?;
280

281 1
        assert_eq!(expected_times_called, *actual_times_called.borrow());
282 1
        Ok(())
283
    }
284
}
285

286
#[cfg(test)]
287
mod intersections_tests {
288
    use crate::linter::{Lint, Location};
289
    use crate::vcs::Section;
290

291
    type TestSection = (&'static str, u32, u32);
292
    #[test]
293

294 1
    fn test_files_match() {
295 1
        let files_to_test = vec![
296 1
            (("foo.rs", 1, 10), ("foo.rs", 5, 12)),
297 1
            (("bar.rs", 1, 10), ("bar.rs", 5, 12)),
298 1
            (("foo/bar/baz.rs", 1, 10), ("foo/bar/baz.rs", 5, 12)),
299 1
            (("foo\\bar\\baz.rs", 1, 10), ("foo/bar/baz.rs", 9, 12)),
300 1
            (("foo/1.rs", 1, 10), ("foo/1.rs", 5, 12)),
301
        ];
302 1
        assert_all_files_match(files_to_test);
303
    }
304

305
    #[test]
306 1
    fn test_files_dont_match() {
307 1
        let files_to_test = vec![
308 1
            (("foo.rs", 1, 10), ("foo1.rs", 5, 12)),
309 1
            (("bar.rs", 1, 10), ("baz.rs", 5, 12)),
310 1
            (("bar.rs", 1, 10), ("bar.js", 5, 12)),
311 1
            (("foo/bar/baz.rs", 1, 10), ("/foo/bar/baz.rs", 5, 12)),
312 1
            (("foo\\\\bar\\baz.rs", 1, 10), ("foo/bar/baz.rs", 9, 12)),
313 1
            (("foo/1.rs", 1, 10), ("foo/2.rs", 5, 12)),
314
        ];
315 1
        assert_no_files_match(files_to_test);
316
    }
317

318
    #[test]
319 1
    fn test_lines_in_range_simple() {
320 1
        let ranges_to_test = vec![
321 1
            (("foo.rs", 1, 10), ("foo.rs", 5, 12)),
322 1
            (("foo.rs", 1, 10), ("foo.rs", 5, 11)),
323 1
            (("foo.rs", 1, 10), ("foo.rs", 10, 19)),
324 1
            (("foo.rs", 1, 10), ("foo.rs", 9, 12)),
325 1
            (("foo.rs", 8, 16), ("foo.rs", 5, 12)),
326
        ];
327 1
        assert_all_in_range(ranges_to_test);
328
    }
329

330
    #[test]
331 1
    fn test_lines_not_in_range_simple() {
332 1
        let ranges_to_test = vec![
333 1
            (("foo.rs", 1, 10), ("foo.rs", 11, 12)),
334 1
            (("foo.rs", 2, 10), ("foo.rs", 0, 1)),
335 1
            (("foo.rs", 15, 20), ("foo.rs", 21, 30)),
336 1
            (("foo.rs", 15, 20), ("foo.rs", 10, 14)),
337 1
            (("foo.rs", 1, 1), ("foo.rs", 2, 2)),
338
        ];
339 1
        assert_all_not_in_range(ranges_to_test);
340
    }
341

342 1
    fn assert_all_files_match(ranges: Vec<(TestSection, TestSection)>) {
343
        use crate::scout::files_match;
344 1
        for range in ranges {
345 1
            let lint_section = range.0;
346 1
            let git_section = range.1;
347
            let lint = Lint {
348 1
                message: String::new(),
349 1
                location: Location {
350
                    path: String::from(lint_section.0),
351
                    lines: [lint_section.1, lint_section.2],
352
                },
353
            };
354
            let git = Section {
355 1
                file_name: String::from(git_section.0),
356 1
                line_start: git_section.1,
357 1
                line_end: git_section.2,
358
            };
359 1
            assert!(
360 1
                files_match(&lint, &git),
361 0
                print!(
362 0
                    "Expected files match for {} and {}",
363
                    lint_section.0, git_section.0
364
                )
365
            );
366
        }
367
    }
368

369 1
    fn assert_no_files_match(ranges: Vec<(TestSection, TestSection)>) {
370
        use crate::scout::files_match;
371 1
        for range in ranges {
372 1
            let lint_section = range.0;
373 1
            let git_section = range.1;
374
            let lint = Lint {
375 1
                message: String::new(),
376 1
                location: Location {
377
                    path: String::from(lint_section.0),
378
                    lines: [lint_section.1, lint_section.2],
379
                },
380
            };
381
            let git = Section {
382 1
                file_name: String::from(git_section.0),
383 1
                line_start: git_section.1,
384 1
                line_end: git_section.2,
385
            };
386 1
            assert!(
387 1
                !files_match(&lint, &git),
388 0
                print!(
389 0
                    "Expected files not to match for {} and {}",
390
                    lint_section.0, git_section.0
391
                )
392
            );
393
        }
394
    }
395

396 1
    fn assert_all_in_range(ranges: Vec<(TestSection, TestSection)>) {
397 1
        for range in ranges {
398 1
            let lint = range.0;
399 1
            let section = range.1;
400 1
            assert!(
401 1
                in_range(lint, section),
402 0
                print!(
403 0
                    "Expected in range, found not in range for \n {:#?} and {:#?}",
404
                    lint, section
405
                )
406
            );
407
        }
408
    }
409

410 1
    fn assert_all_not_in_range(ranges: Vec<(TestSection, TestSection)>) {
411 1
        for range in ranges {
412 1
            let lint = range.0;
413 1
            let section = range.1;
414 1
            assert!(
415 1
                !in_range(lint, section),
416 0
                print!(
417 0
                    "Expected not in range, found in range for \n {:#?} and {:#?}",
418
                    lint, section
419
                )
420
            );
421
        }
422
    }
423

424 1
    fn in_range(lint_section: (&str, u32, u32), git_section: (&str, u32, u32)) -> bool {
425
        use crate::scout::lines_in_range;
426
        let lint = Lint {
427 1
            message: String::new(),
428 1
            location: Location {
429
                path: String::from(lint_section.0),
430
                lines: [lint_section.1, lint_section.2],
431
            },
432
        };
433

434
        let git_section = Section {
435 1
            file_name: String::from(git_section.0),
436 1
            line_start: git_section.1,
437 1
            line_end: git_section.2,
438
        };
439 1
        lines_in_range(&lint, &git_section)
440
    }
441
}

Read our documentation on viewing source code .

Loading