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