From f859ee0e3873239420d40f07b07be6310e91c138 Mon Sep 17 00:00:00 2001 From: Yoni Feigelson Date: Tue, 28 Feb 2023 17:38:43 +0200 Subject: [PATCH] feat(linter): declare_oxc_lint proc_macro --- Cargo.lock | 11 ++++ crates/oxc_linter/Cargo.toml | 1 + crates/oxc_linter/src/lib.rs | 2 +- crates/oxc_linter/src/rule.rs | 8 +++ crates/oxc_linter/src/rules/no_debugger.rs | 19 ++++++ crates/oxc_linter/src/rules/no_empty.rs | 19 ++++++ crates/oxc_linter/tests/integration_test.rs | 31 +++++++++ crates/oxc_macros/Cargo.toml | 20 ++++++ crates/oxc_macros/src/declare_oxc_lint.rs | 69 +++++++++++++++++++++ crates/oxc_macros/src/lib.rs | 52 ++++++++++++++++ 10 files changed, 231 insertions(+), 1 deletion(-) create mode 100644 crates/oxc_linter/tests/integration_test.rs create mode 100644 crates/oxc_macros/Cargo.toml create mode 100644 crates/oxc_macros/src/declare_oxc_lint.rs create mode 100644 crates/oxc_macros/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index f82e0d504..404291eb2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -882,11 +882,22 @@ dependencies = [ "oxc_allocator", "oxc_ast", "oxc_diagnostics", + "oxc_macros", "oxc_parser", "oxc_semantic", "serde_json", ] +[[package]] +name = "oxc_macros" +version = "0.0.0" +dependencies = [ + "itertools", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "oxc_parser" version = "0.0.0" diff --git a/crates/oxc_linter/Cargo.toml b/crates/oxc_linter/Cargo.toml index 494034c00..c1c68d176 100644 --- a/crates/oxc_linter/Cargo.toml +++ b/crates/oxc_linter/Cargo.toml @@ -12,6 +12,7 @@ version.workspace = true [dependencies] oxc_ast = { path = "../oxc_ast" } oxc_diagnostics = { path = "../oxc_diagnostics" } +oxc_macros = { path = "../oxc_macros" } oxc_semantic = { path = "../oxc_semantic" } lazy_static = { workspace = true } diff --git a/crates/oxc_linter/src/lib.rs b/crates/oxc_linter/src/lib.rs index 852d2e586..180bb11bc 100644 --- a/crates/oxc_linter/src/lib.rs +++ b/crates/oxc_linter/src/lib.rs @@ -4,7 +4,7 @@ mod tester; mod context; -mod rule; +pub mod rule; mod rules; use std::{fs, rc::Rc}; diff --git a/crates/oxc_linter/src/rule.rs b/crates/oxc_linter/src/rule.rs index 9cdd72823..bb23b1e39 100644 --- a/crates/oxc_linter/src/rule.rs +++ b/crates/oxc_linter/src/rule.rs @@ -6,9 +6,17 @@ pub trait Rule: Sized + Default + Debug { const NAME: &'static str; /// Initialize from eslint json configuration + #[must_use] fn from_configuration(_value: serde_json::Value) -> Self { Self::default() } fn run<'a>(&self, node: &AstNode<'a>, _ctx: &LintContext<'a>); } + +pub trait RuleMeta { + #[must_use] + fn documentation() -> Option<&'static str> { + None + } +} diff --git a/crates/oxc_linter/src/rules/no_debugger.rs b/crates/oxc_linter/src/rules/no_debugger.rs index aab4614f3..3f9a7fee6 100644 --- a/crates/oxc_linter/src/rules/no_debugger.rs +++ b/crates/oxc_linter/src/rules/no_debugger.rs @@ -3,6 +3,7 @@ use oxc_diagnostics::{ miette::{self, Diagnostic}, thiserror::Error, }; +use oxc_macros::declare_oxc_lint; use crate::{context::LintContext, rule::Rule, AstNode}; @@ -14,6 +15,24 @@ struct NoDebuggerDiagnostic(#[label] pub Span); #[derive(Debug, Default, Clone)] pub struct NoDebugger; +declare_oxc_lint!( + /// ### What it does + /// Checks for usage of the `debugger` statement + /// + /// ### Why is this bad? + /// `debugger` statements do not affect functionality when a debugger isn't attached. + /// They're most commonly an accidental debugging leftover. + /// + /// + /// ### Example + /// ```javascript + /// const data = await getData(); + /// const result = complexCalculation(data); + /// debugger; + /// ``` + NoDebugger +); + const RULE_NAME: &str = "no-debugger"; impl Rule for NoDebugger { diff --git a/crates/oxc_linter/src/rules/no_empty.rs b/crates/oxc_linter/src/rules/no_empty.rs index 86e9cbdd7..a1182d421 100644 --- a/crates/oxc_linter/src/rules/no_empty.rs +++ b/crates/oxc_linter/src/rules/no_empty.rs @@ -3,6 +3,7 @@ use oxc_diagnostics::{ miette::{self, Diagnostic}, thiserror::Error, }; +use oxc_macros::declare_oxc_lint; use crate::{context::LintContext, rule::Rule, AstNode}; @@ -16,6 +17,24 @@ pub struct NoEmpty { allow_empty_catch: bool, } +declare_oxc_lint!( + /// ### What it does + /// Disallows empty block statements + /// + /// ### Why is this bad? + /// Empty block statements, while not technically errors, usually occur due to refactoring that wasn’t completed. + /// They can cause confusion when reading code. + /// + /// + /// ### Example + /// ```javascript + /// if (condition) { + /// + /// } + /// ``` + NoEmpty +); + const RULE_NAME: &str = "no-empty"; impl Rule for NoEmpty { diff --git a/crates/oxc_linter/tests/integration_test.rs b/crates/oxc_linter/tests/integration_test.rs new file mode 100644 index 000000000..87816816a --- /dev/null +++ b/crates/oxc_linter/tests/integration_test.rs @@ -0,0 +1,31 @@ +use oxc_linter::rule::RuleMeta; +use oxc_macros::declare_oxc_lint_test; + +struct TestRule {} + +declare_oxc_lint_test!( + /// Dummy description + /// # which is multiline + TestRule, + "test" +); + +struct TestRule2 { + #[allow(dead_code)] + dummy_field: u8, +} + +declare_oxc_lint_test!( + /// Dummy description2 + TestRule2, + "test" +); + +#[test] +fn test_declare_oxc_lint() { + // Simple, multiline documentation + assert_eq!(TestRule::documentation().unwrap(), "Dummy description\n# which is multiline\n"); + + // Ensure structs with fields can be passed to the macro + assert_eq!(TestRule2::documentation().unwrap(), "Dummy description2\n"); +} diff --git a/crates/oxc_macros/Cargo.toml b/crates/oxc_macros/Cargo.toml new file mode 100644 index 000000000..bbd26eb55 --- /dev/null +++ b/crates/oxc_macros/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "oxc_macros" +authors.workspace = true +description.workspace = true +edition.workspace = true +homepage.workspace = true +keywords.workspace = true +license.workspace = true +repository.workspace = true +version.workspace = true + +[lib] +proc-macro = true +doctest = false + +[dependencies] +syn = "1.0.109" +quote = "1.0.23" +proc-macro2 = "1.0.51" +itertools = "0.10.5" \ No newline at end of file diff --git a/crates/oxc_macros/src/declare_oxc_lint.rs b/crates/oxc_macros/src/declare_oxc_lint.rs new file mode 100644 index 000000000..b49e35d8b --- /dev/null +++ b/crates/oxc_macros/src/declare_oxc_lint.rs @@ -0,0 +1,69 @@ +use proc_macro2::TokenStream; +use quote::quote; +use syn::parse::{Parse, ParseStream}; +use syn::{Attribute, Error, Ident, Lit, LitStr, Meta, Result}; + +fn parse_attr(path: [&'static str; LEN], attr: &Attribute) -> Option { + if let Meta::NameValue(name_value) = attr.parse_meta().ok()? { + let path_idents = name_value.path.segments.iter().map(|segment| &segment.ident); + + if itertools::equal(path_idents, path) { + if let Lit::Str(lit) = name_value.lit { + return Some(lit); + } + } + } + + None +} + +pub struct LintRuleMeta { + name: Ident, + documentation: String, + pub used_in_test: bool, +} + +impl Parse for LintRuleMeta { + fn parse(input: ParseStream<'_>) -> Result { + let attrs = input.call(Attribute::parse_outer)?; + + let mut documentation = String::new(); + for attr in &attrs { + if let Some(lit) = parse_attr(["doc"], attr) { + let value = lit.value(); + let line = value.strip_prefix(' ').unwrap_or(&value); + + documentation.push_str(line); + documentation.push('\n'); + } else { + return Err(Error::new_spanned(attr, "unexpected attribute")); + } + } + + let struct_name = input.parse()?; + + // Ignore the rest + input.parse::()?; + + Ok(Self { name: struct_name, documentation, used_in_test: false }) + } +} + +pub fn declare_oxc_lint(metadata: LintRuleMeta) -> TokenStream { + let LintRuleMeta { name, documentation, used_in_test } = metadata; + + let import_statement = + if used_in_test { None } else { Some(quote! { use crate::rule::RuleMeta; }) }; + + let output = quote! { + #import_statement + + impl RuleMeta for #name { + fn documentation() -> Option<&'static str> { + Some(#documentation) + } + } + }; + + output +} diff --git a/crates/oxc_macros/src/lib.rs b/crates/oxc_macros/src/lib.rs new file mode 100644 index 000000000..c4af47788 --- /dev/null +++ b/crates/oxc_macros/src/lib.rs @@ -0,0 +1,52 @@ +use syn::parse_macro_input; + +mod declare_oxc_lint; + +/// Macro used to declare an oxc lint rule +/// +/// Every lint declaration consists of 2 parts: +/// +/// 1. The documentation +/// 2. The lint's struct +/// +/// # Example +/// +/// ``` +/// use oxc_macros::declare_oxc_lint; +/// +/// declare_oxc_lint! { +/// /// ### What it does +/// /// Checks for usage of the `debugger` statement +/// /// +/// /// ### Why is this bad? +/// /// `debugger` statements do not affect functionality when a debugger isn't attached. +/// /// They're most commonly an accidental debugging leftover. +/// /// +/// /// +/// /// ### Example +/// /// ```javascript +/// /// const data = await getData(); +/// /// const result = complexCalculation(data); +/// /// debugger; +/// /// ``` +/// /// +/// /// ``` +/// pub struct NoDebugger +/// } +/// ``` +#[proc_macro] +pub fn declare_oxc_lint(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let metadata = parse_macro_input!(input as declare_oxc_lint::LintRuleMeta); + + declare_oxc_lint::declare_oxc_lint(metadata).into() +} + +/// Same as `declare_oxc_lint`, but doesn't do imports. +/// Enables multiple usages in a single file. +#[proc_macro] +pub fn declare_oxc_lint_test(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let mut metadata = parse_macro_input!(input as declare_oxc_lint::LintRuleMeta); + metadata.used_in_test = true; + + declare_oxc_lint::declare_oxc_lint(metadata).into() +}