Skip to main content

stern4rust/rules/
readable_source_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::offence::Offence;
6use crate::rule::Rule;
7use crate::source_file::SourceFile;
8
9// Every .rs file can be read and parsed.
10//
11// This rule exists because silence is indistinguishable from success. Every
12// other rule that parses gives up quietly on source it cannot read, trusting
13// rustc to say so more clearly -- which is right for a file somebody is
14// actively editing and wrong for a file nobody is looking at. A corrupted file
15// disappears from the report entirely and the package looks cleaner than it is.
16//
17// Not hypothetical: during development a test file became a run of NUL bytes
18// and the tool reported one fewer offence than the tree contained, with nothing
19// to indicate anything had been skipped.
20pub struct ReadableSourceRule;
21
22impl ReadableSourceRule {
23    // Shared with SourceReader, which reports the same rule for a file that
24    // could not be read at all -- there is no SourceFile to hand a rule in that
25    // case, but it is the same finding about the same tree.
26    pub const NAME: &'static str = "readable-source";
27
28    pub fn new() -> Self {
29        Self
30    }
31
32    // A parse error's span can be the call site rather than a real position, so
33    // line 0 is reported as line 1 instead of as a line no editor can go to.
34    fn line_of(error: &syn::Error) -> usize {
35        error.span().start().line.max(1)
36    }
37}
38
39impl Default for ReadableSourceRule {
40    fn default() -> Self {
41        Self::new()
42    }
43}
44
45impl Rule for ReadableSourceRule {
46    fn name(&self) -> &'static str {
47        Self::NAME
48    }
49
50    fn check(&self, file: &SourceFile) -> Vec<Offence> {
51        match syn::parse_file(&file.contents()) {
52            Ok(_) => Vec::new(),
53            Err(error) => vec![
54                Offence::new(
55                    file.relative_path(),
56                    Self::line_of(&error),
57                    self.name(),
58                    format!("file does not parse as Rust: {error}"),
59                    "correct the syntax error rustc reports for this file, or restore \
60                     the file if it is corrupted"
61                        .to_string(),
62                )
63                .with_subject(file.relative_path()),
64            ],
65        }
66    }
67}