Skip to main content

cargo/diagnostics/rules/
redundant_readme.rs

1use std::path::Path;
2
3use cargo_util_schemas::manifest::InheritableField;
4use cargo_util_schemas::manifest::StringOrBool;
5use cargo_util_schemas::manifest::TomlToolLints;
6use cargo_util_terminal::report::AnnotationKind;
7use cargo_util_terminal::report::Group;
8use cargo_util_terminal::report::Level;
9use cargo_util_terminal::report::Origin;
10use cargo_util_terminal::report::Patch;
11use cargo_util_terminal::report::Snippet;
12use tracing::instrument;
13
14use super::STYLE;
15use crate::CargoResult;
16use crate::GlobalContext;
17use crate::core::Package;
18use crate::diagnostics::DiagnosticStats;
19use crate::diagnostics::Lint;
20use crate::diagnostics::LintLevel;
21use crate::diagnostics::LintLevelSource;
22use crate::diagnostics::get_key_value_span;
23use crate::diagnostics::rel_cwd_manifest_path;
24use crate::util::toml::DEFAULT_README_FILES;
25
26pub static LINT: &Lint = &Lint {
27    name: "redundant_readme",
28    desc: "explicit `package.readme` can be inferred",
29    primary_group: &STYLE,
30    msrv: Some(super::CARGO_LINTS_MSRV),
31    feature_gate: None,
32    docs: Some(
33        r#"
34### What it does
35
36Checks for `package.readme` fields that can be inferred.
37
38See also [`package.readme` reference documentation](manifest.md#the-readme-field).
39
40### Why it is bad
41
42Adds boilerplate.
43
44### Drawbacks
45
46It might not be obvious if they named their file correctly.
47
48### Example
49
50```toml
51[package]
52name = "foo"
53readme = "README.md"
54```
55
56Should be written as:
57
58```toml
59[package]
60name = "foo"
61```
62"#,
63    ),
64};
65
66#[instrument(skip_all)]
67pub fn redundant_readme(
68    pkg: &Package,
69    manifest_path: &Path,
70    cargo_lints: &TomlToolLints,
71    stats: &mut DiagnosticStats,
72    gctx: &GlobalContext,
73) -> CargoResult<()> {
74    let (lint_level, source) = LINT.level(
75        cargo_lints,
76        pkg.rust_version(),
77        pkg.manifest().unstable_features(),
78    );
79
80    if lint_level == LintLevel::Allow {
81        return Ok(());
82    }
83
84    let manifest_path = rel_cwd_manifest_path(manifest_path, gctx);
85
86    lint_package(pkg, &manifest_path, lint_level, source, stats, gctx)
87}
88
89fn lint_package(
90    pkg: &Package,
91    manifest_path: &str,
92    lint_level: LintLevel,
93    source: LintLevelSource,
94    stats: &mut DiagnosticStats,
95    gctx: &GlobalContext,
96) -> CargoResult<()> {
97    let manifest = pkg.manifest();
98
99    // Must check `original_toml`, before any inferring happened
100    let Some(original_toml) = manifest.original_toml() else {
101        return Ok(());
102    };
103    let Some(original_pkg) = &original_toml.package else {
104        return Ok(());
105    };
106    let Some(readme) = &original_pkg.readme else {
107        return Ok(());
108    };
109
110    let InheritableField::Value(StringOrBool::String(readme)) = readme else {
111        // Not checking inheritance because at most one package can be identified from the lint and
112        // consistency of inheritance is likely best.
113        return Ok(());
114    };
115
116    if !DEFAULT_README_FILES.contains(&readme.as_str()) {
117        return Ok(());
118    }
119
120    let document = manifest.document();
121    let contents = manifest.contents();
122    let level = lint_level.to_diagnostic_level();
123    let emitted_source = LINT.emitted_source(lint_level, source);
124
125    let mut primary = Group::with_title(level.primary_title(LINT.desc));
126    if let Some(document) = document
127        && let Some(contents) = contents
128        && let Some(span) = get_key_value_span(document, &["package", "readme"])
129    {
130        let span = span.key.start..span.value.end;
131        primary = primary.element(
132            Snippet::source(contents)
133                .path(manifest_path)
134                .annotation(AnnotationKind::Primary.span(span)),
135        );
136    } else {
137        primary = primary.element(Origin::path(manifest_path));
138    }
139    primary = primary.element(Level::NOTE.message(emitted_source));
140    let mut report = vec![primary];
141    if let Some(document) = document
142        && let Some(contents) = contents
143        && let Some(span) = get_key_value_span(document, &["package", "readme"])
144    {
145        let span = if let Some(workspace_span) =
146            get_key_value_span(document, &["package", "readme", "workspace"])
147        {
148            span.key.start..workspace_span.value.end
149        } else {
150            span.key.start..span.value.end
151        };
152        let mut help =
153            Group::with_title(Level::HELP.secondary_title("consider removing `package.readme`"));
154        help = help.element(
155            Snippet::source(contents)
156                .path(manifest_path)
157                .patch(Patch::new(span, "")),
158        );
159        report.push(help);
160    }
161
162    stats.record_lint(lint_level);
163    gctx.shell().print_report(&report, lint_level.force())?;
164
165    Ok(())
166}