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