diff --git a/crates/oxc_macros/src/declare_oxc_lint.rs b/crates/oxc_macros/src/declare_oxc_lint.rs index cd2c8d47cd06c..aac5e8c38c5b0 100644 --- a/crates/oxc_macros/src/declare_oxc_lint.rs +++ b/crates/oxc_macros/src/declare_oxc_lint.rs @@ -7,6 +7,15 @@ use syn::{ parse::{Parse, ParseStream}, }; +/// Documentation source for a lint rule +#[cfg(feature = "ruledocs")] +pub enum DocumentationSource { + /// Inline documentation from doc comments + Inline(String), + /// Reference to a shared documentation constant + Path(syn::Path), +} + pub struct LintRuleMeta { name: Ident, // Whether this rule should be exposed to tsgolint integration @@ -16,7 +25,7 @@ pub struct LintRuleMeta { /// Describes what auto-fixing capabilities the rule has fix: Option, #[cfg(feature = "ruledocs")] - documentation: String, + documentation: DocumentationSource, pub used_in_test: bool, /// Rule configuration /// This is the name of a struct/enum/whatever implementing @@ -27,7 +36,9 @@ pub struct LintRuleMeta { impl Parse for LintRuleMeta { fn parse(input: ParseStream<'_>) -> Result { #[cfg(feature = "ruledocs")] - let mut documentation = String::new(); + let mut documentation = None; + #[cfg(feature = "ruledocs")] + let mut doc_comments = String::new(); #[cfg(feature = "ruledocs")] let mut backtick_fences_count: usize = 0; @@ -39,9 +50,8 @@ impl Parse for LintRuleMeta { { let value = lit.value(); let line = value.strip_prefix(' ').unwrap_or(&value); - - documentation.push_str(line); - documentation.push('\n'); + doc_comments.push_str(line); + doc_comments.push('\n'); // Count occurrences of "```" to ensure the markdown code blocks are closed properly. backtick_fences_count += line.matches("```").count(); @@ -57,6 +67,13 @@ impl Parse for LintRuleMeta { } } + #[cfg(feature = "ruledocs")] + { + if !doc_comments.is_empty() { + documentation.replace(DocumentationSource::Inline(doc_comments)); + } + } + let struct_name: Ident = input.parse()?; // Optional marker `(tsgolint)` directly after the rule struct name let mut is_tsgolint_rule = false; @@ -111,13 +128,27 @@ impl Parse for LintRuleMeta { input.parse::()?; config.replace(input.parse()?); } + // docs = path::to::SHARED_DOCUMENTATION_CONSTANT + "docs" => { + #[cfg(feature = "ruledocs")] + { + if documentation.is_some() { + return Err(Error::new_spanned( + key, + "documentation source already specified inlined via doc comments", + )); + } + input.parse::()?; + + documentation.replace(DocumentationSource::Path(input.parse()?)); + } + } _ => { if input.peek(Token!(=)) || fix.is_some() { - panic!("invalid key: {key}"); - } else { - // fix kind shorthand, e.g. `dangerous-suggestion`` - fix.replace(key); + return Err(Error::new_spanned(key, "unexpected key in rule declaration")); } + // fix kind shorthand, e.g. `dangerous-suggestion`` + fix.replace(key); } } } @@ -139,6 +170,13 @@ impl Parse for LintRuleMeta { "unclosed markdown code block in documentation, please close all ``` fences", )); } + #[cfg(feature = "ruledocs")] + let Some(documentation) = documentation else { + return Err(Error::new( + struct_name.span(), + "rule documentation must be specified either via doc comments or the `docs` attribute", + )); + }; Ok(Self { name: struct_name, @@ -204,10 +242,17 @@ pub fn declare_oxc_lint(metadata: LintRuleMeta) -> TokenStream { let docs: Option = None; #[cfg(feature = "ruledocs")] - let docs = Some(quote! { - fn documentation() -> Option<&'static str> { - Some(#documentation) - } + let docs = Some(match documentation { + DocumentationSource::Inline(documentation) => quote! { + fn documentation() -> Option<&'static str> { + Some(#documentation) + } + }, + DocumentationSource::Path(shared_docs_path) => quote! { + fn documentation() -> Option<&'static str> { + Some(#shared_docs_path) + } + }, }); let has_config = if config.is_some() {