From dcddb1d783ee1c0e6f54d53582a3f9a24dfb5a5c Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Sun, 11 Feb 2024 17:23:03 +0700 Subject: [PATCH] init: doc comment support Co-authored-by: Shahar Dawn Or --- CHANGELOG.md | 33 ++ README.md | 61 +++- src/comment.rs | 110 ++++++ src/format.rs | 104 ++++++ src/legacy.rs | 116 ++++++ src/main.rs | 338 +++--------------- ...snap => nixdoc__test__arg_formatting.snap} | 0 ...xdoc__test__description_of_lib_debug.snap} | 0 src/snapshots/nixdoc__test__doc_comment.md | 58 +++ src/snapshots/nixdoc__test__doc_comment.snap | 58 +++ ...test__doc_comment_section_description.snap | 8 + ...p => nixdoc__test__inherited_exports.snap} | 0 ....snap => nixdoc__test__line_comments.snap} | 0 ...doc__main.snap => nixdoc__test__main.snap} | 0 ...ine.snap => nixdoc__test__multi_line.snap} | 0 src/test.rs | 172 +++++++++ test.nix | 37 ++ test/doc-comment-sec-heading.nix | 4 + test/doc-comment.nix | 53 +++ 19 files changed, 869 insertions(+), 283 deletions(-) create mode 100644 src/comment.rs create mode 100644 src/format.rs create mode 100644 src/legacy.rs rename src/snapshots/{nixdoc__arg_formatting.snap => nixdoc__test__arg_formatting.snap} (100%) rename src/snapshots/{nixdoc__description_of_lib_debug.snap => nixdoc__test__description_of_lib_debug.snap} (100%) create mode 100644 src/snapshots/nixdoc__test__doc_comment.md create mode 100644 src/snapshots/nixdoc__test__doc_comment.snap create mode 100644 src/snapshots/nixdoc__test__doc_comment_section_description.snap rename src/snapshots/{nixdoc__inherited_exports.snap => nixdoc__test__inherited_exports.snap} (100%) rename src/snapshots/{nixdoc__line_comments.snap => nixdoc__test__line_comments.snap} (100%) rename src/snapshots/{nixdoc__main.snap => nixdoc__test__main.snap} (100%) rename src/snapshots/{nixdoc__multi_line.snap => nixdoc__test__multi_line.snap} (100%) create mode 100644 src/test.rs create mode 100644 test.nix create mode 100644 test/doc-comment-sec-heading.nix create mode 100644 test/doc-comment.nix diff --git a/CHANGELOG.md b/CHANGELOG.md index 0170e3c..e55ad45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,38 @@ # Changelog +## 3.0.0 + +- Added support for official doc-comments (RFC145) +- Custom nixdoc format is now deprecated and will be removed eventually in future releases. + + By @hsjobeki in https://github.com/nix-community/nixdoc/pull/91 + +### Migration from nixdoc < 2.x.x to 3.x.x + +#### Double asterisk + +Comments that are intended for documentation should have `/** content */`. This allows the tool to distinguish between internal-comments and documentation comments. + +#### Argument documentation + +As of RFC145 specified there are is no argument documentation. For single arguments such as `a:` write the documentation into the regular content of your doc-comment. + +**Example:** Single argument migration + +```nix +{ + /** + The id function + + # Arguments + + - `x` (`Any`): The provided value + + */ + id = x: x; +} +``` + ## 2.7.0 - Added support to customise the attribute set prefix, which was previously hardcoded to `lib`. diff --git a/README.md b/README.md index a593696..b4e162d 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,57 @@ function set. ## Comment format -Currently, identifiers are included in the documentation if they have -a preceding comment in multiline syntax `/* something */`. +This tool implements a subset of the doc-comment standard specified in [RFC-145/doc-comments](https://github.com/NixOS/rfcs/blob/master/rfcs/0145-doc-strings.md). +But, it is currently limited to generating documentation for statically analyzable attribute paths only. +In the future, it could be the role of a Nix interpreter to obtain the values to be documented and their doc-comments. + +It is important to start doc-comments with the additional asterisk (`*`) -> `/**` which renders as a doc-comment. + +The content of the doc-comment should be some markdown. ( See [Commonmark](https://spec.commonmark.org/0.30/) specification) + +### Example + +The following is an example of markdown documentation for new and current users of nixdoc. + +> Sidenote: Indentation is automatically detected and should be consistent across the content. +> +> If you are used to multiline-strings (`''`) in nix this should be intuitive to follow. + +````nix +{ + /** + This function adds two numbers + + # Example + + ```nix + add 4 5 + => + 9 + ``` + + # Type + + ``` + add :: Number -> Number -> Number + ``` + + # Arguments + + - a: The first number + - b: The second number + + */ + add = a: b: a + b; +} +```` + +## Outdated Format (Legacy) + +### Comment format + +Identifiers are included in the documentation if they have +a preceding comment in multiline syntax `/* something */`. You should consider migrating to the new format described above. Two special line beginnings are recognised: @@ -22,10 +71,16 @@ Two special line beginnings are recognised: These will result in appropriate elements being inserted into the output. -## Function arguments +### Function arguments Function arguments can be documented by prefixing them with a comment: +> Note: This only works in the legacy format and should not be relied on anymore. +> +> See the markdown example above how to write proper documentation. +> +> For multiple reasons we cannot continue this feature. + ``` /* This function does the thing a number of times. */ myFunction = diff --git a/src/comment.rs b/src/comment.rs new file mode 100644 index 0000000..ec28039 --- /dev/null +++ b/src/comment.rs @@ -0,0 +1,110 @@ +use rnix::ast::{self, AstToken}; +use rnix::{match_ast, SyntaxNode}; +use rowan::ast::AstNode; + +/// Implements functions for doc-comments according to rfc145. +pub trait DocComment { + fn doc_text(&self) -> Option<&str>; +} + +impl DocComment for ast::Comment { + /// Function returns the contents of the doc-comment, if the [ast::Comment] is a + /// doc-comment, or None otherwise. + /// + /// Note: [ast::Comment] holds both the single-line and multiline comment. + /// + /// /**{content}*/ + /// -> {content} + /// + /// It is named `doc_text` to complement [ast::Comment::text]. + fn doc_text(&self) -> Option<&str> { + let text = self.syntax().text(); + // Check whether this is a doc-comment + if text.starts_with(r#"/**"#) && self.text().starts_with('*') { + self.text().strip_prefix('*') + } else { + None + } + } +} + +/// Function retrieves a doc-comment from the [ast::Expr] +/// +/// Returns an [Option] of the first suitable doc-comment. +/// Returns [None] in case no suitable comment was found. +/// +/// Doc-comments can appear in two places for any expression +/// +/// ```nix +/// # (1) directly before the expression (anonymous) +/// /** Doc */ +/// bar: bar; +/// +/// # (2) when assigning a name. +/// { +/// /** Doc */ +/// foo = bar: bar; +/// } +/// ``` +/// +/// If the doc-comment is not found in place (1) the search continues at place (2) +/// More precisely before the NODE_ATTRPATH_VALUE (ast) +/// If no doc-comment was found in place (1) or (2) this function returns None. +pub fn get_expr_docs(expr: &SyntaxNode) -> Option { + if let Some(doc) = get_doc_comment(expr) { + // Found in place (1) + doc.doc_text().map(|v| v.to_owned()) + } else if let Some(ref parent) = expr.parent() { + match_ast! { + match parent { + ast::AttrpathValue(_) => { + if let Some(doc_comment) = get_doc_comment(parent) { + doc_comment.doc_text().map(|v| v.to_owned()) + }else{ + None + } + }, + _ => { + // Yet unhandled ast-nodes + None + } + + } + } + // None + } else { + // There is no parent; + // No further places where a doc-comment could be. + None + } +} + +/// Looks backwards from the given expression +/// Only whitespace or non-doc-comments are allowed in between an expression and the doc-comment. +/// Any other Node or Token stops the peek. +fn get_doc_comment(expr: &SyntaxNode) -> Option { + let mut prev = expr.prev_sibling_or_token(); + loop { + match prev { + Some(rnix::NodeOrToken::Token(ref token)) => { + match_ast! { match token { + ast::Whitespace(_) => { + prev = token.prev_sibling_or_token(); + }, + ast::Comment(it) => { + if it.doc_text().is_some() { + break Some(it); + }else{ + //Ignore non-doc comments. + prev = token.prev_sibling_or_token(); + } + }, + _ => { + break None; + } + }} + } + _ => break None, + }; + } +} diff --git a/src/format.rs b/src/format.rs new file mode 100644 index 0000000..1eece5b --- /dev/null +++ b/src/format.rs @@ -0,0 +1,104 @@ +use textwrap::dedent; + +/// Ensure all lines in a multi-line doc-comments have the same indentation. +/// +/// Consider such a doc comment: +/// +/// ```nix +/// { +/// /* foo is +/// the value: +/// 10 +/// */ +/// foo = 10; +/// } +/// ``` +/// +/// The parser turns this into: +/// +/// ``` +/// foo is +/// the value: +/// 10 +/// ``` +/// +/// +/// where the first line has no leading indentation, and all other lines have preserved their +/// original indentation. +/// +/// What we want instead is: +/// +/// ``` +/// foo is +/// the value: +/// 10 +/// ``` +/// +/// i.e. we want the whole thing to be dedented. To achieve this, we remove all leading whitespace +/// from the first line, and remove all common whitespace from the rest of the string. +pub fn handle_indentation(raw: &str) -> Option { + let result: String = match raw.split_once('\n') { + Some((first, rest)) => { + format!("{}\n{}", first.trim_start(), dedent(rest)) + } + None => raw.into(), + }; + + Some(result.trim().to_owned()).filter(|s| !s.is_empty()) +} + +/// Shift down markdown headings +/// +/// Performs a line-wise matching to ' # Heading ' +/// +/// Counts the current numbers of '#' and adds levels: [usize] to them +/// levels := 1; gives +/// '# Heading' -> '## Heading' +/// +/// Markdown has 6 levels of headings. Everything beyond that (e.g., H7) may produce unexpected renderings. +/// by default this function makes sure, headings don't exceed the H6 boundary. +/// levels := 2; +/// ... +/// H4 -> H6 +/// H6 -> H6 +/// +pub fn shift_headings(raw: &str, levels: usize) -> String { + let mut result = String::new(); + for line in raw.lines() { + if line.trim_start().starts_with('#') { + result.push_str(&format!("{}\n", &handle_heading(line, levels))); + } else { + result.push_str(&format!("{line}\n")); + } + } + result +} + +// Dumb heading parser. +pub fn handle_heading(line: &str, levels: usize) -> String { + let chars = line.chars(); + + let mut leading_trivials: String = String::new(); + let mut hashes = String::new(); + let mut rest = String::new(); + for char in chars { + match char { + ' ' | '\t' if hashes.is_empty() => { + // only collect trivial before the initial hash + leading_trivials.push(char) + } + '#' if rest.is_empty() => { + // only collect hashes if no other tokens + hashes.push(char) + } + _ => rest.push(char), + } + } + let new_hashes = match hashes.len() + levels { + // We reached the maximum heading size. + 6.. => "#".repeat(6), + _ => "#".repeat(hashes.len() + levels), + }; + + format!("{leading_trivials}{new_hashes} {rest}") +} diff --git a/src/legacy.rs b/src/legacy.rs new file mode 100644 index 0000000..b495848 --- /dev/null +++ b/src/legacy.rs @@ -0,0 +1,116 @@ +use rnix::{ + ast::{AstToken, Attr, AttrpathValue, Comment, Expr, Inherit, Lambda, LetIn, Param}, + SyntaxKind, SyntaxNode, +}; +use rowan::ast::AstNode; + +use crate::{ + commonmark::{Argument, SingleArg}, + format::handle_indentation, + retrieve_doc_comment, +}; + +/// Retrieve documentation comments. +pub fn retrieve_legacy_comment(node: &SyntaxNode, allow_line_comments: bool) -> Option { + // if the current node has a doc comment it'll be immediately preceded by that comment, + // or there will be a whitespace token and *then* the comment tokens before it. We merge + // multiple line comments into one large comment if they are on adjacent lines for + // documentation simplicity. + let mut token = node.first_token()?.prev_token()?; + if token.kind() == SyntaxKind::TOKEN_WHITESPACE { + token = token.prev_token()?; + } + if token.kind() != SyntaxKind::TOKEN_COMMENT { + return None; + } + // if we want to ignore line comments (eg because they may contain deprecation + // comments on attributes) we'll backtrack to the first preceding multiline comment. + while !allow_line_comments && token.text().starts_with('#') { + token = token.prev_token()?; + if token.kind() == SyntaxKind::TOKEN_WHITESPACE { + token = token.prev_token()?; + } + if token.kind() != SyntaxKind::TOKEN_COMMENT { + return None; + } + } + + if token.text().starts_with("/*") { + return Some(Comment::cast(token)?.text().to_string()); + } + + // backtrack to the start of the doc comment, allowing only adjacent line comments. + // we don't care much about optimization here, doc comments aren't long enough for that. + if token.text().starts_with('#') { + let mut result = String::new(); + while let Some(comment) = Comment::cast(token) { + if !comment.syntax().text().starts_with('#') { + break; + } + result.insert_str(0, comment.text().trim()); + let ws = match comment.syntax().prev_token() { + Some(t) if t.kind() == SyntaxKind::TOKEN_WHITESPACE => t, + _ => break, + }; + // only adjacent lines continue a doc comment, empty lines do not. + match ws.text().strip_prefix('\n') { + Some(trail) if !trail.contains('\n') => result.insert(0, ' '), + _ => break, + } + token = match ws.prev_token() { + Some(c) => c, + _ => break, + }; + } + return Some(result); + } + + None +} + +/// Traverse directly chained nix lambdas and collect the identifiers of all lambda arguments +/// until an unexpected AST node is encountered. +pub fn collect_lambda_args(mut lambda: Lambda) -> Vec { + let mut args = vec![]; + + loop { + match lambda.param().unwrap() { + // a variable, e.g. `x:` in `id = x: x` + // Single args are not supported by RFC145, due to ambiguous placement rules. + Param::IdentParam(id) => { + args.push(Argument::Flat(SingleArg { + name: id.to_string(), + doc: handle_indentation( + &retrieve_legacy_comment(id.syntax(), true).unwrap_or_default(), + ), + })); + } + // an ident in a pattern, e.g. `a` in `foo = { a }: a` + Param::Pattern(pat) => { + // collect doc-comments for each lambda formal + // Lambda formals are supported by RFC145 + let pattern_vec: Vec<_> = pat + .pat_entries() + .map(|entry| SingleArg { + name: entry.ident().unwrap().to_string(), + doc: handle_indentation( + &retrieve_doc_comment(&entry.syntax(), Some(1)) + .or(retrieve_legacy_comment(&entry.syntax(), true)) + .unwrap_or_default(), + ), + }) + .collect(); + + args.push(Argument::Pattern(pattern_vec)); + } + } + + // Curried or not? + match lambda.body() { + Some(Expr::Lambda(inner)) => lambda = inner, + _ => break, + } + } + + args +} diff --git a/src/main.rs b/src/main.rs index 3a0b2a1..2c95a9c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,16 +21,25 @@ //! * extract line number & add it to generated output //! * figure out how to specify examples (& leading whitespace?!) +mod comment; mod commonmark; +mod format; +mod legacy; +#[cfg(test)] +mod test; +use crate::{format::handle_indentation, legacy::retrieve_legacy_comment}; + +use self::comment::get_expr_docs; use self::commonmark::*; +use format::shift_headings; +use legacy::collect_lambda_args; use rnix::{ ast::{AstToken, Attr, AttrpathValue, Comment, Expr, Inherit, Lambda, LetIn, Param}, SyntaxKind, SyntaxNode, }; use rowan::{ast::AstNode, WalkEvent}; use std::fs; -use textwrap::dedent; use std::collections::HashMap; use std::io; @@ -70,9 +79,11 @@ struct DocComment { doc: String, /// Optional type annotation for the thing being documented. + /// This is only available as legacy feature doc_type: Option, /// Usage example(s) (interpreted as a single code block) + /// This is only available as legacy feature example: Option, } @@ -102,127 +113,62 @@ impl DocItem { } } -/// Retrieve documentation comments. -fn retrieve_doc_comment(node: &SyntaxNode, allow_line_comments: bool) -> Option { - // if the current node has a doc comment it'll be immediately preceded by that comment, - // or there will be a whitespace token and *then* the comment tokens before it. We merge - // multiple line comments into one large comment if they are on adjacent lines for - // documentation simplicity. - let mut token = node.first_token()?.prev_token()?; - if token.kind() == SyntaxKind::TOKEN_WHITESPACE { - token = token.prev_token()?; - } - if token.kind() != SyntaxKind::TOKEN_COMMENT { - return None; - } - - // if we want to ignore line comments (eg because they may contain deprecation - // comments on attributes) we'll backtrack to the first preceding multiline comment. - while !allow_line_comments && token.text().starts_with('#') { - token = token.prev_token()?; - if token.kind() == SyntaxKind::TOKEN_WHITESPACE { - token = token.prev_token()?; - } - if token.kind() != SyntaxKind::TOKEN_COMMENT { - return None; - } - } - - if token.text().starts_with("/*") { - return Some(Comment::cast(token)?.text().to_string()); - } - - // backtrack to the start of the doc comment, allowing only adjacent line comments. - // we don't care much about optimization here, doc comments aren't long enough for that. - if token.text().starts_with('#') { - let mut result = String::new(); - while let Some(comment) = Comment::cast(token) { - if !comment.syntax().text().starts_with('#') { - break; - } - result.insert_str(0, comment.text().trim()); - let ws = match comment.syntax().prev_token() { - Some(t) if t.kind() == SyntaxKind::TOKEN_WHITESPACE => t, - _ => break, - }; - // only adjacent lines continue a doc comment, empty lines do not. - match ws.text().strip_prefix('\n') { - Some(trail) if !trail.contains('\n') => result.insert(0, ' '), - _ => break, - } - token = match ws.prev_token() { - Some(c) => c, - _ => break, - }; - } - return Some(result); - } +pub fn retrieve_doc_comment(node: &SyntaxNode, shift_headings_by: Option) -> Option { + // Return a rfc145 doc-comment if one is present + // Otherwise do the legacy parsing + // If there is a doc comment according to RFC145 just return it. + let doc_comment = match node.kind() { + // NODE_IDENT_PARAM: Special case, for backwards compatibility with function args + // In rfc145 this is equivalent with lookup of the lambda docs of the partial function. + // a: /** Doc comment */b: + // NODE_LAMBDA(b:) <- (parent of IDENT_PARAM) + // NODE_IDENT_PARAM(b) + SyntaxKind::NODE_IDENT_PARAM => get_expr_docs(&node.parent().unwrap()), + _ => get_expr_docs(node), + }; - None + doc_comment.map(|doc_comment| { + let doc = shift_headings( + &handle_indentation(&doc_comment).unwrap_or(String::new()), + // H1 to H4 can be used in the doc-comment with the current rendering. + // They will be shifted to H3, H6 + // H1 and H2 are currently used by the outer rendering. (category and function name) + shift_headings_by.unwrap_or(2), + ); + return doc; + }) } /// Transforms an AST node into a `DocItem` if it has a leading /// documentation comment. fn retrieve_doc_item(node: &AttrpathValue) -> Option { - let comment = retrieve_doc_comment(node.syntax(), false)?; let ident = node.attrpath().unwrap(); // TODO this should join attrs() with '.' to handle whitespace, dynamic attrs and string // attrs. none of these happen in nixpkgs lib, and the latter two should probably be // rejected entirely. let item_name = ident.to_string(); - Some(DocItem { - name: item_name, - comment: parse_doc_comment(&comment), - args: vec![], - }) -} - -/// Ensure all lines in a multi-line doc-comments have the same indentation. -/// -/// Consider such a doc comment: -/// -/// ```nix -/// { -/// /* foo is -/// the value: -/// 10 -/// */ -/// foo = 10; -/// } -/// ``` -/// -/// The parser turns this into: -/// -/// ``` -/// foo is -/// the value: -/// 10 -/// ``` -/// -/// -/// where the first line has no leading indentation, and all other lines have preserved their -/// original indentation. -/// -/// What we want instead is: -/// -/// ``` -/// foo is -/// the value: -/// 10 -/// ``` -/// -/// i.e. we want the whole thing to be dedented. To achieve this, we remove all leading whitespace -/// from the first line, and remove all common whitespace from the rest of the string. -fn handle_indentation(raw: &str) -> Option { - let result: String = match raw.split_once('\n') { - Some((first, rest)) => { - format!("{}\n{}", first.trim_start(), dedent(rest)) + let doc_comment = retrieve_doc_comment(node.syntax(), Some(2)); + match doc_comment { + Some(comment) => Some(DocItem { + name: item_name, + comment: DocComment { + doc: comment, + doc_type: None, + example: None, + }, + args: vec![], + }), + // Fallback to legacy comment is there is no doc_comment + None => { + let comment = retrieve_legacy_comment(node.syntax(), false)?; + Some(DocItem { + name: item_name, + comment: parse_doc_comment(&comment), + args: vec![], + }) } - None => raw.into(), - }; - - Some(result.trim().to_owned()).filter(|s| !s.is_empty()) + } } /// Dumb, mutable, hacky doc comment "parser". @@ -266,49 +212,6 @@ fn parse_doc_comment(raw: &str) -> DocComment { } } -/// Traverse a Nix lambda and collect the identifiers of arguments -/// until an unexpected AST node is encountered. -fn collect_lambda_args(mut lambda: Lambda) -> Vec { - let mut args = vec![]; - - loop { - match lambda.param().unwrap() { - // a variable, e.g. `id = x: x` - Param::IdentParam(id) => { - args.push(Argument::Flat(SingleArg { - name: id.to_string(), - doc: handle_indentation( - &retrieve_doc_comment(id.syntax(), true).unwrap_or_default(), - ), - })); - } - // an attribute set, e.g. `foo = { a }: a` - Param::Pattern(pat) => { - // collect doc-comments for each attribute in the set - let pattern_vec: Vec<_> = pat - .pat_entries() - .map(|entry| SingleArg { - name: entry.ident().unwrap().to_string(), - doc: handle_indentation( - &retrieve_doc_comment(entry.syntax(), true).unwrap_or_default(), - ), - }) - .collect(); - - args.push(Argument::Pattern(pattern_vec)); - } - } - - // Curried or not? - match lambda.body() { - Some(Expr::Lambda(inner)) => lambda = inner, - _ => break, - } - } - - args -} - /// Traverse the arena from a top-level SetEntry and collect, where /// possible: /// @@ -409,8 +312,9 @@ fn retrieve_description(nix: &rnix::Root, description: &str, category: &str) -> category, &nix.syntax() .first_child() - .and_then(|node| retrieve_doc_comment(&node, false)) - .and_then(|comment| handle_indentation(&comment)) + .and_then(|node| retrieve_doc_comment(&node, Some(1)) + .or(retrieve_legacy_comment(&node, false))) + .and_then(|doc_item| handle_indentation(&doc_item)) .unwrap_or_default() ) } @@ -438,129 +342,3 @@ fn main() { .expect("Failed to write section") } } - -#[test] -fn test_main() { - let mut output = Vec::new(); - let src = fs::read_to_string("test/strings.nix").unwrap(); - let locs = serde_json::from_str(&fs::read_to_string("test/strings.json").unwrap()).unwrap(); - let nix = rnix::Root::parse(&src).ok().expect("failed to parse input"); - let desc = "string manipulation functions"; - let prefix = "lib"; - let category = "strings"; - - // TODO: move this to commonmark.rs - writeln!( - output, - "# {} {{#sec-functions-library-{}}}\n", - desc, category - ) - .expect("Failed to write header"); - - for entry in collect_entries(nix, prefix, category) { - entry - .write_section(&locs, &mut output) - .expect("Failed to write section") - } - - let output = String::from_utf8(output).expect("not utf8"); - - insta::assert_snapshot!(output); -} - -#[test] -fn test_description_of_lib_debug() { - let mut output = Vec::new(); - let src = fs::read_to_string("test/lib-debug.nix").unwrap(); - let nix = rnix::Root::parse(&src).ok().expect("failed to parse input"); - let prefix = "lib"; - let category = "debug"; - let desc = retrieve_description(&nix, &"Debug", category); - writeln!(output, "{}", desc).expect("Failed to write header"); - - for entry in collect_entries(nix, prefix, category) { - entry - .write_section(&Default::default(), &mut output) - .expect("Failed to write section") - } - - let output = String::from_utf8(output).expect("not utf8"); - - insta::assert_snapshot!(output); -} - -#[test] -fn test_arg_formatting() { - let mut output = Vec::new(); - let src = fs::read_to_string("test/arg-formatting.nix").unwrap(); - let nix = rnix::Root::parse(&src).ok().expect("failed to parse input"); - let prefix = "lib"; - let category = "options"; - - for entry in collect_entries(nix, prefix, category) { - entry - .write_section(&Default::default(), &mut output) - .expect("Failed to write section") - } - - let output = String::from_utf8(output).expect("not utf8"); - - insta::assert_snapshot!(output); -} - -#[test] -fn test_inherited_exports() { - let mut output = Vec::new(); - let src = fs::read_to_string("test/inherited-exports.nix").unwrap(); - let nix = rnix::Root::parse(&src).ok().expect("failed to parse input"); - let prefix = "lib"; - let category = "let"; - - for entry in collect_entries(nix, prefix, category) { - entry - .write_section(&Default::default(), &mut output) - .expect("Failed to write section") - } - - let output = String::from_utf8(output).expect("not utf8"); - - insta::assert_snapshot!(output); -} - -#[test] -fn test_line_comments() { - let mut output = Vec::new(); - let src = fs::read_to_string("test/line-comments.nix").unwrap(); - let nix = rnix::Root::parse(&src).ok().expect("failed to parse input"); - let prefix = "lib"; - let category = "let"; - - for entry in collect_entries(nix, prefix, category) { - entry - .write_section(&Default::default(), &mut output) - .expect("Failed to write section") - } - - let output = String::from_utf8(output).expect("not utf8"); - - insta::assert_snapshot!(output); -} - -#[test] -fn test_multi_line() { - let mut output = Vec::new(); - let src = fs::read_to_string("test/multi-line.nix").unwrap(); - let nix = rnix::Root::parse(&src).ok().expect("failed to parse input"); - let prefix = "lib"; - let category = "let"; - - for entry in collect_entries(nix, prefix, category) { - entry - .write_section(&Default::default(), &mut output) - .expect("Failed to write section") - } - - let output = String::from_utf8(output).expect("not utf8"); - - insta::assert_snapshot!(output); -} diff --git a/src/snapshots/nixdoc__arg_formatting.snap b/src/snapshots/nixdoc__test__arg_formatting.snap similarity index 100% rename from src/snapshots/nixdoc__arg_formatting.snap rename to src/snapshots/nixdoc__test__arg_formatting.snap diff --git a/src/snapshots/nixdoc__description_of_lib_debug.snap b/src/snapshots/nixdoc__test__description_of_lib_debug.snap similarity index 100% rename from src/snapshots/nixdoc__description_of_lib_debug.snap rename to src/snapshots/nixdoc__test__description_of_lib_debug.snap diff --git a/src/snapshots/nixdoc__test__doc_comment.md b/src/snapshots/nixdoc__test__doc_comment.md new file mode 100644 index 0000000..fa3a4fd --- /dev/null +++ b/src/snapshots/nixdoc__test__doc_comment.md @@ -0,0 +1,58 @@ +--- +source: src/test.rs +expression: output +--- +## `lib.debug.nixdoc` {#function-library-lib.debug.nixdoc} + +**Type**: `This is a parsed type` + +nixdoc-legacy comment + +::: {.example #function-library-example-lib.debug.nixdoc} +# `lib.debug.nixdoc` usage example + +```nix +This is a parsed example +``` +::: + +## `lib.debug.rfc-style` {#function-library-lib.debug.rfc-style} + +doc comment in markdown format + + +## `lib.debug.argumentTest` {#function-library-lib.debug.argumentTest} + +doc comment in markdown format + +Example: + +This is just markdown + +Type: + +This is just markdown + + +structured function argument + +: `formal1` + + : Legacy line comment + + `formal2` + + : Legacy Block + + `formal3` + + : Legacy + multiline + comment + + `formal4` + + : official doc-comment variant + + + diff --git a/src/snapshots/nixdoc__test__doc_comment.snap b/src/snapshots/nixdoc__test__doc_comment.snap new file mode 100644 index 0000000..fa3a4fd --- /dev/null +++ b/src/snapshots/nixdoc__test__doc_comment.snap @@ -0,0 +1,58 @@ +--- +source: src/test.rs +expression: output +--- +## `lib.debug.nixdoc` {#function-library-lib.debug.nixdoc} + +**Type**: `This is a parsed type` + +nixdoc-legacy comment + +::: {.example #function-library-example-lib.debug.nixdoc} +# `lib.debug.nixdoc` usage example + +```nix +This is a parsed example +``` +::: + +## `lib.debug.rfc-style` {#function-library-lib.debug.rfc-style} + +doc comment in markdown format + + +## `lib.debug.argumentTest` {#function-library-lib.debug.argumentTest} + +doc comment in markdown format + +Example: + +This is just markdown + +Type: + +This is just markdown + + +structured function argument + +: `formal1` + + : Legacy line comment + + `formal2` + + : Legacy Block + + `formal3` + + : Legacy + multiline + comment + + `formal4` + + : official doc-comment variant + + + diff --git a/src/snapshots/nixdoc__test__doc_comment_section_description.snap b/src/snapshots/nixdoc__test__doc_comment_section_description.snap new file mode 100644 index 0000000..1450900 --- /dev/null +++ b/src/snapshots/nixdoc__test__doc_comment_section_description.snap @@ -0,0 +1,8 @@ +--- +source: src/test.rs +expression: output +--- +# Debug {#sec-functions-library-debug} +Markdown section heading + + diff --git a/src/snapshots/nixdoc__inherited_exports.snap b/src/snapshots/nixdoc__test__inherited_exports.snap similarity index 100% rename from src/snapshots/nixdoc__inherited_exports.snap rename to src/snapshots/nixdoc__test__inherited_exports.snap diff --git a/src/snapshots/nixdoc__line_comments.snap b/src/snapshots/nixdoc__test__line_comments.snap similarity index 100% rename from src/snapshots/nixdoc__line_comments.snap rename to src/snapshots/nixdoc__test__line_comments.snap diff --git a/src/snapshots/nixdoc__main.snap b/src/snapshots/nixdoc__test__main.snap similarity index 100% rename from src/snapshots/nixdoc__main.snap rename to src/snapshots/nixdoc__test__main.snap diff --git a/src/snapshots/nixdoc__multi_line.snap b/src/snapshots/nixdoc__test__multi_line.snap similarity index 100% rename from src/snapshots/nixdoc__multi_line.snap rename to src/snapshots/nixdoc__test__multi_line.snap diff --git a/src/test.rs b/src/test.rs new file mode 100644 index 0000000..6d1caea --- /dev/null +++ b/src/test.rs @@ -0,0 +1,172 @@ +use rnix; +use std::fs; + +use std::io::Write; + +use crate::{collect_entries, retrieve_description}; + +#[test] +fn test_main() { + let mut output = Vec::new(); + let src = fs::read_to_string("test/strings.nix").unwrap(); + let locs = serde_json::from_str(&fs::read_to_string("test/strings.json").unwrap()).unwrap(); + let nix = rnix::Root::parse(&src).ok().expect("failed to parse input"); + let desc = "string manipulation functions"; + let prefix = "lib"; + let category = "strings"; + + // TODO: move this to commonmark.rs + writeln!( + output, + "# {} {{#sec-functions-library-{}}}\n", + desc, category + ) + .expect("Failed to write header"); + + for entry in collect_entries(nix, prefix, category) { + entry + .write_section(&locs, &mut output) + .expect("Failed to write section") + } + + let output = String::from_utf8(output).expect("not utf8"); + + insta::assert_snapshot!(output); +} + +#[test] +fn test_description_of_lib_debug() { + let mut output = Vec::new(); + let src = fs::read_to_string("test/lib-debug.nix").unwrap(); + let nix = rnix::Root::parse(&src).ok().expect("failed to parse input"); + let prefix = "lib"; + let category = "debug"; + let desc = retrieve_description(&nix, &"Debug", category); + writeln!(output, "{}", desc).expect("Failed to write header"); + + for entry in collect_entries(nix, prefix, category) { + entry + .write_section(&Default::default(), &mut output) + .expect("Failed to write section") + } + + let output = String::from_utf8(output).expect("not utf8"); + + insta::assert_snapshot!(output); +} + +#[test] +fn test_arg_formatting() { + let mut output = Vec::new(); + let src = fs::read_to_string("test/arg-formatting.nix").unwrap(); + let nix = rnix::Root::parse(&src).ok().expect("failed to parse input"); + let prefix = "lib"; + let category = "options"; + + for entry in collect_entries(nix, prefix, category) { + entry + .write_section(&Default::default(), &mut output) + .expect("Failed to write section") + } + + let output = String::from_utf8(output).expect("not utf8"); + + insta::assert_snapshot!(output); +} + +#[test] +fn test_inherited_exports() { + let mut output = Vec::new(); + let src = fs::read_to_string("test/inherited-exports.nix").unwrap(); + let nix = rnix::Root::parse(&src).ok().expect("failed to parse input"); + let prefix = "lib"; + let category = "let"; + + for entry in collect_entries(nix, prefix, category) { + entry + .write_section(&Default::default(), &mut output) + .expect("Failed to write section") + } + + let output = String::from_utf8(output).expect("not utf8"); + + insta::assert_snapshot!(output); +} + +#[test] +fn test_line_comments() { + let mut output = Vec::new(); + let src = fs::read_to_string("test/line-comments.nix").unwrap(); + let nix = rnix::Root::parse(&src).ok().expect("failed to parse input"); + let prefix = "lib"; + let category = "let"; + + for entry in collect_entries(nix, prefix, category) { + entry + .write_section(&Default::default(), &mut output) + .expect("Failed to write section") + } + + let output = String::from_utf8(output).expect("not utf8"); + + insta::assert_snapshot!(output); +} + +#[test] +fn test_multi_line() { + let mut output = Vec::new(); + let src = fs::read_to_string("test/multi-line.nix").unwrap(); + let nix = rnix::Root::parse(&src).ok().expect("failed to parse input"); + let prefix = "lib"; + let category = "let"; + + for entry in collect_entries(nix, prefix, category) { + entry + .write_section(&Default::default(), &mut output) + .expect("Failed to write section") + } + + let output = String::from_utf8(output).expect("not utf8"); + + insta::assert_snapshot!(output); +} + +#[test] +fn test_doc_comment() { + let mut output = Vec::new(); + let src = fs::read_to_string("test/doc-comment.nix").unwrap(); + let nix = rnix::Root::parse(&src).ok().expect("failed to parse input"); + let prefix = "lib"; + let category = "debug"; + + for entry in collect_entries(nix, prefix, category) { + entry + .write_section(&Default::default(), &mut output) + .expect("Failed to write section") + } + + let output = String::from_utf8(output).expect("not utf8"); + + insta::assert_snapshot!(output); +} + +#[test] +fn test_doc_comment_section_description() { + let mut output = Vec::new(); + let src = fs::read_to_string("test/doc-comment-sec-heading.nix").unwrap(); + let nix = rnix::Root::parse(&src).ok().expect("failed to parse input"); + let prefix = "lib"; + let category = "debug"; + let desc = retrieve_description(&nix, &"Debug", category); + writeln!(output, "{}", desc).expect("Failed to write header"); + + for entry in collect_entries(nix, prefix, category) { + entry + .write_section(&Default::default(), &mut output) + .expect("Failed to write section") + } + + let output = String::from_utf8(output).expect("not utf8"); + + insta::assert_snapshot!(output); +} diff --git a/test.nix b/test.nix new file mode 100644 index 0000000..3a2310e --- /dev/null +++ b/test.nix @@ -0,0 +1,37 @@ +/** +Prequel +*/ +{lib}: +{ + + /** + Create a file set from a path that may or may not exist + */ + packagesFromDirectoryRecursive = + # Options. + { + /** + rfc style + + ``` + Path -> AttrSet -> a + ``` + */ + callPackage, + /* + legacy multiline + + ``` + Path + ``` + */ + directory, + # legacy single line + config, + # legacy + # block + # comment + moreConfig, + }: + 1; +} \ No newline at end of file diff --git a/test/doc-comment-sec-heading.nix b/test/doc-comment-sec-heading.nix new file mode 100644 index 0000000..c906d30 --- /dev/null +++ b/test/doc-comment-sec-heading.nix @@ -0,0 +1,4 @@ +/** + Markdown section heading +*/ +{}:{} diff --git a/test/doc-comment.nix b/test/doc-comment.nix new file mode 100644 index 0000000..b50ea8a --- /dev/null +++ b/test/doc-comment.nix @@ -0,0 +1,53 @@ +{ + # not a doc comment + hidden = a: a; + + /* + nixdoc-legacy comment + + Example: + + This is a parsed example + + Type: + + This is a parsed type + */ + nixdoc = {}; + + /** + doc comment in markdown format + */ + rfc-style = {}; + + /** + doc comment in markdown format + + Example: + + This is just markdown + + Type: + + This is just markdown + */ + argumentTest = { + # Legacy line comment + formal1, + # Legacy + # Block + formal2, + /* + Legacy + multiline + comment + */ + formal3, + /** + official doc-comment variant + */ + formal4, + + }: + {}; +}