mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 12:21:58 +00:00
feat(linter): declare_oxc_lint proc_macro
This commit is contained in:
parent
c2fb3613bd
commit
f859ee0e38
10 changed files with 231 additions and 1 deletions
11
Cargo.lock
generated
11
Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
mod tester;
|
||||
|
||||
mod context;
|
||||
mod rule;
|
||||
pub mod rule;
|
||||
mod rules;
|
||||
|
||||
use std::{fs, rc::Rc};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
31
crates/oxc_linter/tests/integration_test.rs
Normal file
31
crates/oxc_linter/tests/integration_test.rs
Normal file
|
|
@ -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");
|
||||
}
|
||||
20
crates/oxc_macros/Cargo.toml
Normal file
20
crates/oxc_macros/Cargo.toml
Normal file
|
|
@ -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"
|
||||
69
crates/oxc_macros/src/declare_oxc_lint.rs
Normal file
69
crates/oxc_macros/src/declare_oxc_lint.rs
Normal file
|
|
@ -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<const LEN: usize>(path: [&'static str; LEN], attr: &Attribute) -> Option<LitStr> {
|
||||
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<Self> {
|
||||
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::<TokenStream>()?;
|
||||
|
||||
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
|
||||
}
|
||||
52
crates/oxc_macros/src/lib.rs
Normal file
52
crates/oxc_macros/src/lib.rs
Normal file
|
|
@ -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()
|
||||
}
|
||||
Loading…
Reference in a new issue