fix(macros): support #[doc = include_str!(...)] for macros (#444)
Add tests and return syn::Result from extract_doc_line to propagate parsing errors.
This commit is contained in:
parent
4c8f5ba7b3
commit
a11c4de332
3 changed files with 88 additions and 29 deletions
|
|
@ -9,33 +9,45 @@ pub fn none_expr() -> syn::Result<Expr> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract documentation from doc attributes
|
/// Extract documentation from doc attributes
|
||||||
pub fn extract_doc_line(existing_docs: Option<String>, attr: &Attribute) -> Option<String> {
|
pub fn extract_doc_line(
|
||||||
|
existing_docs: Option<Expr>,
|
||||||
|
attr: &Attribute,
|
||||||
|
) -> syn::Result<Option<Expr>> {
|
||||||
if !attr.path().is_ident("doc") {
|
if !attr.path().is_ident("doc") {
|
||||||
return None;
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
let syn::Meta::NameValue(name_value) = &attr.meta else {
|
let syn::Meta::NameValue(name_value) = &attr.meta else {
|
||||||
return None;
|
return Ok(None);
|
||||||
};
|
};
|
||||||
|
|
||||||
let syn::Expr::Lit(expr_lit) = &name_value.value else {
|
let value = &name_value.value;
|
||||||
return None;
|
let this_expr: Option<Expr> = match value {
|
||||||
};
|
// Preserve macros such as `include_str!(...)`
|
||||||
|
syn::Expr::Macro(_) => Some(value.clone()),
|
||||||
let syn::Lit::Str(lit_str) = &expr_lit.lit else {
|
syn::Expr::Lit(syn::ExprLit {
|
||||||
return None;
|
lit: syn::Lit::Str(lit_str),
|
||||||
};
|
..
|
||||||
|
}) => {
|
||||||
let content = lit_str.value().trim().to_string();
|
let content = lit_str.value().trim().to_string();
|
||||||
match (existing_docs, content) {
|
if content.is_empty() {
|
||||||
(Some(mut existing_docs), content) if !content.is_empty() => {
|
return Ok(existing_docs);
|
||||||
existing_docs.push('\n');
|
}
|
||||||
existing_docs.push_str(&content);
|
Some(Expr::Lit(syn::ExprLit {
|
||||||
Some(existing_docs)
|
attrs: Vec::new(),
|
||||||
|
lit: syn::Lit::Str(syn::LitStr::new(&content, lit_str.span())),
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
(Some(existing_docs), _) => Some(existing_docs),
|
_ => return Ok(None),
|
||||||
(None, content) if !content.is_empty() => Some(content),
|
};
|
||||||
_ => None,
|
|
||||||
|
match (existing_docs, this_expr) {
|
||||||
|
(Some(existing), Some(this)) => {
|
||||||
|
syn::parse2::<Expr>(quote! { concat!(#existing, "\n", #this) }).map(Some)
|
||||||
|
}
|
||||||
|
(Some(existing), None) => Ok(Some(existing)),
|
||||||
|
(None, Some(this)) => Ok(Some(this)),
|
||||||
|
_ => Ok(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use darling::{FromMeta, ast::NestedMeta};
|
use darling::{FromMeta, ast::NestedMeta};
|
||||||
use proc_macro2::TokenStream;
|
use proc_macro2::{Span, TokenStream};
|
||||||
use quote::{format_ident, quote};
|
use quote::{format_ident, quote};
|
||||||
use syn::{Expr, Ident, ImplItemFn, ReturnType};
|
use syn::{Expr, Ident, ImplItemFn, ReturnType};
|
||||||
|
|
||||||
|
|
@ -23,7 +23,7 @@ pub struct PromptAttribute {
|
||||||
pub struct ResolvedPromptAttribute {
|
pub struct ResolvedPromptAttribute {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub title: Option<String>,
|
pub title: Option<String>,
|
||||||
pub description: Option<String>,
|
pub description: Option<Expr>,
|
||||||
pub arguments: Expr,
|
pub arguments: Expr,
|
||||||
pub icons: Option<Expr>,
|
pub icons: Option<Expr>,
|
||||||
}
|
}
|
||||||
|
|
@ -98,9 +98,14 @@ pub fn prompt(attr: TokenStream, input: TokenStream) -> syn::Result<TokenStream>
|
||||||
};
|
};
|
||||||
|
|
||||||
let name = attribute.name.unwrap_or_else(|| fn_ident.to_string());
|
let name = attribute.name.unwrap_or_else(|| fn_ident.to_string());
|
||||||
let description = attribute
|
let description = if let Some(s) = attribute.description {
|
||||||
.description
|
Some(Expr::Lit(syn::ExprLit {
|
||||||
.or_else(|| fn_item.attrs.iter().fold(None, extract_doc_line));
|
attrs: Vec::new(),
|
||||||
|
lit: syn::Lit::Str(syn::LitStr::new(&s, Span::call_site())),
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
fn_item.attrs.iter().try_fold(None, extract_doc_line)?
|
||||||
|
};
|
||||||
let arguments = arguments_expr;
|
let arguments = arguments_expr;
|
||||||
|
|
||||||
let resolved_prompt_attr = ResolvedPromptAttribute {
|
let resolved_prompt_attr = ResolvedPromptAttribute {
|
||||||
|
|
@ -200,4 +205,22 @@ mod test {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_doc_include_description() -> syn::Result<()> {
|
||||||
|
let attr = quote! {}; // No explicit description
|
||||||
|
let input = quote! {
|
||||||
|
#[doc = include_str!("some/test/data/doc.txt")]
|
||||||
|
fn test_prompt_included(&self) -> Result<String> {
|
||||||
|
Ok("Test".to_string())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let result = prompt(attr, input)?;
|
||||||
|
|
||||||
|
// The generated tokens should preserve the include_str! invocation
|
||||||
|
let result_str = result.to_string();
|
||||||
|
assert!(result_str.contains("include_str"));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use darling::{FromMeta, ast::NestedMeta};
|
use darling::{FromMeta, ast::NestedMeta};
|
||||||
use proc_macro2::TokenStream;
|
use proc_macro2::{Span, TokenStream};
|
||||||
use quote::{ToTokens, format_ident, quote};
|
use quote::{ToTokens, format_ident, quote};
|
||||||
use syn::{Expr, Ident, ImplItemFn, ReturnType, parse_quote};
|
use syn::{Expr, Ident, ImplItemFn, LitStr, ReturnType, parse_quote};
|
||||||
|
|
||||||
use crate::common::{extract_doc_line, none_expr};
|
use crate::common::{extract_doc_line, none_expr};
|
||||||
|
|
||||||
|
|
@ -82,7 +82,7 @@ pub struct ToolAttribute {
|
||||||
pub struct ResolvedToolAttribute {
|
pub struct ResolvedToolAttribute {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub title: Option<String>,
|
pub title: Option<String>,
|
||||||
pub description: Option<String>,
|
pub description: Option<Expr>,
|
||||||
pub input_schema: Expr,
|
pub input_schema: Expr,
|
||||||
pub output_schema: Option<Expr>,
|
pub output_schema: Option<Expr>,
|
||||||
pub annotations: Expr,
|
pub annotations: Expr,
|
||||||
|
|
@ -244,11 +244,17 @@ pub fn tool(attr: TokenStream, input: TokenStream) -> syn::Result<TokenStream> {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let description_expr = if let Some(s) = attribute.description {
|
||||||
|
Some(Expr::Lit(syn::ExprLit {
|
||||||
|
attrs: Vec::new(),
|
||||||
|
lit: syn::Lit::Str(LitStr::new(&s, Span::call_site())),
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
fn_item.attrs.iter().try_fold(None, extract_doc_line)?
|
||||||
|
};
|
||||||
let resolved_tool_attr = ResolvedToolAttribute {
|
let resolved_tool_attr = ResolvedToolAttribute {
|
||||||
name: attribute.name.unwrap_or_else(|| fn_ident.to_string()),
|
name: attribute.name.unwrap_or_else(|| fn_ident.to_string()),
|
||||||
description: attribute
|
description: description_expr,
|
||||||
.description
|
|
||||||
.or_else(|| fn_item.attrs.iter().fold(None, extract_doc_line)),
|
|
||||||
input_schema: input_schema_expr,
|
input_schema: input_schema_expr,
|
||||||
output_schema: output_schema_expr,
|
output_schema: output_schema_expr,
|
||||||
annotations: annotations_expr,
|
annotations: annotations_expr,
|
||||||
|
|
@ -352,4 +358,22 @@ mod test {
|
||||||
assert!(result_str.contains("Explicit description has priority"));
|
assert!(result_str.contains("Explicit description has priority"));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_doc_include_description() -> syn::Result<()> {
|
||||||
|
let attr = quote! {}; // No explicit description
|
||||||
|
let input = quote! {
|
||||||
|
#[doc = include_str!("some/test/data/doc.txt")]
|
||||||
|
fn test_function(&self) -> Result<(), Error> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let result = tool(attr, input)?;
|
||||||
|
|
||||||
|
// The macro should preserve include_str! in the generated tokens so we at least
|
||||||
|
// see the include_str invocation in the generated function source.
|
||||||
|
let result_str = result.to_string();
|
||||||
|
assert!(result_str.contains("include_str"));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue