stern4rust/rules/source/readable_source_rule.rs
1// Copyright 2026 Umberto Gotti <umberto.gotti@umbertogotti.dev>
2// Licensed under the MIT License
3// SPDX-License-Identifier: MIT
4
5use syn::parse_file;
6
7use crate::reporting::offence::Offence;
8use crate::reporting::rule_explanation::RuleExplanation;
9use crate::rule::Rule;
10use crate::source_file::SourceFile;
11use syn::Error;
12
13// Every .rs file can be read and parsed.
14//
15// This rule exists because silence is indistinguishable from success. Every
16// other rule that parses gives up quietly on source it cannot read, trusting
17// rustc to say so more clearly -- which is right for a file somebody is
18// actively editing and wrong for a file nobody is looking at. A corrupted file
19// disappears from the report entirely and the package looks cleaner than it is.
20//
21// Not hypothetical: during development a test file became a run of NUL bytes
22// and the tool reported one fewer offence than the tree contained, with nothing
23// to indicate anything had been skipped.
24pub struct ReadableSourceRule;
25
26impl ReadableSourceRule {
27 // Shared with SourceReader, which reports the same rule for a file that
28 // could not be read at all -- there is no SourceFile to hand a rule in that
29 // case, but it is the same finding about the same tree.
30 pub const NAME: &'static str = "readable-source";
31
32 pub fn new() -> Self {
33 Self
34 }
35
36 // A parse error's span can be the call site rather than a real position, so
37 // line 0 is reported as line 1 instead of as a line no editor can go to.
38 fn line_of(error: &Error) -> usize {
39 error.span().start().line.max(1)
40 }
41}
42
43impl Default for ReadableSourceRule {
44 fn default() -> Self {
45 Self::new()
46 }
47}
48
49impl Rule for ReadableSourceRule {
50 fn name(&self) -> &'static str {
51 Self::NAME
52 }
53
54 fn check(&self, file: &SourceFile) -> Vec<Offence> {
55 match parse_file(&file.contents()) {
56 Ok(_) => Vec::new(),
57 Err(error) => vec![
58 Offence::new(
59 file.relative_path(),
60 Self::line_of(&error),
61 self.name(),
62 format!("file does not parse as Rust: {error}"),
63 "correct the syntax error rustc reports for this file, or restore \
64 the file if it is corrupted"
65 .to_string(),
66 )
67 .with_subject(file.relative_path()),
68 ],
69 }
70 }
71
72 fn check_workspace(&self, _files: &[SourceFile]) -> Vec<Offence> {
73 Vec::new()
74 }
75
76 fn requirement(&self) -> Option<&'static str> {
77 None
78 }
79
80 fn is_configured(&self) -> bool {
81 true
82 }
83
84 fn explanation(&self) -> RuleExplanation {
85 RuleExplanation::new(
86 self.name(),
87 "Every .rs file can be read and parsed.",
88 "fn broken( {",
89 "fn broken() {}",
90 )
91 }
92}