Allow lifetime refs in KCL stdlib parameters (#2802)
Previously these functions could not be used with `#[stdlib]` proc-macro: ```rust fn someFn(data: &'a str) {} fn someFn<'a>(data: Foo<'a>) {} ``` But now they can.
This commit is contained in:
@ -96,10 +96,16 @@ fn do_stdlib_inner(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !ast.sig.generics.params.is_empty() {
|
if !ast.sig.generics.params.is_empty() {
|
||||||
errors.push(Error::new_spanned(
|
if ast.sig.generics.params.iter().any(|generic_type| match generic_type {
|
||||||
&ast.sig.generics,
|
syn::GenericParam::Lifetime(_) => false,
|
||||||
"generics are not permitted for stdlib functions",
|
syn::GenericParam::Type(_) => true,
|
||||||
));
|
syn::GenericParam::Const(_) => true,
|
||||||
|
}) {
|
||||||
|
errors.push(Error::new_spanned(
|
||||||
|
&ast.sig.generics,
|
||||||
|
"Stdlib functions may not be generic over types or constants, only lifetimes.",
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ast.sig.variadic.is_some() {
|
if ast.sig.variadic.is_some() {
|
||||||
@ -650,7 +656,12 @@ impl Parse for ItemFnForSignature {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn clean_ty_string(t: &str) -> (String, proc_macro2::TokenStream) {
|
fn clean_ty_string(t: &str) -> (String, proc_macro2::TokenStream) {
|
||||||
let mut ty_string = t.replace('&', "").replace("mut", "").replace(' ', "");
|
let mut ty_string = t
|
||||||
|
.replace("& 'a", "")
|
||||||
|
.replace('&', "")
|
||||||
|
.replace("mut", "")
|
||||||
|
.replace("< 'a >", "")
|
||||||
|
.replace(' ', "");
|
||||||
if ty_string.starts_with("Args") {
|
if ty_string.starts_with("Args") {
|
||||||
ty_string = "Args".to_string();
|
ty_string = "Args".to_string();
|
||||||
}
|
}
|
||||||
|
@ -35,6 +35,56 @@ fn test_get_inner_array_type() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_args_with_refs() {
|
||||||
|
let (item, mut errors) = do_stdlib(
|
||||||
|
quote! {
|
||||||
|
name = "someFn",
|
||||||
|
},
|
||||||
|
quote! {
|
||||||
|
/// Docs
|
||||||
|
/// ```
|
||||||
|
/// someFn()
|
||||||
|
/// ```
|
||||||
|
fn someFn(
|
||||||
|
data: &'a str,
|
||||||
|
) -> i32 {
|
||||||
|
3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
if let Some(e) = errors.pop() {
|
||||||
|
panic!("{e}");
|
||||||
|
}
|
||||||
|
expectorate::assert_contents("tests/args_with_refs.gen", &get_text_fmt(&item).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_args_with_lifetime() {
|
||||||
|
let (item, mut errors) = do_stdlib(
|
||||||
|
quote! {
|
||||||
|
name = "someFn",
|
||||||
|
},
|
||||||
|
quote! {
|
||||||
|
/// Docs
|
||||||
|
/// ```
|
||||||
|
/// someFn()
|
||||||
|
/// ```
|
||||||
|
fn someFn<'a>(
|
||||||
|
data: Foo<'a>,
|
||||||
|
) -> i32 {
|
||||||
|
3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
if let Some(e) = errors.pop() {
|
||||||
|
panic!("{e}");
|
||||||
|
}
|
||||||
|
expectorate::assert_contents("tests/args_with_lifetime.gen", &get_text_fmt(&item).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_stdlib_line_to() {
|
fn test_stdlib_line_to() {
|
||||||
let (item, errors) = do_stdlib(
|
let (item, errors) = do_stdlib(
|
||||||
@ -64,7 +114,6 @@ fn test_stdlib_line_to() {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let _expected = quote! {};
|
|
||||||
|
|
||||||
assert!(errors.is_empty());
|
assert!(errors.is_empty());
|
||||||
expectorate::assert_contents("tests/lineTo.gen", &get_text_fmt(&item).unwrap());
|
expectorate::assert_contents("tests/lineTo.gen", &get_text_fmt(&item).unwrap());
|
||||||
|
194
src/wasm-lib/derive-docs/tests/args_with_lifetime.gen
Normal file
194
src/wasm-lib/derive-docs/tests/args_with_lifetime.gen
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod test_examples_someFn {
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn test_mock_example_someFn0() {
|
||||||
|
let tokens = crate::token::lexer("someFn()").unwrap();
|
||||||
|
let parser = crate::parser::Parser::new(tokens);
|
||||||
|
let program = parser.ast().unwrap();
|
||||||
|
let ctx = crate::executor::ExecutorContext {
|
||||||
|
engine: std::sync::Arc::new(Box::new(
|
||||||
|
crate::engine::conn_mock::EngineConnection::new()
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
)),
|
||||||
|
fs: std::sync::Arc::new(crate::fs::FileManager::new()),
|
||||||
|
stdlib: std::sync::Arc::new(crate::std::StdLib::new()),
|
||||||
|
settings: Default::default(),
|
||||||
|
is_mock: true,
|
||||||
|
};
|
||||||
|
ctx.run(program, None).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||||
|
async fn serial_test_example_someFn0() {
|
||||||
|
let user_agent = concat!(env!("CARGO_PKG_NAME"), ".rs/", env!("CARGO_PKG_VERSION"),);
|
||||||
|
let http_client = reqwest::Client::builder()
|
||||||
|
.user_agent(user_agent)
|
||||||
|
.timeout(std::time::Duration::from_secs(600))
|
||||||
|
.connect_timeout(std::time::Duration::from_secs(60));
|
||||||
|
let ws_client = reqwest::Client::builder()
|
||||||
|
.user_agent(user_agent)
|
||||||
|
.timeout(std::time::Duration::from_secs(600))
|
||||||
|
.connect_timeout(std::time::Duration::from_secs(60))
|
||||||
|
.connection_verbose(true)
|
||||||
|
.tcp_keepalive(std::time::Duration::from_secs(600))
|
||||||
|
.http1_only();
|
||||||
|
let token = std::env::var("KITTYCAD_API_TOKEN").expect("KITTYCAD_API_TOKEN not set");
|
||||||
|
let mut client = kittycad::Client::new_from_reqwest(token, http_client, ws_client);
|
||||||
|
if let Ok(addr) = std::env::var("LOCAL_ENGINE_ADDR") {
|
||||||
|
client.set_base_url(addr);
|
||||||
|
}
|
||||||
|
|
||||||
|
let tokens = crate::token::lexer("someFn()").unwrap();
|
||||||
|
let parser = crate::parser::Parser::new(tokens);
|
||||||
|
let program = parser.ast().unwrap();
|
||||||
|
let ctx = crate::executor::ExecutorContext::new(&client, Default::default())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
ctx.run(program, None).await.unwrap();
|
||||||
|
ctx.engine
|
||||||
|
.send_modeling_cmd(
|
||||||
|
uuid::Uuid::new_v4(),
|
||||||
|
crate::executor::SourceRange::default(),
|
||||||
|
kittycad::types::ModelingCmd::ZoomToFit {
|
||||||
|
object_ids: Default::default(),
|
||||||
|
padding: 0.1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let resp = ctx
|
||||||
|
.engine
|
||||||
|
.send_modeling_cmd(
|
||||||
|
uuid::Uuid::new_v4(),
|
||||||
|
crate::executor::SourceRange::default(),
|
||||||
|
kittycad::types::ModelingCmd::TakeSnapshot {
|
||||||
|
format: kittycad::types::ImageFormat::Png,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let output_file =
|
||||||
|
std::env::temp_dir().join(format!("kcl_output_{}.png", uuid::Uuid::new_v4()));
|
||||||
|
if let kittycad::types::OkWebSocketResponseData::Modeling {
|
||||||
|
modeling_response: kittycad::types::OkModelingCmdResponse::TakeSnapshot { data },
|
||||||
|
} = &resp
|
||||||
|
{
|
||||||
|
std::fs::write(&output_file, &data.contents.0).unwrap();
|
||||||
|
} else {
|
||||||
|
panic!("Unexpected response from engine: {:?}", resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
let actual = image::io::Reader::open(output_file)
|
||||||
|
.unwrap()
|
||||||
|
.decode()
|
||||||
|
.unwrap();
|
||||||
|
twenty_twenty::assert_image(
|
||||||
|
&format!("tests/outputs/{}.png", "serial_test_example_someFn0"),
|
||||||
|
&actual,
|
||||||
|
1.0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_camel_case_types, missing_docs)]
|
||||||
|
#[doc = "Std lib function: someFn\nDocs"]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, schemars :: JsonSchema, ts_rs :: TS)]
|
||||||
|
#[ts(export)]
|
||||||
|
pub(crate) struct SomeFn {}
|
||||||
|
|
||||||
|
#[allow(non_upper_case_globals, missing_docs)]
|
||||||
|
#[doc = "Std lib function: someFn\nDocs"]
|
||||||
|
pub(crate) const SomeFn: SomeFn = SomeFn {};
|
||||||
|
fn boxed_someFn(
|
||||||
|
args: crate::std::Args,
|
||||||
|
) -> std::pin::Pin<
|
||||||
|
Box<
|
||||||
|
dyn std::future::Future<
|
||||||
|
Output = anyhow::Result<crate::executor::MemoryItem, crate::errors::KclError>,
|
||||||
|
> + Send,
|
||||||
|
>,
|
||||||
|
> {
|
||||||
|
Box::pin(someFn(args))
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::docs::StdLibFn for SomeFn {
|
||||||
|
fn name(&self) -> String {
|
||||||
|
"someFn".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn summary(&self) -> String {
|
||||||
|
"Docs".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> String {
|
||||||
|
"".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tags(&self) -> Vec<String> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn args(&self) -> Vec<crate::docs::StdLibFnArg> {
|
||||||
|
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
||||||
|
settings.inline_subschemas = true;
|
||||||
|
let mut generator = schemars::gen::SchemaGenerator::new(settings);
|
||||||
|
vec![crate::docs::StdLibFnArg {
|
||||||
|
name: "data".to_string(),
|
||||||
|
type_: "Foo".to_string(),
|
||||||
|
schema: Foo::json_schema(&mut generator),
|
||||||
|
required: true,
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn return_value(&self) -> Option<crate::docs::StdLibFnArg> {
|
||||||
|
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
||||||
|
settings.inline_subschemas = true;
|
||||||
|
let mut generator = schemars::gen::SchemaGenerator::new(settings);
|
||||||
|
Some(crate::docs::StdLibFnArg {
|
||||||
|
name: "".to_string(),
|
||||||
|
type_: "i32".to_string(),
|
||||||
|
schema: <i32>::json_schema(&mut generator),
|
||||||
|
required: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unpublished(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deprecated(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn examples(&self) -> Vec<String> {
|
||||||
|
let code_blocks = vec!["someFn()"];
|
||||||
|
code_blocks
|
||||||
|
.iter()
|
||||||
|
.map(|cb| {
|
||||||
|
let tokens = crate::token::lexer(cb).unwrap();
|
||||||
|
let parser = crate::parser::Parser::new(tokens);
|
||||||
|
let program = parser.ast().unwrap();
|
||||||
|
let mut options: crate::ast::types::FormatOptions = Default::default();
|
||||||
|
options.insert_final_newline = false;
|
||||||
|
program.recast(&options, 0)
|
||||||
|
})
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn std_lib_fn(&self) -> crate::std::StdFn {
|
||||||
|
boxed_someFn
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clone_box(&self) -> Box<dyn crate::docs::StdLibFn> {
|
||||||
|
Box::new(self.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc = r" Docs"]
|
||||||
|
#[doc = r" ```"]
|
||||||
|
#[doc = r" someFn()"]
|
||||||
|
#[doc = r" ```"]
|
||||||
|
fn someFn<'a>(data: Foo<'a>) -> i32 {
|
||||||
|
3
|
||||||
|
}
|
194
src/wasm-lib/derive-docs/tests/args_with_refs.gen
Normal file
194
src/wasm-lib/derive-docs/tests/args_with_refs.gen
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod test_examples_someFn {
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn test_mock_example_someFn0() {
|
||||||
|
let tokens = crate::token::lexer("someFn()").unwrap();
|
||||||
|
let parser = crate::parser::Parser::new(tokens);
|
||||||
|
let program = parser.ast().unwrap();
|
||||||
|
let ctx = crate::executor::ExecutorContext {
|
||||||
|
engine: std::sync::Arc::new(Box::new(
|
||||||
|
crate::engine::conn_mock::EngineConnection::new()
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
)),
|
||||||
|
fs: std::sync::Arc::new(crate::fs::FileManager::new()),
|
||||||
|
stdlib: std::sync::Arc::new(crate::std::StdLib::new()),
|
||||||
|
settings: Default::default(),
|
||||||
|
is_mock: true,
|
||||||
|
};
|
||||||
|
ctx.run(program, None).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||||
|
async fn serial_test_example_someFn0() {
|
||||||
|
let user_agent = concat!(env!("CARGO_PKG_NAME"), ".rs/", env!("CARGO_PKG_VERSION"),);
|
||||||
|
let http_client = reqwest::Client::builder()
|
||||||
|
.user_agent(user_agent)
|
||||||
|
.timeout(std::time::Duration::from_secs(600))
|
||||||
|
.connect_timeout(std::time::Duration::from_secs(60));
|
||||||
|
let ws_client = reqwest::Client::builder()
|
||||||
|
.user_agent(user_agent)
|
||||||
|
.timeout(std::time::Duration::from_secs(600))
|
||||||
|
.connect_timeout(std::time::Duration::from_secs(60))
|
||||||
|
.connection_verbose(true)
|
||||||
|
.tcp_keepalive(std::time::Duration::from_secs(600))
|
||||||
|
.http1_only();
|
||||||
|
let token = std::env::var("KITTYCAD_API_TOKEN").expect("KITTYCAD_API_TOKEN not set");
|
||||||
|
let mut client = kittycad::Client::new_from_reqwest(token, http_client, ws_client);
|
||||||
|
if let Ok(addr) = std::env::var("LOCAL_ENGINE_ADDR") {
|
||||||
|
client.set_base_url(addr);
|
||||||
|
}
|
||||||
|
|
||||||
|
let tokens = crate::token::lexer("someFn()").unwrap();
|
||||||
|
let parser = crate::parser::Parser::new(tokens);
|
||||||
|
let program = parser.ast().unwrap();
|
||||||
|
let ctx = crate::executor::ExecutorContext::new(&client, Default::default())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
ctx.run(program, None).await.unwrap();
|
||||||
|
ctx.engine
|
||||||
|
.send_modeling_cmd(
|
||||||
|
uuid::Uuid::new_v4(),
|
||||||
|
crate::executor::SourceRange::default(),
|
||||||
|
kittycad::types::ModelingCmd::ZoomToFit {
|
||||||
|
object_ids: Default::default(),
|
||||||
|
padding: 0.1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let resp = ctx
|
||||||
|
.engine
|
||||||
|
.send_modeling_cmd(
|
||||||
|
uuid::Uuid::new_v4(),
|
||||||
|
crate::executor::SourceRange::default(),
|
||||||
|
kittycad::types::ModelingCmd::TakeSnapshot {
|
||||||
|
format: kittycad::types::ImageFormat::Png,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let output_file =
|
||||||
|
std::env::temp_dir().join(format!("kcl_output_{}.png", uuid::Uuid::new_v4()));
|
||||||
|
if let kittycad::types::OkWebSocketResponseData::Modeling {
|
||||||
|
modeling_response: kittycad::types::OkModelingCmdResponse::TakeSnapshot { data },
|
||||||
|
} = &resp
|
||||||
|
{
|
||||||
|
std::fs::write(&output_file, &data.contents.0).unwrap();
|
||||||
|
} else {
|
||||||
|
panic!("Unexpected response from engine: {:?}", resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
let actual = image::io::Reader::open(output_file)
|
||||||
|
.unwrap()
|
||||||
|
.decode()
|
||||||
|
.unwrap();
|
||||||
|
twenty_twenty::assert_image(
|
||||||
|
&format!("tests/outputs/{}.png", "serial_test_example_someFn0"),
|
||||||
|
&actual,
|
||||||
|
1.0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_camel_case_types, missing_docs)]
|
||||||
|
#[doc = "Std lib function: someFn\nDocs"]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, schemars :: JsonSchema, ts_rs :: TS)]
|
||||||
|
#[ts(export)]
|
||||||
|
pub(crate) struct SomeFn {}
|
||||||
|
|
||||||
|
#[allow(non_upper_case_globals, missing_docs)]
|
||||||
|
#[doc = "Std lib function: someFn\nDocs"]
|
||||||
|
pub(crate) const SomeFn: SomeFn = SomeFn {};
|
||||||
|
fn boxed_someFn(
|
||||||
|
args: crate::std::Args,
|
||||||
|
) -> std::pin::Pin<
|
||||||
|
Box<
|
||||||
|
dyn std::future::Future<
|
||||||
|
Output = anyhow::Result<crate::executor::MemoryItem, crate::errors::KclError>,
|
||||||
|
> + Send,
|
||||||
|
>,
|
||||||
|
> {
|
||||||
|
Box::pin(someFn(args))
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::docs::StdLibFn for SomeFn {
|
||||||
|
fn name(&self) -> String {
|
||||||
|
"someFn".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn summary(&self) -> String {
|
||||||
|
"Docs".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> String {
|
||||||
|
"".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tags(&self) -> Vec<String> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn args(&self) -> Vec<crate::docs::StdLibFnArg> {
|
||||||
|
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
||||||
|
settings.inline_subschemas = true;
|
||||||
|
let mut generator = schemars::gen::SchemaGenerator::new(settings);
|
||||||
|
vec![crate::docs::StdLibFnArg {
|
||||||
|
name: "data".to_string(),
|
||||||
|
type_: "string".to_string(),
|
||||||
|
schema: str::json_schema(&mut generator),
|
||||||
|
required: true,
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn return_value(&self) -> Option<crate::docs::StdLibFnArg> {
|
||||||
|
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
||||||
|
settings.inline_subschemas = true;
|
||||||
|
let mut generator = schemars::gen::SchemaGenerator::new(settings);
|
||||||
|
Some(crate::docs::StdLibFnArg {
|
||||||
|
name: "".to_string(),
|
||||||
|
type_: "i32".to_string(),
|
||||||
|
schema: <i32>::json_schema(&mut generator),
|
||||||
|
required: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unpublished(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deprecated(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn examples(&self) -> Vec<String> {
|
||||||
|
let code_blocks = vec!["someFn()"];
|
||||||
|
code_blocks
|
||||||
|
.iter()
|
||||||
|
.map(|cb| {
|
||||||
|
let tokens = crate::token::lexer(cb).unwrap();
|
||||||
|
let parser = crate::parser::Parser::new(tokens);
|
||||||
|
let program = parser.ast().unwrap();
|
||||||
|
let mut options: crate::ast::types::FormatOptions = Default::default();
|
||||||
|
options.insert_final_newline = false;
|
||||||
|
program.recast(&options, 0)
|
||||||
|
})
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn std_lib_fn(&self) -> crate::std::StdFn {
|
||||||
|
boxed_someFn
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clone_box(&self) -> Box<dyn crate::docs::StdLibFn> {
|
||||||
|
Box::new(self.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc = r" Docs"]
|
||||||
|
#[doc = r" ```"]
|
||||||
|
#[doc = r" someFn()"]
|
||||||
|
#[doc = r" ```"]
|
||||||
|
fn someFn(data: &'a str) -> i32 {
|
||||||
|
3
|
||||||
|
}
|
Reference in New Issue
Block a user