From e8c955da4cbb042e6f9b89307d143f5bfa6779fa Mon Sep 17 00:00:00 2001 From: Akshay Date: Sun, 31 Oct 2021 14:35:26 +0530 Subject: add `explain` subcommand and explanations to all lints --- macros/src/explain.rs | 28 ++++++++ macros/src/lib.rs | 173 +++-------------------------------------------- macros/src/metadata.rs | 177 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 216 insertions(+), 162 deletions(-) create mode 100644 macros/src/explain.rs create mode 100644 macros/src/metadata.rs (limited to 'macros/src') diff --git a/macros/src/explain.rs b/macros/src/explain.rs new file mode 100644 index 0000000..41dc5d4 --- /dev/null +++ b/macros/src/explain.rs @@ -0,0 +1,28 @@ +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; +use syn::{ItemStruct, Lit, Meta, MetaNameValue}; + +pub fn generate_explain_impl(struct_item: &ItemStruct) -> TokenStream2 { + let struct_name = &struct_item.ident; + let explain = struct_item + .attrs + .iter() + .filter_map(|attr| match attr.parse_meta().ok() { + Some(Meta::NameValue(MetaNameValue { + path, + lit: Lit::Str(str_lit), + .. + })) if path.is_ident("doc") => Some(str_lit.value()), + _ => None, + }) + .map(|s| s.strip_prefix(' ').unwrap_or(&s).to_owned()) + .collect::>() + .join("\n"); + quote! { + impl crate::Explain for #struct_name { + fn explanation(&self) -> &'static str { + #explain + } + } + } +} diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 127b4cb..86fa509 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -1,44 +1,12 @@ -use std::collections::HashMap; +mod explain; +mod metadata; +use explain::generate_explain_impl; +use metadata::{generate_meta_impl, RawLintMeta}; use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; - -use quote::{format_ident, quote}; -use syn::{ - parse::{Parse, ParseStream, Result as ParseResult}, - parse_macro_input, - punctuated::Punctuated, - Expr, Ident, ItemStruct, Lit, Token, -}; - -struct KeyValue { - key: Ident, - _eq: Token![=], - value: Expr, -} - -impl Parse for KeyValue { - fn parse(input: ParseStream) -> ParseResult { - Ok(Self { - key: input.parse()?, - _eq: input.parse()?, - value: input.parse()?, - }) - } -} - -struct LintMeta(HashMap); - -impl Parse for LintMeta { - fn parse(input: ParseStream) -> ParseResult { - Ok(Self( - Punctuated::::parse_terminated(input)? - .into_iter() - .map(|item| (item.key, item.value)) - .collect(), - )) - } -} +use quote::quote; +use syn::{parse_macro_input, Ident, ItemStruct}; fn generate_self_impl(struct_name: &Ident) -> TokenStream2 { quote! { @@ -50,136 +18,16 @@ fn generate_self_impl(struct_name: &Ident) -> TokenStream2 { } } -fn generate_meta_impl(struct_name: &Ident, meta: &LintMeta) -> TokenStream2 { - let name_fn = generate_name_fn(meta); - let note_fn = generate_note_fn(meta); - let code_fn = generate_code_fn(meta); - let report_fn = generate_report_fn(); - let match_with_fn = generate_match_with_fn(meta); - let match_kind = generate_match_kind(meta); - quote! { - impl Metadata for #struct_name { - #name_fn - #note_fn - #code_fn - #report_fn - #match_with_fn - #match_kind - } - } -} - -fn generate_name_fn(meta: &LintMeta) -> TokenStream2 { - let name = meta - .0 - .get(&format_ident!("name")) - .unwrap_or_else(|| panic!("`name` not present")); - if let syn::Expr::Lit(name_lit) = name { - if let Lit::Str(name_str) = &name_lit.lit { - return quote! { - fn name() -> &'static str { - #name_str - } - }; - } - } - panic!("Invalid value for `name`"); -} - -fn generate_note_fn(meta: &LintMeta) -> TokenStream2 { - let note = meta - .0 - .get(&format_ident!("note")) - .unwrap_or_else(|| panic!("`note` not present")); - if let syn::Expr::Lit(note_lit) = note { - if let Lit::Str(note_str) = ¬e_lit.lit { - return quote! { - fn note() -> &'static str { - #note_str - } - }; - } - } - panic!("Invalid value for `note`"); -} - -fn generate_code_fn(meta: &LintMeta) -> TokenStream2 { - let code = meta - .0 - .get(&format_ident!("code")) - .unwrap_or_else(|| panic!("`code` not present")); - if let syn::Expr::Lit(code_lit) = code { - if let Lit::Int(code_int) = &code_lit.lit { - return quote! { - fn code() -> u32 { - #code_int - } - }; - } - } - panic!("Invalid value for `note`"); -} - -fn generate_report_fn() -> TokenStream2 { - quote! { - fn report() -> Report { - Report::new(Self::note(), Self::code()) - } - } -} - -fn generate_match_with_fn(meta: &LintMeta) -> TokenStream2 { - let match_with_lit = meta - .0 - .get(&format_ident!("match_with")) - .unwrap_or_else(|| panic!("`match_with` not present")); - if let syn::Expr::Path(match_path) = match_with_lit { - quote! { - fn match_with(&self, with: &SyntaxKind) -> bool { - #match_path == *with - } - } - } else if let syn::Expr::Array(array_expr) = match_with_lit { - quote! { - fn match_with(&self, with: &SyntaxKind) -> bool { - #array_expr.contains(with) - } - } - } else { - panic!("`match_with` has non-path value") - } -} - -fn generate_match_kind(meta: &LintMeta) -> TokenStream2 { - let match_with_lit = meta - .0 - .get(&format_ident!("match_with")) - .unwrap_or_else(|| panic!("`match_with` not present")); - if let syn::Expr::Path(match_path) = match_with_lit { - quote! { - fn match_kind(&self) -> Vec { - vec![#match_path] - } - } - } else if let syn::Expr::Array(array_expr) = match_with_lit { - quote! { - fn match_kind(&self) -> Vec { - #array_expr.to_vec() - } - } - } else { - panic!("`match_with` has non-path value") - } -} - #[proc_macro_attribute] pub fn lint(attr: TokenStream, item: TokenStream) -> TokenStream { let struct_item = parse_macro_input!(item as ItemStruct); - let meta = parse_macro_input!(attr as LintMeta); + let meta = parse_macro_input!(attr as RawLintMeta); let struct_name = &struct_item.ident; let self_impl = generate_self_impl(struct_name); let meta_impl = generate_meta_impl(struct_name, &meta); + let explain_impl = generate_explain_impl(&struct_item); + (quote! { #struct_item @@ -189,8 +37,9 @@ pub fn lint(attr: TokenStream, item: TokenStream) -> TokenStream { #self_impl #meta_impl + #explain_impl - impl Lint for #struct_name {} + impl crate::Lint for #struct_name {} }) .into() } diff --git a/macros/src/metadata.rs b/macros/src/metadata.rs new file mode 100644 index 0000000..b41eb9c --- /dev/null +++ b/macros/src/metadata.rs @@ -0,0 +1,177 @@ +use std::collections::HashMap; + +use proc_macro2::TokenStream as TokenStream2; +use quote::{format_ident, quote}; +use syn::{ + parse::{Parse, ParseStream, Result}, + punctuated::Punctuated, + Expr, ExprArray, Ident, Lit, Path, Token, +}; + +struct KeyValue { + key: Ident, + _eq: Token![=], + value: Expr, +} + +impl Parse for KeyValue { + fn parse(input: ParseStream) -> Result { + Ok(Self { + key: input.parse()?, + _eq: input.parse()?, + value: input.parse()?, + }) + } +} + +pub struct RawLintMeta(HashMap); + +impl Parse for RawLintMeta { + fn parse(input: ParseStream) -> Result { + Ok(Self( + Punctuated::::parse_terminated(input)? + .into_iter() + .map(|item| (item.key, item.value)) + .collect(), + )) + } +} + +pub struct LintMeta<'μ> { + name: &'μ Lit, + note: &'μ Lit, + code: &'μ Lit, + match_with: MatchWith<'μ>, +} + +enum MatchWith<'π> { + Path(&'π Path), + Array(&'π ExprArray), +} + +fn extract<'λ>(id: &str, raw: &'λ RawLintMeta) -> &'λ Expr { + raw.0 + .get(&format_ident!("{}", id)) + .unwrap_or_else(|| panic!("`{}` not present", id)) +} + +fn as_lit(e: &Expr) -> &Lit { + match e { + Expr::Lit(l) => &l.lit, + _ => panic!("expected a literal"), + } +} + +impl<'μ> LintMeta<'μ> { + fn from_raw(raw: &'μ RawLintMeta) -> Self { + let name = as_lit(extract("name", raw)); + let note = as_lit(extract("note", raw)); + let code = as_lit(extract("code", raw)); + let match_with_expr = extract("match_with", raw); + let match_with = match match_with_expr { + Expr::Path(p) => MatchWith::Path(&p.path), + Expr::Array(a) => MatchWith::Array(a), + _ => panic!("`match_with` is neither a path nor an array"), + }; + Self { + name, + note, + code, + match_with, + } + } + + fn generate_name_fn(&self) -> TokenStream2 { + let name_str = self.name; + return quote! { + fn name(&self) -> &'static str { + #name_str + } + }; + } + + fn generate_note_fn(&self) -> TokenStream2 { + let note_str = self.note; + return quote! { + fn note(&self) -> &'static str { + #note_str + } + }; + } + + fn generate_code_fn(&self) -> TokenStream2 { + let code_int = self.code; + return quote! { + fn code(&self) -> u32 { + #code_int + } + }; + } + + fn generate_match_with_fn(&self) -> TokenStream2 { + match self.match_with { + MatchWith::Path(p) => { + quote! { + fn match_with(&self, with: &SyntaxKind) -> bool { + #p == *with + } + } + } + MatchWith::Array(a) => { + quote! { + fn match_with(&self, with: &SyntaxKind) -> bool { + #a.contains(with) + } + } + } + } + } + + fn generate_match_kind_fn(&self) -> TokenStream2 { + match self.match_with { + MatchWith::Path(p) => { + quote! { + fn match_kind(&self) -> Vec { + vec![#p] + } + } + } + MatchWith::Array(a) => { + quote! { + fn match_kind(&self) -> Vec { + #a.to_vec() + } + } + } + } + } + + fn generate_report_fn(&self) -> TokenStream2 { + quote! { + fn report(&self) -> crate::Report { + crate::Report::new(self.note(), self.code()) + } + } + } +} + +pub fn generate_meta_impl(struct_name: &Ident, meta: &RawLintMeta) -> TokenStream2 { + let not_raw = LintMeta::from_raw(&meta); + let name_fn = not_raw.generate_name_fn(); + let note_fn = not_raw.generate_note_fn(); + let code_fn = not_raw.generate_code_fn(); + let match_with_fn = not_raw.generate_match_with_fn(); + let match_kind = not_raw.generate_match_kind_fn(); + let report_fn = not_raw.generate_report_fn(); + + quote! { + impl crate::Metadata for #struct_name { + #name_fn + #note_fn + #code_fn + #match_with_fn + #match_kind + #report_fn + } + } +} -- cgit v1.2.3