stern4rust/rules/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::qualified_call::QualifiedCall;
6use crate::finding::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}