Skip to main content

stern4rust/rules/source/
imported_paths_rule.rs

1// Copyright 2025 Umberto Gotti <umberto.gotti@umbertogotti.dev>
2// Licensed under the MIT License
3// SPDX-License-Identifier: MIT
4
5use crate::finding::model::qualified_call::QualifiedCall;
6use crate::finding::parsing::qualified_call_finder::QualifiedCallFinder;
7use crate::reporting::offence::Offence;
8use crate::rule::Rule;
9use crate::source_file::SourceFile;
10
11// A function is called through a name this file imported, not through a path.
12//
13// `use` statements are a file's list of dependencies. A path written inline at
14// the call site is a dependency that never appears on that list, so the list
15// stops being an answer to what this file needs -- and the reader who scans the
16// top of the file to find out is quietly given a wrong answer.
17//
18// One imported segment is allowed and is the point rather than an exception.
19// `use std::fs` followed by `fs::read_to_string` names the route once at the top
20// and still says at the call site which module the function came from. What the
21// rule removes is the route being respelt at every call: `std::env::args`, or a
22// `syn::parse_file` whose crate no line in the file mentions.
23//
24// Both productive and test files are checked. A test file has the same reader.
25pub struct ImportedPathsRule;
26
27impl ImportedPathsRule {
28    pub fn new() -> Self {
29        Self
30    }
31
32    fn offence(&self, file: &SourceFile, call: &QualifiedCall) -> Offence {
33        Offence::new(
34            file.relative_path(),
35            call.line,
36            self.name(),
37            format!(
38                "`{}` is reached through a path; no import of this file names it",
39                call.path
40            ),
41            format!("add `use {};` and call `{}`", call.import(), call.call()),
42        )
43        .with_subject(&call.path)
44    }
45}
46
47impl Default for ImportedPathsRule {
48    fn default() -> Self {
49        Self::new()
50    }
51}
52
53impl Rule for ImportedPathsRule {
54    fn name(&self) -> &'static str {
55        "imported-paths"
56    }
57
58    fn check(&self, file: &SourceFile) -> Vec<Offence> {
59        QualifiedCallFinder::find(file)
60            .unwrap_or_default()
61            .iter()
62            .map(|call| self.offence(file, call))
63            .collect()
64    }
65
66    fn check_workspace(&self, _files: &[SourceFile]) -> Vec<Offence> {
67        Vec::new()
68    }
69
70    fn requirement(&self) -> Option<&'static str> {
71        None
72    }
73
74    fn is_configured(&self) -> bool {
75        true
76    }
77}