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