use ide_db::assists::{AssistId, AssistKind}; use stdx::to_lower_snake_case; use syntax::{ ast::{self, edit::IndentLevel, HasDocComments, HasName}, AstNode, }; use crate::assist_context::{AssistContext, Assists}; // Assist: generate_documentation_template // // Adds a documentation template above a function definition / declaration. // // ``` // fn my_$0func(a: i32, b: i32) -> Result<(), std::io::Error> { // unimplemented!() // } // ``` // -> // ``` // /// . // /// // /// # Examples // /// // /// ``` // /// use test::my_func; // /// // /// assert_eq!(my_func(a, b), ); // /// ``` // /// // /// # Errors // /// // /// This function will return an error if . // fn my_func(a: i32, b: i32) -> Result<(), std::io::Error> { // unimplemented!() // } // ``` pub(crate) fn generate_documentation_template( acc: &mut Assists, ctx: &AssistContext, ) -> Option<()> { let name = ctx.find_node_at_offset::()?; let ast_func = ast::Fn::cast(name.syntax().parent()?)?; if is_in_trait_impl(&ast_func) { return None; } // TODO disable at least examples if function not public, as the example will fail to build on // `cargo test`. What is the exact criteria of `pub`ness? All parent modules must be `pub`, for // `impl { fn }` both `fn` and `struct`* must be public. // // What about `pub(crate)`? // // *: Seems complex but maybe ignoring this criteria can be ignored. let parent_syntax = ast_func.syntax(); let text_range = parent_syntax.text_range(); let indent_level = IndentLevel::from_node(&parent_syntax); let krate_name = ctx.sema.scope(&parent_syntax).module()?.krate().display_name(ctx.db())?.to_string(); acc.add( AssistId("generate_documentation_template", AssistKind::Generate), "Generate a documentation template", text_range, |builder| { let mut doc_lines = Vec::new(); // Introduction / short function description before the sections doc_lines.push(introduction_builder(&ast_func)); // Then come the sections if let Some(mut lines) = examples_builder(&ast_func, krate_name) { doc_lines.push("".into()); doc_lines.append(&mut lines); } for section_builder in [panics_builder, errors_builder, safety_builder] { if let Some(mut lines) = section_builder(&ast_func) { doc_lines.push("".into()); doc_lines.append(&mut lines); } } if ast_func.doc_comments().next().is_some() { doc_lines.push("--- OLD VERSION BELOW ---".into()); } builder.insert(text_range.start(), documentation_from_lines(doc_lines, indent_level)); }, ) } /// Builds an introduction, trying to be smart if the function is `::new()` fn introduction_builder(ast_func: &ast::Fn) -> String { let is_new = ast_func.name().map(|name| &name.to_string() == "new").unwrap_or(false); if is_new { let ret_type = return_type(ast_func).map(|ret_type| ret_type.to_string()); let self_type = self_type(ast_func); if ret_type.as_deref() == Some("Self") || ret_type == self_type { if let Some(self_type) = self_type { return format!("Creates a new [`{}`].", self_type); } } } ".".into() } /// Builds an `# Examples` section. An option is returned to be able to manage an error in the AST. fn examples_builder(ast_func: &ast::Fn, krate_name: String) -> Option> { let (no_panic_ex, panic_ex) = if is_in_trait_def(ast_func) { let message = "// Example template not implemented for trait functions"; (Some(vec![message.into()]), Some(vec![message.into()])) } else { let panic_ex = match can_panic(ast_func) { Some(true) => gen_panic_ex_template(ast_func, krate_name.clone()), _ => None, }; let no_panic_ex = gen_ex_template(ast_func, krate_name); (no_panic_ex, panic_ex) }; let mut lines = string_vec_from(&["# Examples", "", "```"]); lines.append(&mut no_panic_ex?); lines.push("```".into()); if let Some(mut ex) = panic_ex { lines.push("".into()); lines.push("```should_panic".into()); lines.append(&mut ex); lines.push("```".into()); } Some(lines) } /// Builds an optional `# Panics` section fn panics_builder(ast_func: &ast::Fn) -> Option> { match can_panic(ast_func) { Some(true) => Some(string_vec_from(&["# Panics", "", "Panics if ."])), _ => None, } } /// Builds an optional `# Errors` section fn errors_builder(ast_func: &ast::Fn) -> Option> { match return_type(ast_func)?.to_string().contains("Result") { true => Some(string_vec_from(&["# Errors", "", "This function will return an error if ."])), false => None, } } /// Builds an optional `# Safety` section fn safety_builder(ast_func: &ast::Fn) -> Option> { let is_unsafe = ast_func.unsafe_token().is_some(); match is_unsafe { true => Some(string_vec_from(&["# Safety", "", "."])), false => None, } } /// Generate an example template which should not panic /// `None` if the function has a `self` parameter but is not in an `impl`. fn gen_ex_template(ast_func: &ast::Fn, krate_name: String) -> Option> { let (mut lines, ex_helper) = gen_ex_start_helper(ast_func, krate_name)?; // Call the function, check result if returns_a_value(ast_func) { if count_parameters(&ex_helper.param_list) < 3 { lines.push(format!("assert_eq!({}, );", ex_helper.function_call)); } else { lines.push(format!("let result = {};", ex_helper.function_call)); lines.push("assert_eq!(result, );".into()); } } else { lines.push(format!("{};", ex_helper.function_call)); } // Check the mutated values if is_ref_mut_self(ast_func) == Some(true) { lines.push(format!("assert_eq!({}, );", ex_helper.self_name?)); } for param_name in &ex_helper.ref_mut_params { lines.push(format!("assert_eq!({}, );", param_name)); } Some(lines) } /// Generate an example template which should panic /// `None` if the function has a `self` parameter but is not in an `impl`. fn gen_panic_ex_template(ast_func: &ast::Fn, krate_name: String) -> Option> { let (mut lines, ex_helper) = gen_ex_start_helper(ast_func, krate_name)?; match returns_a_value(ast_func) { true => lines.push(format!("let _ = {}; // panics", ex_helper.function_call)), false => lines.push(format!("{}; // panics", ex_helper.function_call)), } Some(lines) } /// Intermediary results of the start of example generation struct ExHelper { function_call: String, param_list: ast::ParamList, ref_mut_params: Vec, self_name: Option, } /// Build the start of the example and transmit the useful intermediary results. /// `None` if the function has a `self` parameter but is not in an `impl`. fn gen_ex_start_helper(ast_func: &ast::Fn, krate_name: String) -> Option<(Vec, ExHelper)> { let mut lines = Vec::new(); let is_unsafe = ast_func.unsafe_token().is_some(); let param_list = ast_func.param_list()?; let ref_mut_params = ref_mut_params(¶m_list); let self_name: Option = self_name(ast_func); lines.push(format!("use {};", build_path(ast_func, krate_name))); lines.push("".into()); if let Some(self_definition) = self_definition(ast_func, self_name.as_deref()) { lines.push(self_definition); } for param_name in &ref_mut_params { lines.push(format!("let mut {} = ;", param_name)) } let function_call = function_call(ast_func, ¶m_list, self_name.as_deref(), is_unsafe)?; let ex_helper = ExHelper { function_call, param_list, ref_mut_params, self_name }; Some((lines, ex_helper)) } /// `None` if function without a body; some bool to guess if function can panic fn can_panic(ast_func: &ast::Fn) -> Option { let body = ast_func.body()?.to_string(); let can_panic = body.contains("panic!(") || body.contains("assert!(") || body.contains(".unwrap()") || body.contains(".expect("); Some(can_panic) } /// Helper function to get the name that should be given to `self` arguments fn self_name(ast_func: &ast::Fn) -> Option { self_partial_type(ast_func).map(|name| to_lower_snake_case(&name)) } /// Heper function to get the name of the type of `self` fn self_type(ast_func: &ast::Fn) -> Option { ast_func .syntax() .ancestors() .find_map(ast::Impl::cast) .and_then(|i| i.self_ty()) .map(|t| (t.to_string())) } /// Heper function to get the name of the type of `self` without generic arguments fn self_partial_type(ast_func: &ast::Fn) -> Option { let mut self_type = self_type(ast_func)?; if let Some(idx) = self_type.find(|c| ['<', ' '].contains(&c)) { self_type.truncate(idx); } Some(self_type) } /// Helper function to determine if the function is in a trait implementation fn is_in_trait_impl(ast_func: &ast::Fn) -> bool { ast_func .syntax() .ancestors() .find_map(ast::Impl::cast) .and_then(|impl_| impl_.trait_()) .is_some() } /// Helper function to determine if the function definition is in a trait definition fn is_in_trait_def(ast_func: &ast::Fn) -> bool { ast_func.syntax().ancestors().find_map(ast::Trait::cast).is_some() } /// Returns `None` if no `self` at all, `Some(true)` if there is `&mut self` else `Some(false)` fn is_ref_mut_self(ast_func: &ast::Fn) -> Option { let self_param = ast_func.param_list()?.self_param()?; Some(self_param.mut_token().is_some() && self_param.amp_token().is_some()) } /// Helper function to define an variable to be the `self` argument fn self_definition(ast_func: &ast::Fn, self_name: Option<&str>) -> Option { let definition = match is_ref_mut_self(ast_func)? { true => format!("let mut {} = ;", self_name?), false => format!("let {} = ;", self_name?), }; Some(definition) } /// Helper function to determine if a parameter is `&mut` fn is_a_ref_mut_param(param: &ast::Param) -> bool { match param.ty() { Some(ast::Type::RefType(param_ref)) => param_ref.mut_token().is_some(), _ => false, } } /// Helper function to build the list of `&mut` parameters fn ref_mut_params(param_list: &ast::ParamList) -> Vec { param_list .params() .filter_map(|param| match is_a_ref_mut_param(¶m) { // Maybe better filter the param name (to do this maybe extract a function from // `arguments_from_params`?) in case of a `mut a: &mut T`. Anyway managing most (not // all) cases might be enough, the goal is just to produce a template. true => Some(param.pat()?.to_string()), false => None, }) .collect() } /// Helper function to build the comma-separated list of arguments of the function fn arguments_from_params(param_list: &ast::ParamList) -> String { let args_iter = param_list.params().map(|param| match param.pat() { // To avoid `mut` in the function call (which would be a nonsense), `Pat` should not be // written as is so its variants must be managed independently. Other variants (for // instance `TuplePat`) could be managed later. Some(ast::Pat::IdentPat(ident_pat)) => match ident_pat.name() { Some(name) => match is_a_ref_mut_param(¶m) { true => format!("&mut {}", name.to_string()), false => name.to_string(), }, None => "_".to_string(), }, _ => "_".to_string(), }); intersperse_string(args_iter, ", ") } /// Helper function to build a function call. `None` if expected `self_name` was not provided fn function_call( ast_func: &ast::Fn, param_list: &ast::ParamList, self_name: Option<&str>, is_unsafe: bool, ) -> Option { let name = ast_func.name()?; let arguments = arguments_from_params(¶m_list); let function_call = if param_list.self_param().is_some() { format!("{}.{}({})", self_name?, name, arguments) } else if let Some(implementation) = self_partial_type(ast_func) { format!("{}::{}({})", implementation, name, arguments) } else { format!("{}({})", name, arguments) }; match is_unsafe { true => Some(format!("unsafe {{ {} }}", function_call)), false => Some(function_call), } } /// Helper function to count the parameters including `self` fn count_parameters(param_list: &ast::ParamList) -> usize { param_list.params().count() + if param_list.self_param().is_some() { 1 } else { 0 } } /// Helper function to transform lines of documentation into a Rust code documentation fn documentation_from_lines(doc_lines: Vec, indent_level: IndentLevel) -> String { let mut result = String::new(); for doc_line in doc_lines { result.push_str("///"); if !doc_line.is_empty() { result.push(' '); result.push_str(&doc_line); } result.push('\n'); result.push_str(&indent_level.to_string()); } result } /// Helper function to transform an array of borrowed strings to an owned `Vec` fn string_vec_from(string_array: &[&str]) -> Vec { string_array.iter().map(|&s| s.to_owned()).collect() } /// Helper function to build the path of the module in the which is the node fn build_path(ast_func: &ast::Fn, krate_name: String) -> String { let mut path: Vec = ast_func .syntax() .ancestors() .filter_map(|m| ast::Module::cast(m).and_then(|m| m.name())) .map(|m| m.to_string()) .collect(); path.push(krate_name); path.reverse(); path.push( self_partial_type(ast_func) .or_else(|| ast_func.name().map(|n| n.to_string())) .unwrap_or_else(|| "*".into()), ); intersperse_string(path.into_iter(), "::") } /// Helper function to get the return type of a function fn return_type(ast_func: &ast::Fn) -> Option { ast_func.ret_type()?.ty() } /// Helper function to determine if the function returns some data fn returns_a_value(ast_func: &ast::Fn) -> bool { match return_type(ast_func) { Some(ret_type) => !["()", "!"].contains(&ret_type.to_string().as_str()), None => false, } } /// Helper function to concatenate string with a separator between them fn intersperse_string(mut iter: impl Iterator, separator: &str) -> String { let mut result = String::new(); if let Some(first) = iter.next() { result.push_str(&first); } for string in iter { result.push_str(separator); result.push_str(&string); } result } #[cfg(test)] mod tests { use crate::tests::{check_assist, check_assist_not_applicable}; use super::*; #[test] fn not_applicable_on_function_calls() { check_assist_not_applicable( generate_documentation_template, r#" fn hello_world() {} fn calls_hello_world() { hello_world$0(); } "#, ) } #[test] fn not_applicable_in_trait_impl() { check_assist_not_applicable( generate_documentation_template, r#" trait MyTrait {} struct MyStruct; impl MyTrait for MyStruct { fn hello_world$0(); } "#, ) } #[test] fn supports_noop_function() { check_assist( generate_documentation_template, r#" fn no$0op() {} "#, r#" /// . /// /// # Examples /// /// ``` /// use test::noop; /// /// noop(); /// ``` fn noop() {} "#, ); } #[test] fn supports_a_parameter() { check_assist( generate_documentation_template, r#" fn no$0op_with_param(_a: i32) {} "#, r#" /// . /// /// # Examples /// /// ``` /// use test::noop_with_param; /// /// noop_with_param(_a); /// ``` fn noop_with_param(_a: i32) {} "#, ); } #[test] fn detects_unsafe_function() { check_assist( generate_documentation_template, r#" unsafe fn no$0op_unsafe() {} "#, r#" /// . /// /// # Examples /// /// ``` /// use test::noop_unsafe; /// /// unsafe { noop_unsafe() }; /// ``` /// /// # Safety /// /// . unsafe fn noop_unsafe() {} "#, ); } #[test] fn guesses_panic_macro_can_panic() { check_assist( generate_documentation_template, r#" fn panic$0s_if(a: bool) { if a { panic!(); } } "#, r#" /// . /// /// # Examples /// /// ``` /// use test::panics_if; /// /// panics_if(a); /// ``` /// /// ```should_panic /// use test::panics_if; /// /// panics_if(a); // panics /// ``` /// /// # Panics /// /// Panics if . fn panics_if(a: bool) { if a { panic!(); } } "#, ); } #[test] fn guesses_assert_macro_can_panic() { check_assist( generate_documentation_template, r#" fn $0panics_if_not(a: bool) { assert!(a == true); } "#, r#" /// . /// /// # Examples /// /// ``` /// use test::panics_if_not; /// /// panics_if_not(a); /// ``` /// /// ```should_panic /// use test::panics_if_not; /// /// panics_if_not(a); // panics /// ``` /// /// # Panics /// /// Panics if . fn panics_if_not(a: bool) { assert!(a == true); } "#, ); } #[test] fn guesses_unwrap_can_panic() { check_assist( generate_documentation_template, r#" fn $0panics_if_none(a: Option<()>) { a.unwrap(); } "#, r#" /// . /// /// # Examples /// /// ``` /// use test::panics_if_none; /// /// panics_if_none(a); /// ``` /// /// ```should_panic /// use test::panics_if_none; /// /// panics_if_none(a); // panics /// ``` /// /// # Panics /// /// Panics if . fn panics_if_none(a: Option<()>) { a.unwrap(); } "#, ); } #[test] fn guesses_expect_can_panic() { check_assist( generate_documentation_template, r#" fn $0panics_if_none2(a: Option<()>) { a.expect("Bouh!"); } "#, r#" /// . /// /// # Examples /// /// ``` /// use test::panics_if_none2; /// /// panics_if_none2(a); /// ``` /// /// ```should_panic /// use test::panics_if_none2; /// /// panics_if_none2(a); // panics /// ``` /// /// # Panics /// /// Panics if . fn panics_if_none2(a: Option<()>) { a.expect("Bouh!"); } "#, ); } #[test] fn checks_output_in_example() { check_assist( generate_documentation_template, r#" fn returns_a_value$0() -> i32 { 0 } "#, r#" /// . /// /// # Examples /// /// ``` /// use test::returns_a_value; /// /// assert_eq!(returns_a_value(), ); /// ``` fn returns_a_value() -> i32 { 0 } "#, ); } #[test] fn detects_result_output() { check_assist( generate_documentation_template, r#" fn returns_a_result$0() -> Result { Ok(0) } "#, r#" /// . /// /// # Examples /// /// ``` /// use test::returns_a_result; /// /// assert_eq!(returns_a_result(), ); /// ``` /// /// # Errors /// /// This function will return an error if . fn returns_a_result() -> Result { Ok(0) } "#, ); } #[test] fn checks_ref_mut_in_example() { check_assist( generate_documentation_template, r#" fn modifies_a_value$0(a: &mut i32) { *a = 0; } "#, r#" /// . /// /// # Examples /// /// ``` /// use test::modifies_a_value; /// /// let mut a = ; /// modifies_a_value(&mut a); /// assert_eq!(a, ); /// ``` fn modifies_a_value(a: &mut i32) { *a = 0; } "#, ); } #[test] fn stores_result_if_at_least_3_params() { check_assist( generate_documentation_template, r#" fn sum3$0(a: i32, b: i32, c: i32) -> i32 { a + b + c } "#, r#" /// . /// /// # Examples /// /// ``` /// use test::sum3; /// /// let result = sum3(a, b, c); /// assert_eq!(result, ); /// ``` fn sum3(a: i32, b: i32, c: i32) -> i32 { a + b + c } "#, ); } #[test] fn supports_fn_in_mods() { check_assist( generate_documentation_template, r#" mod a { mod b { fn no$0op() {} } } "#, r#" mod a { mod b { /// . /// /// # Examples /// /// ``` /// use test::a::b::noop; /// /// noop(); /// ``` fn noop() {} } } "#, ); } #[test] fn supports_fn_in_impl() { check_assist( generate_documentation_template, r#" struct MyStruct; impl MyStruct { fn no$0op() {} } "#, r#" struct MyStruct; impl MyStruct { /// . /// /// # Examples /// /// ``` /// use test::MyStruct; /// /// MyStruct::noop(); /// ``` fn noop() {} } "#, ); } #[test] fn detects_new() { check_assist( generate_documentation_template, r#" #[derive(Debug, PartialEq)] pub struct MyGenericStruct { pub x: T, } impl MyGenericStruct { pub fn new$0(x: T) -> MyGenericStruct { MyGenericStruct { x } } } "#, r#" #[derive(Debug, PartialEq)] pub struct MyGenericStruct { pub x: T, } impl MyGenericStruct { /// Creates a new [`MyGenericStruct`]. /// /// # Examples /// /// ``` /// use test::MyGenericStruct; /// /// assert_eq!(MyGenericStruct::new(x), ); /// ``` pub fn new(x: T) -> MyGenericStruct { MyGenericStruct { x } } } "#, ); } #[test] fn detects_new_with_self() { check_assist( generate_documentation_template, r#" #[derive(Debug, PartialEq)] pub struct MyGenericStruct2 { pub x: T, } impl MyGenericStruct2 { pub fn new$0(x: T) -> Self { MyGenericStruct2 { x } } } "#, r#" #[derive(Debug, PartialEq)] pub struct MyGenericStruct2 { pub x: T, } impl MyGenericStruct2 { /// Creates a new [`MyGenericStruct2`]. /// /// # Examples /// /// ``` /// use test::MyGenericStruct2; /// /// assert_eq!(MyGenericStruct2::new(x), ); /// ``` pub fn new(x: T) -> Self { MyGenericStruct2 { x } } } "#, ); } #[test] fn supports_method_call() { check_assist( generate_documentation_template, r#" impl MyGenericStruct { pub fn co$0nsume(self) {} } "#, r#" impl MyGenericStruct { /// . /// /// # Examples /// /// ``` /// use test::MyGenericStruct; /// /// let my_generic_struct = ; /// my_generic_struct.consume(); /// ``` pub fn consume(self) {} } "#, ); } #[test] fn checks_modified_self_param() { check_assist( generate_documentation_template, r#" impl MyGenericStruct { pub fn modi$0fy(&mut self, new_value: T) { self.x = new_value; } } "#, r#" impl MyGenericStruct { /// . /// /// # Examples /// /// ``` /// use test::MyGenericStruct; /// /// let mut my_generic_struct = ; /// my_generic_struct.modify(new_value); /// assert_eq!(my_generic_struct, ); /// ``` pub fn modify(&mut self, new_value: T) { self.x = new_value; } } "#, ); } }