From 997a4a3091933a9f5771a96e6d493fc5824f8d1f Mon Sep 17 00:00:00 2001 From: Eduardo Broto Date: Mon, 25 May 2020 00:40:55 +0200 Subject: [PATCH 1/2] Add unknown_features lint --- CHANGELOG.md | 1 + clippy_lints/Cargo.toml | 1 + clippy_lints/src/lib.rs | 4 + clippy_lints/src/unknown_features.rs | 129 ++++++++++++++++++ src/lintlist/mod.rs | 7 + .../ui-cargo/unknown_features/fail/Cargo.toml | 11 ++ .../unknown_features/fail/src/main.rs | 15 ++ .../unknown_features/fail/src/main.stderr | 34 +++++ .../ui-cargo/unknown_features/pass/Cargo.toml | 14 ++ .../unknown_features/pass/src/main.rs | 16 +++ 10 files changed, 232 insertions(+) create mode 100644 clippy_lints/src/unknown_features.rs create mode 100644 tests/ui-cargo/unknown_features/fail/Cargo.toml create mode 100644 tests/ui-cargo/unknown_features/fail/src/main.rs create mode 100644 tests/ui-cargo/unknown_features/fail/src/main.stderr create mode 100644 tests/ui-cargo/unknown_features/pass/Cargo.toml create mode 100644 tests/ui-cargo/unknown_features/pass/src/main.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ac9057199ff..94b568c169d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1596,6 +1596,7 @@ Released 2018-09-13 [`unit_arg`]: https://rust-lang.github.io/rust-clippy/master/index.html#unit_arg [`unit_cmp`]: https://rust-lang.github.io/rust-clippy/master/index.html#unit_cmp [`unknown_clippy_lints`]: https://rust-lang.github.io/rust-clippy/master/index.html#unknown_clippy_lints +[`unknown_features`]: https://rust-lang.github.io/rust-clippy/master/index.html#unknown_features [`unnecessary_cast`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_cast [`unnecessary_filter_map`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_filter_map [`unnecessary_fold`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_fold diff --git a/clippy_lints/Cargo.toml b/clippy_lints/Cargo.toml index 1c0be7278346..6f61352cce91 100644 --- a/clippy_lints/Cargo.toml +++ b/clippy_lints/Cargo.toml @@ -29,6 +29,7 @@ smallvec = { version = "1", features = ["union"] } toml = "0.5.3" unicode-normalization = "0.1" semver = "0.9.0" +strsim = "0.10" # NOTE: cargo requires serde feat in its url dep # see url = { version = "2.1.0", features = ["serde"] } diff --git a/clippy_lints/src/lib.rs b/clippy_lints/src/lib.rs index 057d39d4c825..02dabe99ff93 100644 --- a/clippy_lints/src/lib.rs +++ b/clippy_lints/src/lib.rs @@ -317,6 +317,7 @@ mod trivially_copy_pass_by_ref; mod try_err; mod types; mod unicode; +mod unknown_features; mod unnamed_address; mod unsafe_removed_from_name; mod unused_io_amount; @@ -355,6 +356,7 @@ pub fn register_pre_expansion_lints(store: &mut rustc_lint::LintStore, conf: &Co }); store.register_pre_expansion_pass(|| box attrs::EarlyAttributes); store.register_pre_expansion_pass(|| box dbg_macro::DbgMacro); + store.register_pre_expansion_pass(|| box unknown_features::UnknownFeatures::default()); } #[doc(hidden)] @@ -835,6 +837,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf: &unicode::NON_ASCII_LITERAL, &unicode::UNICODE_NOT_NFC, &unicode::ZERO_WIDTH_SPACE, + &unknown_features::UNKNOWN_FEATURES, &unnamed_address::FN_ADDRESS_COMPARISONS, &unnamed_address::VTABLE_ADDRESS_COMPARISONS, &unsafe_removed_from_name::UNSAFE_REMOVED_FROM_NAME, @@ -1703,6 +1706,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf: store.register_group(true, "clippy::cargo", Some("clippy_cargo"), vec![ LintId::of(&cargo_common_metadata::CARGO_COMMON_METADATA), LintId::of(&multiple_crate_versions::MULTIPLE_CRATE_VERSIONS), + LintId::of(&unknown_features::UNKNOWN_FEATURES), LintId::of(&wildcard_dependencies::WILDCARD_DEPENDENCIES), ]); diff --git a/clippy_lints/src/unknown_features.rs b/clippy_lints/src/unknown_features.rs new file mode 100644 index 000000000000..c1cc8a4b9685 --- /dev/null +++ b/clippy_lints/src/unknown_features.rs @@ -0,0 +1,129 @@ +use rustc_ast::ast::{Attribute, Crate, MacCall, MetaItem, MetaItemKind}; +use rustc_data_structures::fx::FxHashSet; +use rustc_errors::Applicability; +use rustc_lint::{EarlyContext, EarlyLintPass}; +use rustc_parse::{self, MACRO_ARGUMENTS}; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::source_map::DUMMY_SP; + +use crate::utils::{span_lint, span_lint_and_then}; +use cargo_metadata::MetadataCommand; +use strsim::normalized_damerau_levenshtein; + +declare_clippy_lint! { + /// **What it does:** Finds references to features not defined in the cargo manifest file. + /// + /// **Why is this bad?** The referred feature will not be recognised and the related item will not be included + /// by the conditional compilation engine. + /// + /// **Known problems:** None. + /// + /// **Example:** + /// + /// ```rust + /// #[cfg(feature = "unknown")] + /// fn example() { } + /// ``` + pub UNKNOWN_FEATURES, + cargo, + "usage of features not defined in the cargo manifest file" +} + +#[derive(Default)] +pub struct UnknownFeatures { + features: FxHashSet, +} + +impl_lint_pass!(UnknownFeatures => [UNKNOWN_FEATURES]); + +impl EarlyLintPass for UnknownFeatures { + fn check_crate(&mut self, cx: &EarlyContext<'_>, _: &Crate) { + fn transform_feature(name: &str, pkg: &str, local_pkg: &str) -> String { + if pkg == local_pkg { + name.into() + } else { + format!("{}/{}", pkg, name) + } + } + + let metadata = if let Ok(metadata) = MetadataCommand::new().exec() { + metadata + } else { + span_lint(cx, UNKNOWN_FEATURES, DUMMY_SP, "could not read cargo metadata"); + return; + }; + + if let Some(local_pkg) = &cx.sess.opts.crate_name { + for pkg in metadata.packages { + self.features.extend( + pkg.features + .keys() + .map(|name| transform_feature(name, &pkg.name, local_pkg)), + ); + } + } + } + + fn check_attribute(&mut self, cx: &EarlyContext<'_>, attr: &Attribute) { + if attr.check_name(sym!(cfg)) { + if let Some(item) = &attr.meta() { + self.walk_cfg_metas(cx, item); + } + } + } + + fn check_mac(&mut self, cx: &EarlyContext<'_>, mac: &MacCall) { + if mac.path == sym!(cfg) { + let tts = mac.args.inner_tokens(); + let mut parser = rustc_parse::stream_to_parser(&cx.sess.parse_sess, tts, MACRO_ARGUMENTS); + if let Ok(item) = parser.parse_meta_item() { + self.walk_cfg_metas(cx, &item); + } + } + } +} + +impl UnknownFeatures { + fn walk_cfg_metas(&mut self, cx: &EarlyContext<'_>, item: &MetaItem) { + match &item.kind { + MetaItemKind::List(items) => { + for nested in items { + if let Some(item) = nested.meta_item() { + self.walk_cfg_metas(cx, item); + } + } + }, + MetaItemKind::NameValue(lit) if item.name_or_empty().as_str() == "feature" => { + if let Some(value) = item.value_str() { + let feature = &*value.as_str(); + if !self.features.contains(feature) { + let message = format!("unknown feature `{}`", feature); + span_lint_and_then(cx, UNKNOWN_FEATURES, lit.span, &message, |diag| { + if let Some(similar_name) = self.find_similar_name(feature) { + diag.span_suggestion( + lit.span, + "a feature with a similar name exists", + format!("\"{}\"", similar_name), + Applicability::MaybeIncorrect, + ); + } + }); + } + } + }, + _ => {}, + } + } + + fn find_similar_name(&self, name: &str) -> Option { + let mut similar: Vec<_> = self + .features + .iter() + .map(|f| (f, normalized_damerau_levenshtein(name, f))) + .filter(|(_, sim)| *sim >= 0.7) + .collect(); + + similar.sort_by(|(_, a), (_, b)| b.partial_cmp(a).unwrap()); + similar.into_iter().next().map(|(f, _)| f.clone()) + } +} diff --git a/src/lintlist/mod.rs b/src/lintlist/mod.rs index 8211a57b5643..7b82da6bd960 100644 --- a/src/lintlist/mod.rs +++ b/src/lintlist/mod.rs @@ -2257,6 +2257,13 @@ pub static ref ALL_LINTS: Vec = vec![ deprecation: None, module: "attrs", }, + Lint { + name: "unknown_features", + group: "cargo", + desc: "usage of features not defined in the cargo manifest file", + deprecation: None, + module: "unknown_features", + }, Lint { name: "unnecessary_cast", group: "complexity", diff --git a/tests/ui-cargo/unknown_features/fail/Cargo.toml b/tests/ui-cargo/unknown_features/fail/Cargo.toml new file mode 100644 index 000000000000..f4bbf9ce6fd0 --- /dev/null +++ b/tests/ui-cargo/unknown_features/fail/Cargo.toml @@ -0,0 +1,11 @@ + +# Features referenced in the code are not found in this manifest + +[package] +name = "unknown_features" +version = "0.1.0" +publish = false + +[features] +misspelled = [] +another = [] diff --git a/tests/ui-cargo/unknown_features/fail/src/main.rs b/tests/ui-cargo/unknown_features/fail/src/main.rs new file mode 100644 index 000000000000..b17d43c7156a --- /dev/null +++ b/tests/ui-cargo/unknown_features/fail/src/main.rs @@ -0,0 +1,15 @@ +// compile-flags: --crate-name=unknown_features --cfg feature="misspelled" --cfg feature="another" +#![warn(clippy::unknown_features)] + +fn main() { + #[cfg(feature = "mispelled")] + let _ = 42; + + #[cfg(feature = "dependency/unknown")] + let _ = 42; + + #[cfg(any(not(feature = "misspeled"), feature = "not-found"))] + let _ = 21; + + if cfg!(feature = "nothe") {} +} diff --git a/tests/ui-cargo/unknown_features/fail/src/main.stderr b/tests/ui-cargo/unknown_features/fail/src/main.stderr new file mode 100644 index 000000000000..9a5fae1f067c --- /dev/null +++ b/tests/ui-cargo/unknown_features/fail/src/main.stderr @@ -0,0 +1,34 @@ +error: unknown feature `mispelled` + --> $DIR/main.rs:5:21 + | +LL | #[cfg(feature = "mispelled")] + | ^^^^^^^^^^^ help: a feature with a similar name exists: `"misspelled"` + | + = note: `-D clippy::unknown-features` implied by `-D warnings` + +error: unknown feature `dependency/unknown` + --> $DIR/main.rs:8:21 + | +LL | #[cfg(feature = "dependency/unknown")] + | ^^^^^^^^^^^^^^^^^^^^ + +error: unknown feature `misspeled` + --> $DIR/main.rs:11:29 + | +LL | #[cfg(any(not(feature = "misspeled"), feature = "not-found"))] + | ^^^^^^^^^^^ help: a feature with a similar name exists: `"misspelled"` + +error: unknown feature `not-found` + --> $DIR/main.rs:11:53 + | +LL | #[cfg(any(not(feature = "misspeled"), feature = "not-found"))] + | ^^^^^^^^^^^ + +error: unknown feature `nothe` + --> $DIR/main.rs:14:23 + | +LL | if cfg!(feature = "nothe") {} + | ^^^^^^^ help: a feature with a similar name exists: `"another"` + +error: aborting due to 5 previous errors + diff --git a/tests/ui-cargo/unknown_features/pass/Cargo.toml b/tests/ui-cargo/unknown_features/pass/Cargo.toml new file mode 100644 index 000000000000..381bcb6594be --- /dev/null +++ b/tests/ui-cargo/unknown_features/pass/Cargo.toml @@ -0,0 +1,14 @@ + +# Features from this crate and from dependencies are correctly referenced in the code + +[package] +name = "unknown_features" +version = "0.1.0" +publish = false + +[dependencies] +serde = { version = "1.0.110", features = ["derive"] } + +[features] +fancy = [] +another = [] diff --git a/tests/ui-cargo/unknown_features/pass/src/main.rs b/tests/ui-cargo/unknown_features/pass/src/main.rs new file mode 100644 index 000000000000..dd27523ee598 --- /dev/null +++ b/tests/ui-cargo/unknown_features/pass/src/main.rs @@ -0,0 +1,16 @@ +// compile-flags: --crate-name=unknown_features --cfg feature="fancy" --cfg feature="another" +// compile-flags: --cfg feature="serde/derive" +#![warn(clippy::unknown_features)] + +fn main() { + #[cfg(feature = "fancy")] + let _ = 42; + + #[cfg(feature = "serde/derive")] + let _ = 42; + + #[cfg(any(not(feature = "fancy"), feature = "another"))] + let _ = 21; + + if cfg!(feature = "fancy") {} +} From 492a49e4445ba27c11cf20d28a288cf18306e3cc Mon Sep 17 00:00:00 2001 From: Eduardo Broto Date: Mon, 25 May 2020 00:41:13 +0200 Subject: [PATCH 2/2] cargo-ui tests: check that /src exists before processing test --- tests/compile-test.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/compile-test.rs b/tests/compile-test.rs index a5de84293909..74705fa079ac 100644 --- a/tests/compile-test.rs +++ b/tests/compile-test.rs @@ -181,8 +181,11 @@ fn run_ui_cargo(config: &mut compiletest::Config) { } let src_path = case.path().join("src"); - env::set_current_dir(&src_path)?; + if !src_path.exists() { + continue; + } + env::set_current_dir(&src_path)?; for file in fs::read_dir(&src_path)? { let file = file?; if file.file_type()?.is_dir() {