From 997a4a3091933a9f5771a96e6d493fc5824f8d1f Mon Sep 17 00:00:00 2001
From: Eduardo Broto <ebroto@tutanota.com>
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 <https://github.com/rust-lang/rust/pull/63587#issuecomment-522343864>
 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<String>,
+}
+
+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<String> {
+        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<Lint> = 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 <ebroto@tutanota.com>
Date: Mon, 25 May 2020 00:41:13 +0200
Subject: [PATCH 2/2] cargo-ui tests: check that <dir>/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() {