mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 12:21:58 +00:00
docs(ast): auto-generate doc comments for AstBuilder methods (#4471)
# What This PR Does Modifies `ast_codegen` to auto-generate rustdoc comments for generated `AstBuilder` methods. As we add more doc comments to AST node fields, the generated documentation will get better. 
This commit is contained in:
parent
7b5e1f5ac8
commit
c6a11bed1d
2 changed files with 5150 additions and 11 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -1,15 +1,15 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::stringify;
|
use std::stringify;
|
||||||
|
use std::{borrow::Cow, collections::HashMap};
|
||||||
|
|
||||||
use convert_case::{Case, Casing};
|
use convert_case::{Case, Casing};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use proc_macro2::TokenStream;
|
use proc_macro2::{TokenStream, TokenTree};
|
||||||
use quote::{format_ident, quote, ToTokens};
|
use quote::{format_ident, quote, ToTokens};
|
||||||
use syn::{
|
use syn::{
|
||||||
parse_quote, punctuated::Punctuated, AngleBracketedGenericArguments, FnArg, GenericArgument,
|
parse_quote, punctuated::Punctuated, AngleBracketedGenericArguments, Attribute, Expr, Field,
|
||||||
GenericParam, Ident, ImplItemFn, PatType, PathArguments, PredicateType, Token, Type, TypePath,
|
FnArg, GenericArgument, GenericParam, Ident, ImplItemFn, Lit, Meta, MetaNameValue, PatLit,
|
||||||
Variant, WhereClause,
|
PatType, PathArguments, PredicateType, Token, Type, TypePath, Variant, WhereClause,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
@ -147,6 +147,7 @@ fn generate_enum_inherit_builder_fn(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a builder function for an enum variant (e.g. for `Expression::Binary`)
|
||||||
fn generate_enum_variant_builder_fn(
|
fn generate_enum_variant_builder_fn(
|
||||||
enum_: &REnum,
|
enum_: &REnum,
|
||||||
variant: &Variant,
|
variant: &Variant,
|
||||||
|
|
@ -174,14 +175,27 @@ fn generate_enum_variant_builder_fn(
|
||||||
let inner_ident = var_type.get_ident();
|
let inner_ident = var_type.get_ident();
|
||||||
|
|
||||||
let mut inner = quote!(self.#inner_builder(#(#fields),*));
|
let mut inner = quote!(self.#inner_builder(#(#fields),*));
|
||||||
|
let mut does_alloc = false;
|
||||||
if matches!(inner_ident, TypeIdentResult::Box(_)) {
|
if matches!(inner_ident, TypeIdentResult::Box(_)) {
|
||||||
inner = quote!(self.alloc(#inner));
|
inner = quote!(self.alloc(#inner));
|
||||||
|
does_alloc = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
let from_variant_builder = generate_enum_from_variant_builder_fn(enum_, variant, ctx);
|
let from_variant_builder = generate_enum_from_variant_builder_fn(enum_, variant, ctx);
|
||||||
|
let article = article_for(enum_ident.to_string());
|
||||||
|
let mut docs = DocComment::new(format!(" Build {article} [`{enum_ident}::{var_ident}`]"))
|
||||||
|
.with_params(¶ms);
|
||||||
|
if does_alloc {
|
||||||
|
let inner_name = inner_ident.inner_ident().to_string();
|
||||||
|
let inner_article = article_for(&inner_name);
|
||||||
|
docs = docs.with_description(format!(
|
||||||
|
"This node contains {inner_article} [`{inner_name}`] that will be stored in the memory arena."
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
quote! {
|
quote! {
|
||||||
endl!();
|
endl!();
|
||||||
|
#docs
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn #fn_name #generic_params (self, #(#params),*) -> #enum_type #where_clause {
|
pub fn #fn_name #generic_params (self, #(#params),*) -> #enum_type #where_clause {
|
||||||
#enum_ident::#var_ident(#inner)
|
#enum_ident::#var_ident(#inner)
|
||||||
|
|
@ -191,6 +205,8 @@ fn generate_enum_variant_builder_fn(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generate a conversion function that takes some struct and creates an enum
|
||||||
|
/// variant containing that struct using the `IntoIn` trait.
|
||||||
fn generate_enum_from_variant_builder_fn(
|
fn generate_enum_from_variant_builder_fn(
|
||||||
enum_: &REnum,
|
enum_: &REnum,
|
||||||
variant: &Variant,
|
variant: &Variant,
|
||||||
|
|
@ -201,13 +217,19 @@ fn generate_enum_from_variant_builder_fn(
|
||||||
let enum_type = &enum_.as_type();
|
let enum_type = &enum_.as_type();
|
||||||
let var_ident = &variant.ident;
|
let var_ident = &variant.ident;
|
||||||
let var_type = &variant.fields.iter().next().expect("we have already asserted this one!").ty;
|
let var_type = &variant.fields.iter().next().expect("we have already asserted this one!").ty;
|
||||||
let fn_name = enum_builder_name(
|
let struct_ident = var_type.get_ident().inner_ident().to_string();
|
||||||
enum_ident.to_string(),
|
let fn_name = enum_builder_name(enum_ident.to_string(), format!("From{struct_ident}"));
|
||||||
format!("From{}", var_type.get_ident().inner_ident()),
|
|
||||||
);
|
|
||||||
|
|
||||||
|
let from_article = article_for(struct_ident);
|
||||||
|
let to_article = article_for(enum_ident.to_string());
|
||||||
|
|
||||||
|
let docs = DocComment::new(format!(
|
||||||
|
" Convert {from_article} [`{}`] into {to_article} [`{enum_ident}::{var_ident}`]",
|
||||||
|
var_type.get_ident().inner_ident()
|
||||||
|
));
|
||||||
quote! {
|
quote! {
|
||||||
endl!();
|
endl!();
|
||||||
|
#docs
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn #fn_name<T>(self, inner: T) -> #enum_type where T: IntoIn<'a, #var_type> {
|
pub fn #fn_name<T>(self, inner: T) -> #enum_type where T: IntoIn<'a, #var_type> {
|
||||||
#enum_ident::#var_ident(inner.into_in(self.allocator))
|
#enum_ident::#var_ident(inner.into_in(self.allocator))
|
||||||
|
|
@ -268,13 +290,27 @@ fn generate_struct_builder_fn(ty: &RStruct, ctx: &CodegenCtx) -> TokenStream {
|
||||||
|
|
||||||
let alloc_fn_name = format_ident!("alloc_{fn_name}");
|
let alloc_fn_name = format_ident!("alloc_{fn_name}");
|
||||||
|
|
||||||
|
let article = article_for(ident.to_string());
|
||||||
|
let fn_docs = DocComment::new(format!("Builds {article} [`{ident}`]"))
|
||||||
|
.with_description(format!("If you want the built node to be allocated in the memory arena, use [`AstBuilder::{alloc_fn_name}`] instead."))
|
||||||
|
.with_params(¶ms);
|
||||||
|
|
||||||
|
let alloc_docs =
|
||||||
|
DocComment::new(format!("Builds {article} [`{ident}`] and stores it in the memory arena."))
|
||||||
|
.with_description(format!("Returns a [`Box`] containing the newly-allocated node. If you want a stack-allocated node, use [`AstBuilder::{fn_name}`] instead."))
|
||||||
|
.with_params(¶ms);
|
||||||
|
|
||||||
quote! {
|
quote! {
|
||||||
endl!();
|
endl!();
|
||||||
|
#fn_docs
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn #fn_name #generic_params (self, #(#params),*) -> #as_type #where_clause {
|
pub fn #fn_name #generic_params (self, #(#params),*) -> #as_type #where_clause {
|
||||||
#ident { #(#fields),* }
|
#ident { #(#fields),* }
|
||||||
}
|
}
|
||||||
|
|
||||||
endl!();
|
endl!();
|
||||||
|
|
||||||
|
#alloc_docs
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn #alloc_fn_name #generic_params (self, #(#params),*) -> Box<'a, #as_type> #where_clause {
|
pub fn #alloc_fn_name #generic_params (self, #(#params),*) -> Box<'a, #as_type> #where_clause {
|
||||||
Box::new_in(self.#fn_name(#(#args),*), self.allocator)
|
Box::new_in(self.#fn_name(#(#args),*), self.allocator)
|
||||||
|
|
@ -282,6 +318,7 @@ fn generate_struct_builder_fn(ty: &RStruct, ctx: &CodegenCtx) -> TokenStream {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
struct Param {
|
struct Param {
|
||||||
is_default: bool,
|
is_default: bool,
|
||||||
info: TypeAnalyzeResult,
|
info: TypeAnalyzeResult,
|
||||||
|
|
@ -289,6 +326,7 @@ struct Param {
|
||||||
ty: Type,
|
ty: Type,
|
||||||
generic: Option<(/* predicate */ TokenStream, /* param name */ TokenStream)>,
|
generic: Option<(/* predicate */ TokenStream, /* param name */ TokenStream)>,
|
||||||
into_in: bool,
|
into_in: bool,
|
||||||
|
docs: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Param {
|
impl Param {
|
||||||
|
|
@ -309,6 +347,182 @@ impl ToTokens for Param {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents a rusdoc comment that will be added to a generated function,
|
||||||
|
/// struct, etc.
|
||||||
|
///
|
||||||
|
/// [`DocComment`] implements [`ToTokens`], so you can use it in a [`quote!`]
|
||||||
|
/// block as normal.
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// let docs = DocComment::new("This is a summary")
|
||||||
|
/// .with_description("This is a longer description");
|
||||||
|
///
|
||||||
|
/// let my_function = quote! {
|
||||||
|
/// #doc
|
||||||
|
/// fn my_function() {
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// This generates comments in the following format:
|
||||||
|
///
|
||||||
|
/// ```md
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// <description>
|
||||||
|
///
|
||||||
|
/// ## Parameters
|
||||||
|
/// - param1: some docs
|
||||||
|
/// - param2
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// 1. [`summary`] is a single-line overview about the thing being documented.
|
||||||
|
/// 2. [`description`] is a longer-form description that can span multiple
|
||||||
|
/// lines. It will be split into paragraphs for you.
|
||||||
|
/// 3. [`parameters`] is a bulleted list of function parameters. Documentation
|
||||||
|
/// for them can be extracted from struct fields and enums. This really only applies to functions.
|
||||||
|
///
|
||||||
|
/// Each section only appears if there is content for it. Only [`summary`] is required.
|
||||||
|
///
|
||||||
|
/// [`summary`]: DocComment::summary
|
||||||
|
/// [`description`]: DocComment::description
|
||||||
|
/// [`parameters`]: DocComment::params
|
||||||
|
///
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct DocComment<'p> {
|
||||||
|
/// Single-line summary. Put at the top of the comment.
|
||||||
|
summary: Cow<'static, str>,
|
||||||
|
/// Zero or more description paragraphs.
|
||||||
|
description: Vec<Cow<'static, str>>,
|
||||||
|
/// Function parameters, if applicable. Will be used to create a parameter
|
||||||
|
/// section that looks like this:
|
||||||
|
///
|
||||||
|
/// ```md
|
||||||
|
/// ## Parameters
|
||||||
|
/// - first_param: some docs taken from the [`Param`]
|
||||||
|
/// - second_param
|
||||||
|
/// ```
|
||||||
|
params: &'p [Param],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'p> DocComment<'p> {
|
||||||
|
pub fn new<S>(summary: S) -> Self
|
||||||
|
where
|
||||||
|
S: Into<Cow<'static, str>>,
|
||||||
|
{
|
||||||
|
Self { summary: Self::maybe_add_space(summary.into()), description: vec![], params: &[] }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a longer-form description to the doc comment.
|
||||||
|
pub fn with_description<S>(mut self, description: S) -> Self
|
||||||
|
where
|
||||||
|
S: Into<Cow<'static, str>>,
|
||||||
|
{
|
||||||
|
self.description = vec![Self::maybe_add_space(description.into())];
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a description section made up of multiple lines.
|
||||||
|
///
|
||||||
|
/// Each line will be turned into its own paragraph.
|
||||||
|
pub fn with_description_lines<L, S>(mut self, description: L) -> Self
|
||||||
|
where
|
||||||
|
S: Into<Cow<'static, str>>,
|
||||||
|
L: IntoIterator<Item = S>,
|
||||||
|
{
|
||||||
|
self.description =
|
||||||
|
description.into_iter().map(Into::into).map(Self::maybe_add_space).collect();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a section documenting function parameters.
|
||||||
|
pub fn with_params(mut self, params: &'p Vec<Param>) -> Self {
|
||||||
|
self.params = params.as_slice();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a leading space to a doc comment line if it doesn't already have one.
|
||||||
|
/// This makes it easier to read, since the comment won't be directly next
|
||||||
|
/// to the `///`.
|
||||||
|
fn maybe_add_space(s: Cow<'static, str>) -> Cow<'static, str> {
|
||||||
|
if s.is_empty() || s.starts_with(' ') {
|
||||||
|
s
|
||||||
|
} else {
|
||||||
|
Cow::Owned(format!(" {s}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the correct article (a/an) that should precede a `word`.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// if `word` is empty
|
||||||
|
fn article_for<S: AsRef<str>>(word: S) -> &'static str {
|
||||||
|
match word.as_ref().chars().next().unwrap() {
|
||||||
|
'a' | 'e' | 'i' | 'o' | 'u' => "an",
|
||||||
|
_ => "a",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToTokens for DocComment<'_> {
|
||||||
|
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||||
|
macro_rules! newline {
|
||||||
|
() => {
|
||||||
|
tokens.extend(quote!( #[doc = ""]));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let summary = &self.summary;
|
||||||
|
tokens.extend(quote!( #[doc = #summary]));
|
||||||
|
|
||||||
|
// print description
|
||||||
|
for line in &self.description {
|
||||||
|
// extra newline needed to create a new paragraph
|
||||||
|
newline!();
|
||||||
|
tokens.extend(quote!( #[doc = #line]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// print docs for function parameters
|
||||||
|
if !self.params.is_empty() {
|
||||||
|
newline!();
|
||||||
|
tokens.extend(quote!( #[doc = " ## Parameters"]));
|
||||||
|
for param in self.params {
|
||||||
|
match ¶m.docs {
|
||||||
|
Some(docs) => {
|
||||||
|
let docs = format!(" - {}: {}", param.ident, docs.trim());
|
||||||
|
tokens.extend(quote!(
|
||||||
|
#[doc = #docs]
|
||||||
|
));
|
||||||
|
}
|
||||||
|
None if param.ident == "span" => {
|
||||||
|
tokens.extend(quote!(
|
||||||
|
#[doc = " - span: The [`Span`] covering this node"]
|
||||||
|
));
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let docs = format!(" - {}", param.ident);
|
||||||
|
tokens.extend(quote!(#[doc = #docs]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn get_doc_comment(attrs: &[Attribute]) -> Option<String> {
|
||||||
|
attrs.iter().find_map(|attr| match &attr.meta {
|
||||||
|
Meta::NameValue(MetaNameValue { path, value: Expr::Lit(lit), .. }) => {
|
||||||
|
if !path.is_ident("doc") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
match &lit.lit {
|
||||||
|
Lit::Str(lit) => Some(lit.value()),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
}
|
||||||
fn get_enum_params(enum_: &REnum, ctx: &CodegenCtx) -> Vec<Param> {
|
fn get_enum_params(enum_: &REnum, ctx: &CodegenCtx) -> Vec<Param> {
|
||||||
let as_type = enum_.as_type();
|
let as_type = enum_.as_type();
|
||||||
let inner_type = match &as_type {
|
let inner_type = match &as_type {
|
||||||
|
|
@ -345,6 +559,7 @@ fn get_enum_params(enum_: &REnum, ctx: &CodegenCtx) -> Vec<Param> {
|
||||||
ty: inner_type.clone(),
|
ty: inner_type.clone(),
|
||||||
generic: None,
|
generic: None,
|
||||||
into_in: false,
|
into_in: false,
|
||||||
|
docs: None,
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
RType::Struct(it) => get_struct_params(it, ctx),
|
RType::Struct(it) => get_struct_params(it, ctx),
|
||||||
|
|
@ -386,8 +601,13 @@ fn get_struct_params(struct_: &RStruct, ctx: &CodegenCtx) -> Vec<Param> {
|
||||||
.item
|
.item
|
||||||
.fields
|
.fields
|
||||||
.iter()
|
.iter()
|
||||||
.map(|f| (f.ident.as_ref().expect("expected named ident!"), &f.ty))
|
.map(|f| {
|
||||||
.fold(Vec::new(), |mut acc, ref it @ (id, ty)| {
|
let id = f.ident.as_ref().expect("expected named ident! on struct");
|
||||||
|
let docs = get_doc_comment(&f.attrs);
|
||||||
|
((id, &f.ty), docs)
|
||||||
|
})
|
||||||
|
.fold(Vec::new(), |mut acc, (ref it, docs)| {
|
||||||
|
let (id, ty) = *it;
|
||||||
let info = ty.analyze(ctx);
|
let info = ty.analyze(ctx);
|
||||||
let (interface_typ, generic_typ) = match (&info.wrapper, &info.type_ref) {
|
let (interface_typ, generic_typ) = match (&info.wrapper, &info.type_ref) {
|
||||||
(TypeWrapper::Box, Some(ref type_ref)) => {
|
(TypeWrapper::Box, Some(ref type_ref)) => {
|
||||||
|
|
@ -421,6 +641,7 @@ fn get_struct_params(struct_: &RStruct, ctx: &CodegenCtx) -> Vec<Param> {
|
||||||
ty,
|
ty,
|
||||||
into_in: generic_typ.is_some(),
|
into_in: generic_typ.is_some(),
|
||||||
generic: generic_typ,
|
generic: generic_typ,
|
||||||
|
docs,
|
||||||
});
|
});
|
||||||
acc
|
acc
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue