From f991ebaa7810ff3f6254baf2350c7185dce23e58 Mon Sep 17 00:00:00 2001
From: Chris Wong <lambda.fairy@gmail.com>
Date: Mon, 26 Dec 2016 22:04:45 +1300
Subject: [PATCH] Lint against `PreEscaped("<!DOCTYPE html>")`

See #66
---
 maud_macros/src/lib.rs                |  4 ++
 maud_macros/src/lints/doctype_html.rs | 42 ++++++++++++
 maud_macros/src/lints/mod.rs          | 14 ++++
 maud_macros/src/lints/util.rs         | 92 +++++++++++++++++++++++++++
 4 files changed, 152 insertions(+)
 create mode 100644 maud_macros/src/lints/doctype_html.rs
 create mode 100644 maud_macros/src/lints/mod.rs
 create mode 100644 maud_macros/src/lints/util.rs

diff --git a/maud_macros/src/lib.rs b/maud_macros/src/lib.rs
index 25b6789..6db3dc5 100644
--- a/maud_macros/src/lib.rs
+++ b/maud_macros/src/lib.rs
@@ -3,6 +3,8 @@
 #![feature(slice_patterns)]
 #![feature(rustc_private)]
 
+#[macro_use]
+extern crate rustc;
 extern crate rustc_plugin;
 extern crate syntax;
 extern crate maud;
@@ -14,6 +16,7 @@ use syntax::ext::base::{DummyResult, ExtCtxt, MacEager, MacResult};
 use syntax::print::pprust;
 use syntax::tokenstream::TokenTree;
 
+mod lints;
 mod parse;
 mod render;
 
@@ -41,4 +44,5 @@ fn expand_html_debug<'cx>(cx: &'cx mut ExtCtxt, sp: Span, args: &[TokenTree]) ->
 pub fn plugin_registrar(reg: &mut Registry) {
     reg.register_macro("html", expand_html);
     reg.register_macro("html_debug", expand_html_debug);
+    lints::register_lints(reg);
 }
diff --git a/maud_macros/src/lints/doctype_html.rs b/maud_macros/src/lints/doctype_html.rs
new file mode 100644
index 0000000..1a9ff8a
--- /dev/null
+++ b/maud_macros/src/lints/doctype_html.rs
@@ -0,0 +1,42 @@
+use rustc::hir::{Expr, ExprCall, ExprLit, ExprPath};
+use rustc::lint::{LateContext, LateLintPass, LintArray, LintContext, LintPass};
+use std::ascii::AsciiExt;
+use super::util::match_def_path;
+use syntax::ast::LitKind;
+
+declare_lint! {
+    pub MAUD_DOCTYPE_HTML,
+    Warn,
+    "suggest using the maud::DOCTYPE_HTML constant"
+}
+
+pub struct DoctypeHtml;
+
+impl LintPass for DoctypeHtml {
+    fn get_lints(&self) -> LintArray {
+        lint_array![MAUD_DOCTYPE_HTML]
+    }
+}
+
+impl<'a, 'tcx> LateLintPass<'a, 'tcx> for DoctypeHtml {
+    fn check_expr(&mut self, cx: &LateContext<'a, 'tcx>, expr: &'tcx Expr) {
+        if_let_chain! {[
+            // It's a function call...
+            let ExprCall(ref path_expr, ref args) = expr.node,
+            // ... where the argument is a literal "<!doctype html>"
+            let Some(first_arg) = args.first(),
+            let ExprLit(ref lit) = first_arg.node,
+            let LitKind::Str(s, _) = lit.node,
+            s.as_str().eq_ignore_ascii_case("<!doctype html>"),
+        ], {
+            // ... and the callee is `maud::PreEscaped`
+            if let ExprPath(ref qpath) = path_expr.node {
+                let def_id = cx.tcx.tables().qpath_def(qpath, path_expr.id).def_id();
+                if match_def_path(cx, def_id, &["maud", "PreEscaped", "{{constructor}}"]) {
+                    cx.struct_span_lint(MAUD_DOCTYPE_HTML, expr.span,
+                                        "use `maud::DOCTYPE_HTML` instead").emit();
+                }
+            }
+        }}
+    }
+}
diff --git a/maud_macros/src/lints/mod.rs b/maud_macros/src/lints/mod.rs
new file mode 100644
index 0000000..c2409d0
--- /dev/null
+++ b/maud_macros/src/lints/mod.rs
@@ -0,0 +1,14 @@
+use rustc_plugin::Registry;
+
+#[macro_use]
+mod util;
+
+pub mod doctype_html;
+
+pub fn register_lints(reg: &mut Registry) {
+    reg.register_late_lint_pass(Box::new(doctype_html::DoctypeHtml));
+
+    reg.register_lint_group("maud", vec![
+        doctype_html::MAUD_DOCTYPE_HTML,
+    ]);
+}
diff --git a/maud_macros/src/lints/util.rs b/maud_macros/src/lints/util.rs
new file mode 100644
index 0000000..b05e7e1
--- /dev/null
+++ b/maud_macros/src/lints/util.rs
@@ -0,0 +1,92 @@
+//! Miscellaneous utilities for writing lints.
+//!
+//! Most of these are adapted from Clippy.
+
+use rustc::hir::def_id::DefId;
+use rustc::lint::LateContext;
+use rustc::ty;
+use syntax::symbol::{InternedString, Symbol};
+
+/// Produce a nested chain of if-lets and ifs from the patterns:
+///
+/// ```rust,ignore
+/// if_let_chain! {[
+///     let Some(y) = x,
+///     y.len() == 2,
+///     let Some(z) = y,
+/// ], {
+///     block
+/// }}
+/// ```
+///
+/// becomes
+///
+/// ```rust,ignore
+/// if let Some(y) = x {
+///     if y.len() == 2 {
+///         if let Some(z) = y {
+///             block
+///         }
+///     }
+/// }
+/// ```
+#[macro_export]
+macro_rules! if_let_chain {
+    ([let $pat:pat = $expr:expr, $($tt:tt)+], $block:block) => {
+        if let $pat = $expr {
+           if_let_chain!{ [$($tt)+], $block }
+        }
+    };
+    ([let $pat:pat = $expr:expr], $block:block) => {
+        if let $pat = $expr {
+           $block
+        }
+    };
+    ([let $pat:pat = $expr:expr,], $block:block) => {
+        if let $pat = $expr {
+           $block
+        }
+    };
+    ([$expr:expr, $($tt:tt)+], $block:block) => {
+        if $expr {
+           if_let_chain!{ [$($tt)+], $block }
+        }
+    };
+    ([$expr:expr], $block:block) => {
+        if $expr {
+           $block
+        }
+    };
+    ([$expr:expr,], $block:block) => {
+        if $expr {
+           $block
+        }
+    };
+}
+
+/// Check if a `DefId`'s path matches the given absolute type path usage.
+///
+/// # Examples
+/// ```rust,ignore
+/// match_def_path(cx, id, &["core", "option", "Option"])
+/// ```
+pub fn match_def_path(cx: &LateContext, def_id: DefId, path: &[&str]) -> bool {
+    struct AbsolutePathBuffer {
+        names: Vec<InternedString>,
+    }
+
+    impl ty::item_path::ItemPathBuffer for AbsolutePathBuffer {
+        fn root_mode(&self) -> &ty::item_path::RootMode {
+            const ABSOLUTE: &'static ty::item_path::RootMode = &ty::item_path::RootMode::Absolute;
+            ABSOLUTE
+        }
+
+        fn push(&mut self, text: &str) {
+            self.names.push(Symbol::intern(text).as_str());
+        }
+    }
+
+    let mut apb = AbsolutePathBuffer { names: vec![] };
+    cx.tcx.push_item_path(&mut apb, def_id);
+    apb.names.len() == path.len() && apb.names.iter().zip(path.iter()).all(|(a, &b)| &**a == b)
+}