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