diff --git a/CHANGELOG.md b/CHANGELOG.md index f19dc49..6a45168 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,34 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.1.3] - 2026-03-19 + +### Added + +- Section name matching now accepts additional singular and alias forms for both + Google and NumPy styles: + - `"arg"`, `"param"`, `"keyword arg"`, `"keyword param"`, `"other arg"`, + `"other param"`, `"method"`, `"reference"` (Google) + - `"arguments"`, `"argument"`, `"args"`, `"arg"`, `"other arguments"`, + `"other argument"`, `"other args"`, `"other arg"`, `"attribute"`, + `"method"`, `"reference"` (NumPy) + - Common typos tolerated: `"argment"`, `"paramter"` (Google) + +### Fixed + +- Google parser: arg entries with no description (e.g. `b :`) inside a section + body were incorrectly classified as new section headers. Fixed by comparing + the indentation of each line against the current section header's indentation + and skipping header detection for more-indented lines. + +### Changed + +- Refactored Google entry header parsing to use a left-to-right confirmation + algorithm. Handles missing close brackets, missing colons, and text after + brackets without a colon more robustly. `close_bracket` in `TypeInfo` is now + `Option` to represent the missing-bracket case. +- Added `rustfmt.toml` (`max_width = 120`) and reformatted all source files. + ## [0.1.2] - 2026-03-16 ### Added diff --git a/Cargo.lock b/Cargo.lock index 376fa45..fba3b52 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,4 +4,4 @@ version = 4 [[package]] name = "pydocstring" -version = "0.1.2" +version = "0.1.3" diff --git a/Cargo.toml b/Cargo.toml index a08644b..c00ebde 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pydocstring" -version = "0.1.2" +version = "0.1.3" edition = "2024" authors = ["Ryuma Asai"] description = "A zero-dependency Rust parser for Python docstrings (Google and NumPy styles) with a unified syntax tree and byte-precise source locations" diff --git a/README.md b/README.md index e2a0310..da94e11 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Python bindings are also available as [`pydocstring-rs`](https://pypi.org/projec ```toml [dependencies] -pydocstring = "0.1.2" +pydocstring = "0.1.3" ``` ## Usage diff --git a/bindings/python/Cargo.toml b/bindings/python/Cargo.toml index aa61ae6..6bc550f 100644 --- a/bindings/python/Cargo.toml +++ b/bindings/python/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pydocstring-python" -version = "0.1.2" +version = "0.1.3" edition = "2024" authors = ["Ryuma Asai"] description = "Python bindings for pydocstring — a fast docstring parser for Google and NumPy styles" @@ -12,5 +12,5 @@ name = "pydocstring" crate-type = ["cdylib"] [dependencies] -pydocstring_core = { package = "pydocstring", version = "0.1.2", path = "../.." } +pydocstring_core = { package = "pydocstring", version = "0.1.3", path = "../.." } pyo3 = { version = "0.24", features = ["extension-module"] } diff --git a/bindings/python/pyproject.toml b/bindings/python/pyproject.toml index 8184332..38662ba 100644 --- a/bindings/python/pyproject.toml +++ b/bindings/python/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "pydocstring-rs" -version = "0.1.2" +version = "0.1.3" description = "Python bindings for pydocstring — a zero-dependency Rust parser for Python docstrings (Google and NumPy styles) with a unified syntax tree and byte-precise source locations" license = {text = "MIT"} authors = [{name = "Ryuma Asai"}] diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..7530651 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +max_width = 120 diff --git a/src/cursor.rs b/src/cursor.rs index 544617d..d18d527 100644 --- a/src/cursor.rs +++ b/src/cursor.rs @@ -109,11 +109,7 @@ impl<'a> LineCursor<'a> { /// Equivalent to /// `make_line_range(line, current_indent(), current_trimmed().len())`. pub fn current_trimmed_range(&self) -> TextRange { - self.make_line_range( - self.line, - self.current_indent(), - self.current_trimmed().len(), - ) + self.make_line_range(self.line, self.current_indent(), self.current_trimmed().len()) } // ── Arbitrary-line helpers ────────────────────────────────────── @@ -140,13 +136,7 @@ impl<'a> LineCursor<'a> { // ── Span construction ────────────────────────────────────────── /// Build a [`TextRange`] from (line, col) pairs. - pub fn make_range( - &self, - start_line: usize, - start_col: usize, - end_line: usize, - end_col: usize, - ) -> TextRange { + pub fn make_range(&self, start_line: usize, start_col: usize, end_line: usize, end_col: usize) -> TextRange { TextRange::new( TextSize::new((self.offsets[start_line] + start_col) as u32), TextSize::new((self.offsets[end_line] + end_col) as u32), @@ -186,10 +176,7 @@ impl<'a> LineCursor<'a> { /// Convert a byte offset to `(line, col)`. pub fn offset_to_line_col(&self, offset: usize) -> (usize, usize) { - let line = self - .offsets - .partition_point(|&o| o <= offset) - .saturating_sub(1); + let line = self.offsets.partition_point(|&o| o <= offset).saturating_sub(1); let col = offset - self.offsets[line]; (line, col) } diff --git a/src/emit/google.rs b/src/emit/google.rs index a37dcb0..0aac7ae 100644 --- a/src/emit/google.rs +++ b/src/emit/google.rs @@ -1,8 +1,7 @@ //! Emit a [`Docstring`] as a Google-style docstring. use crate::model::{ - Attribute, Docstring, ExceptionEntry, FreeSectionKind, Method, Parameter, Reference, Return, - Section, SeeAlsoEntry, + Attribute, Docstring, ExceptionEntry, FreeSectionKind, Method, Parameter, Reference, Return, Section, SeeAlsoEntry, }; /// Emit a [`Docstring`] as a Google-style docstring string. diff --git a/src/emit/numpy.rs b/src/emit/numpy.rs index 9c4d322..abed3b9 100644 --- a/src/emit/numpy.rs +++ b/src/emit/numpy.rs @@ -1,8 +1,8 @@ //! Emit a [`Docstring`] as a NumPy-style docstring. use crate::model::{ - Attribute, Deprecation, Docstring, ExceptionEntry, FreeSectionKind, Method, Parameter, - Reference, Return, Section, SeeAlsoEntry, + Attribute, Deprecation, Docstring, ExceptionEntry, FreeSectionKind, Method, Parameter, Reference, Return, Section, + SeeAlsoEntry, }; /// Emit a [`Docstring`] as a NumPy-style docstring string. diff --git a/src/parse.rs b/src/parse.rs index 07a0890..3b37c12 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -75,11 +75,7 @@ fn has_numpy_sections(input: &str) -> bool { for i in 0..lines.len().saturating_sub(1) { let current = lines[i].trim(); let next = lines[i + 1].trim(); - if !current.is_empty() - && !next.is_empty() - && next.len() >= 3 - && next.chars().all(|c| c == '-') - { + if !current.is_empty() && !next.is_empty() && next.len() >= 3 && next.chars().all(|c| c == '-') { return true; } } diff --git a/src/parse/google.rs b/src/parse/google.rs index bcea17c..69e4f8e 100644 --- a/src/parse/google.rs +++ b/src/parse/google.rs @@ -9,7 +9,7 @@ pub mod to_model; pub use kind::GoogleSectionKind; pub use nodes::{ - GoogleArg, GoogleAttribute, GoogleDocstring, GoogleException, GoogleMethod, GoogleReturns, - GoogleSection, GoogleSectionHeader, GoogleSeeAlsoItem, GoogleWarning, + GoogleArg, GoogleAttribute, GoogleDocstring, GoogleException, GoogleMethod, GoogleReturns, GoogleSection, + GoogleSectionHeader, GoogleSeeAlsoItem, GoogleWarning, }; pub use parser::parse_google; diff --git a/src/parse/google/kind.rs b/src/parse/google/kind.rs index 971809c..a6b9b86 100644 --- a/src/parse/google/kind.rs +++ b/src/parse/google/kind.rs @@ -97,9 +97,12 @@ impl GoogleSectionKind { #[rustfmt::skip] pub fn from_name(name: &str) -> Self { match name { - "args" | "arguments" | "params" | "parameters" => Self::Args, - "keyword args" | "keyword arguments" | "keyword params" | "keyword parameters" => Self::KeywordArgs, - "other args" | "other arguments" | "other params" | "other parameters" => Self::OtherParameters, + "args" | "arg" | "arguments" | "argment" => Self::Args, + "params" | "param" | "parameters" | "paramter" => Self::Args, + "keyword args" | "keyword arg" | "keyword arguments" | "keyword argument" => Self::KeywordArgs, + "keyword params" | "keyword param" | "keyword parameters" | "keyword paramter" => Self::KeywordArgs, + "other args" | "other arg" | "other arguments" | "other argment" => Self::OtherParameters, + "other params" | "other param" | "other parameters" | "other paramter" => Self::OtherParameters, "receives" | "receive" => Self::Receives, "returns" | "return" => Self::Returns, "yields" | "yield" => Self::Yields, @@ -107,11 +110,11 @@ impl GoogleSectionKind { "warns" | "warn" => Self::Warns, "see also" => Self::SeeAlso, "attributes" | "attribute" => Self::Attributes, - "methods" => Self::Methods, + "methods" | "method" => Self::Methods, "notes" | "note" => Self::Notes, "examples" | "example" => Self::Examples, "todo" => Self::Todo, - "references" => Self::References, + "references" | "reference" => Self::References, "warnings" | "warning" => Self::Warnings, "attention" => Self::Attention, "caution" => Self::Caution, diff --git a/src/parse/google/nodes.rs b/src/parse/google/nodes.rs index 036057a..b53fc5f 100644 --- a/src/parse/google/nodes.rs +++ b/src/parse/google/nodes.rs @@ -50,9 +50,7 @@ impl<'a> GoogleDocstring<'a> { /// Iterate over all section nodes. pub fn sections(&self) -> impl Iterator> { - self.0 - .nodes(SyntaxKind::GOOGLE_SECTION) - .filter_map(GoogleSection::cast) + self.0.nodes(SyntaxKind::GOOGLE_SECTION).filter_map(GoogleSection::cast) } /// Iterate over stray line tokens. @@ -86,9 +84,7 @@ impl<'a> GoogleSection<'a> { /// Iterate over arg entry nodes in this section. pub fn args(&self) -> impl Iterator> { - self.0 - .nodes(SyntaxKind::GOOGLE_ARG) - .filter_map(GoogleArg::cast) + self.0.nodes(SyntaxKind::GOOGLE_ARG).filter_map(GoogleArg::cast) } /// Returns entry node in this section, if present. @@ -107,9 +103,7 @@ impl<'a> GoogleSection<'a> { /// Iterate over warning entry nodes. pub fn warnings(&self) -> impl Iterator> { - self.0 - .nodes(SyntaxKind::GOOGLE_WARNING) - .filter_map(GoogleWarning::cast) + self.0.nodes(SyntaxKind::GOOGLE_WARNING).filter_map(GoogleWarning::cast) } /// Iterate over see-also item nodes. @@ -128,9 +122,7 @@ impl<'a> GoogleSection<'a> { /// Iterate over method entry nodes. pub fn methods(&self) -> impl Iterator> { - self.0 - .nodes(SyntaxKind::GOOGLE_METHOD) - .filter_map(GoogleMethod::cast) + self.0.nodes(SyntaxKind::GOOGLE_METHOD).filter_map(GoogleMethod::cast) } /// Free-text body content, if this is a free-text section. diff --git a/src/parse/google/parser.rs b/src/parse/google/parser.rs index de2434d..4525a33 100644 --- a/src/parse/google/parser.rs +++ b/src/parse/google/parser.rs @@ -5,7 +5,9 @@ use crate::cursor::{LineCursor, indent_len}; use crate::parse::google::kind::GoogleSectionKind; -use crate::parse::utils::{find_entry_colon, try_parse_bracket_entry}; +use crate::parse::utils::{ + find_colon_ignoring_parens, find_entry_colon, find_entry_open_bracket, find_matching_close, strip_optional, +}; use crate::syntax::{Parsed, SyntaxElement, SyntaxKind, SyntaxNode, SyntaxToken}; use crate::text::TextRange; @@ -33,7 +35,7 @@ fn extract_section_name(trimmed: &str) -> (&str, bool) { struct TypeInfo { open_bracket: TextRange, r#type: Option, - close_bracket: TextRange, + close_bracket: Option, optional: Option, } @@ -47,30 +49,88 @@ struct EntryHeader { } /// Parse a Google-style entry header at `cursor.line`. +/// +/// Uses a left-to-right confirmation algorithm: +/// 1. Find opening bracket → NAME is everything before it +/// 2. Find matching close bracket → TYPE is inside brackets +/// 3. Check character after bracket/whitespace for `:` → COLON, rest is DESC +/// 4. Otherwise remaining text is DESC (missing COLON) or nothing fn parse_entry_header(cursor: &LineCursor, parse_type: bool) -> EntryHeader { let line = cursor.current_line_text(); let trimmed = line.trim(); let entry_start = cursor.substr_offset(trimmed); - // --- Pattern 1: `name (type): desc` or `name(type): desc` --- + // --- Bracket entry: `name (type): desc` and variants --- if parse_type { - if let Some(entry) = try_parse_bracket_entry(trimmed) { - let name_span = TextRange::from_offset_len(entry_start, entry.name.len()); - let open_bracket = TextRange::from_offset_len(entry_start + entry.open_bracket, 1); - let close_bracket = TextRange::from_offset_len(entry_start + entry.close_bracket, 1); - - let type_span = if !entry.clean_type.is_empty() { - Some(TextRange::from_offset_len( - entry_start + entry.type_offset, - entry.clean_type.len(), - )) + if let Some(bracket_pos) = find_entry_open_bracket(trimmed) { + let name = trimmed[..bracket_pos].trim_end(); + let name_span = TextRange::from_offset_len(entry_start, name.len()); + let open_bracket = TextRange::from_offset_len(entry_start + bracket_pos, 1); + + let close_pos = find_matching_close(trimmed, bracket_pos); + let (type_text, close_bracket, colon, first_description) = match close_pos { + Some(cp) => { + // Bracket matched — check what follows. + let cb = Some(TextRange::from_offset_len(entry_start + cp, 1)); + let after_close = &trimmed[cp + 1..]; + let after_trimmed = after_close.trim_start(); + if after_trimmed.starts_with(':') { + // `:` confirmed → COLON + DESC + let colon_abs = cp + 1 + (after_close.len() - after_trimmed.len()); + let colon_span = Some(TextRange::from_offset_len(entry_start + colon_abs, 1)); + let after_colon = &trimmed[colon_abs + 1..]; + let desc = after_colon.trim(); + let desc_span = if desc.is_empty() { + None + } else { + let ws = after_colon.len() - after_colon.trim_start().len(); + Some(TextRange::from_offset_len(entry_start + colon_abs + 1 + ws, desc.len())) + }; + (&trimmed[bracket_pos + 1..cp], cb, colon_span, desc_span) + } else if !after_trimmed.is_empty() { + // Text without colon → DESC (missing COLON) + let ws = after_close.len() - after_trimmed.len(); + let desc_span = Some(TextRange::from_offset_len( + entry_start + cp + 1 + ws, + after_trimmed.len(), + )); + (&trimmed[bracket_pos + 1..cp], cb, None, desc_span) + } else { + // Nothing after close bracket + (&trimmed[bracket_pos + 1..cp], cb, None, None) + } + } + None => { + // No matching close bracket — look for colon ignoring paren depth. + if let Some(colon_abs) = find_colon_ignoring_parens(trimmed, bracket_pos + 1) { + let type_raw = &trimmed[bracket_pos + 1..colon_abs]; + let colon_span = Some(TextRange::from_offset_len(entry_start + colon_abs, 1)); + let after_colon = &trimmed[colon_abs + 1..]; + let desc = after_colon.trim(); + let desc_span = if desc.is_empty() { + None + } else { + let ws = after_colon.len() - after_colon.trim_start().len(); + Some(TextRange::from_offset_len(entry_start + colon_abs + 1 + ws, desc.len())) + }; + (type_raw, None, colon_span, desc_span) + } else { + (&trimmed[bracket_pos + 1..], None, None, None) + } + } + }; + + let type_trimmed = type_text.trim(); + let leading_ws = type_text.len() - type_text.trim_start().len(); + let type_offset = bracket_pos + 1 + leading_ws; + let (clean_type, opt_rel) = strip_optional(type_trimmed); + + let type_span = if !clean_type.is_empty() { + Some(TextRange::from_offset_len(entry_start + type_offset, clean_type.len())) } else { None }; - - let opt_span = entry - .optional_offset - .map(|r| TextRange::from_offset_len(entry_start + r, "optional".len())); + let opt_span = opt_rel.map(|r| TextRange::from_offset_len(entry_start + type_offset + r, "optional".len())); let type_info = Some(TypeInfo { open_bracket, @@ -79,20 +139,12 @@ fn parse_entry_header(cursor: &LineCursor, parse_type: bool) -> EntryHeader { optional: opt_span, }); - let colon = entry - .colon - .map(|c| TextRange::from_offset_len(entry_start + c, 1)); - let first_description = entry.description_offset.map(|d| { - TextRange::from_offset_len(entry_start + d, entry.description.unwrap().len()) - }); - - let range_end = if let Some(ref desc) = first_description { - desc.end() - } else if let Some(ref c) = colon { - c.end() - } else { - close_bracket.end() - }; + let range_end = first_description + .as_ref() + .map(|d| d.end()) + .or_else(|| colon.as_ref().map(|c| c.end())) + .or_else(|| close_bracket.map(|cb| cb.end())) + .unwrap_or_else(|| TextRange::from_offset_len(entry_start, trimmed.len()).end()); return EntryHeader { range: TextRange::new(name_span.start(), range_end), @@ -104,7 +156,7 @@ fn parse_entry_header(cursor: &LineCursor, parse_type: bool) -> EntryHeader { } } - // --- Pattern 2: `name: desc` --- + // --- `name: desc` --- if let Some(colon_rel) = find_entry_colon(trimmed) { let name = trimmed[..colon_rel].trim_end(); let after_colon = &trimmed[colon_rel + 1..]; @@ -153,6 +205,7 @@ struct SectionHeaderInfo { kind: GoogleSectionKind, name: TextRange, colon: Option, + indent_columns: usize, } fn try_parse_section_header(cursor: &LineCursor) -> Option { @@ -164,10 +217,7 @@ fn try_parse_section_header(cursor: &LineCursor) -> Option { } let is_header = if has_colon { - !name.contains(':') - && name - .chars() - .all(|c| c.is_alphanumeric() || c.is_ascii_whitespace()) + !name.contains(':') && name.chars().all(|c| c.is_alphanumeric() || c.is_ascii_whitespace()) } else { GoogleSectionKind::is_known(&name.to_ascii_lowercase()) }; @@ -194,6 +244,7 @@ fn try_parse_section_header(cursor: &LineCursor) -> Option { kind, name: cursor.make_line_range(cursor.line, col, header_name.len()), colon, + indent_columns: cursor.current_indent_columns(), }) } @@ -203,14 +254,15 @@ fn try_parse_section_header(cursor: &LineCursor) -> Option { fn build_section_header_node(info: &SectionHeaderInfo) -> SyntaxNode { let mut children = Vec::new(); - children.push(SyntaxElement::Token(SyntaxToken::new( - SyntaxKind::NAME, - info.name, - ))); + children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::NAME, info.name))); if let Some(colon) = info.colon { + children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::COLON, colon))); + } else { + // Colon is grammatically required; emit a zero-length COLON token + // at the position where it should appear (right after the name). children.push(SyntaxElement::Token(SyntaxToken::new( SyntaxKind::COLON, - colon, + TextRange::new(info.name.end(), info.name.end()), ))); } SyntaxNode::new(SyntaxKind::GOOGLE_SECTION_HEADER, info.range, children) @@ -219,10 +271,7 @@ fn build_section_header_node(info: &SectionHeaderInfo) -> SyntaxNode { /// Build a SyntaxNode for an arg-like entry (GoogleArg, GoogleAttribute, GoogleMethod). fn build_arg_node(kind: SyntaxKind, header: &EntryHeader, range: TextRange) -> SyntaxNode { let mut children = Vec::new(); - children.push(SyntaxElement::Token(SyntaxToken::new( - SyntaxKind::NAME, - header.name, - ))); + children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::NAME, header.name))); if let Some(ti) = &header.type_info { children.push(SyntaxElement::Token(SyntaxToken::new( SyntaxKind::OPEN_BRACKET, @@ -231,50 +280,53 @@ fn build_arg_node(kind: SyntaxKind, header: &EntryHeader, range: TextRange) -> S if let Some(t) = ti.r#type { children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::TYPE, t))); } - children.push(SyntaxElement::Token(SyntaxToken::new( - SyntaxKind::CLOSE_BRACKET, - ti.close_bracket, - ))); - if let Some(opt) = ti.optional { + if let Some(cb) = ti.close_bracket { + children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::CLOSE_BRACKET, cb))); + } else { + // Close bracket expected but missing. + let missing_pos = ti.r#type.map(|t| t.end()).unwrap_or(ti.open_bracket.end()); children.push(SyntaxElement::Token(SyntaxToken::new( - SyntaxKind::OPTIONAL, - opt, + SyntaxKind::CLOSE_BRACKET, + TextRange::new(missing_pos, missing_pos), ))); } + if let Some(opt) = ti.optional { + children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::OPTIONAL, opt))); + } } if let Some(colon) = header.colon { + children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::COLON, colon))); + } else if header.type_info.is_some() && header.first_description.is_some() { + // Bracket-style entry with text after it but no colon. + let missing_pos = header + .type_info + .as_ref() + .and_then(|ti| ti.close_bracket.map(|cb| cb.end())) + .or_else(|| header.type_info.as_ref().and_then(|ti| ti.r#type.map(|t| t.end()))) + .unwrap_or(header.name.end()); children.push(SyntaxElement::Token(SyntaxToken::new( SyntaxKind::COLON, - colon, + TextRange::new(missing_pos, missing_pos), ))); } if let Some(desc) = header.first_description { - children.push(SyntaxElement::Token(SyntaxToken::new( - SyntaxKind::DESCRIPTION, - desc, - ))); + children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::DESCRIPTION, desc))); } + // Ensure children are in source order (needed when colon/description + // appear before the close bracket, e.g., `arg (int:desc.)`). + children.sort_by_key(|c| c.range().start()); SyntaxNode::new(kind, range, children) } /// Build a SyntaxNode for an exception entry. fn build_exception_node(header: &EntryHeader, range: TextRange) -> SyntaxNode { let mut children = Vec::new(); - children.push(SyntaxElement::Token(SyntaxToken::new( - SyntaxKind::TYPE, - header.name, - ))); + children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::TYPE, header.name))); if let Some(colon) = header.colon { - children.push(SyntaxElement::Token(SyntaxToken::new( - SyntaxKind::COLON, - colon, - ))); + children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::COLON, colon))); } if let Some(desc) = header.first_description { - children.push(SyntaxElement::Token(SyntaxToken::new( - SyntaxKind::DESCRIPTION, - desc, - ))); + children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::DESCRIPTION, desc))); } SyntaxNode::new(SyntaxKind::GOOGLE_EXCEPTION, range, children) } @@ -287,16 +339,10 @@ fn build_warning_node(header: &EntryHeader, range: TextRange) -> SyntaxNode { header.name, ))); if let Some(colon) = header.colon { - children.push(SyntaxElement::Token(SyntaxToken::new( - SyntaxKind::COLON, - colon, - ))); + children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::COLON, colon))); } if let Some(desc) = header.first_description { - children.push(SyntaxElement::Token(SyntaxToken::new( - SyntaxKind::DESCRIPTION, - desc, - ))); + children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::DESCRIPTION, desc))); } SyntaxNode::new(SyntaxKind::GOOGLE_WARNING, range, children) } @@ -320,16 +366,10 @@ fn build_see_also_node(header: &EntryHeader, range: TextRange, source: &str) -> offset += part.len() + 1; } if let Some(colon) = header.colon { - children.push(SyntaxElement::Token(SyntaxToken::new( - SyntaxKind::COLON, - colon, - ))); + children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::COLON, colon))); } if let Some(desc) = header.first_description { - children.push(SyntaxElement::Token(SyntaxToken::new( - SyntaxKind::DESCRIPTION, - desc, - ))); + children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::DESCRIPTION, desc))); } SyntaxNode::new(SyntaxKind::GOOGLE_SEE_ALSO_ITEM, range, children) } @@ -350,11 +390,7 @@ fn parse_entry(cursor: &LineCursor, parse_type: bool) -> (EntryHeader, TextRange (header, entry_range) } -fn build_content_range( - cursor: &LineCursor, - first: Option, - last: usize, -) -> Option { +fn build_content_range(cursor: &LineCursor, first: Option, last: usize) -> Option { first.map(|f| { let first_line = cursor.line_text(f); let first_col = indent_len(first_line); @@ -383,10 +419,7 @@ fn extend_last_node_description(nodes: &mut [SyntaxElement], cont: TextRange) { } } if !found_desc { - node.push_child(SyntaxElement::Token(SyntaxToken::new( - SyntaxKind::DESCRIPTION, - cont, - ))); + node.push_child(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::DESCRIPTION, cont))); } // Extend node range node.extend_range_to(cont.end()); @@ -410,18 +443,10 @@ fn process_arg_line( *entry_indent = Some(indent_cols); } let (header, entry_range) = parse_entry(cursor, node_kind != SyntaxKind::GOOGLE_METHOD); - nodes.push(SyntaxElement::Node(build_arg_node( - node_kind, - &header, - entry_range, - ))); + nodes.push(SyntaxElement::Node(build_arg_node(node_kind, &header, entry_range))); } -fn process_exception_line( - cursor: &LineCursor, - nodes: &mut Vec, - entry_indent: &mut Option, -) { +fn process_exception_line(cursor: &LineCursor, nodes: &mut Vec, entry_indent: &mut Option) { let indent_cols = cursor.current_indent_columns(); if let Some(base) = *entry_indent { if indent_cols > base { @@ -433,17 +458,10 @@ fn process_exception_line( *entry_indent = Some(indent_cols); } let (header, entry_range) = parse_entry(cursor, false); - nodes.push(SyntaxElement::Node(build_exception_node( - &header, - entry_range, - ))); + nodes.push(SyntaxElement::Node(build_exception_node(&header, entry_range))); } -fn process_warning_line( - cursor: &LineCursor, - nodes: &mut Vec, - entry_indent: &mut Option, -) { +fn process_warning_line(cursor: &LineCursor, nodes: &mut Vec, entry_indent: &mut Option) { let indent_cols = cursor.current_indent_columns(); if let Some(base) = *entry_indent { if indent_cols > base { @@ -455,17 +473,10 @@ fn process_warning_line( *entry_indent = Some(indent_cols); } let (header, entry_range) = parse_entry(cursor, false); - nodes.push(SyntaxElement::Node(build_warning_node( - &header, - entry_range, - ))); + nodes.push(SyntaxElement::Node(build_warning_node(&header, entry_range))); } -fn process_see_also_line( - cursor: &LineCursor, - nodes: &mut Vec, - entry_indent: &mut Option, -) { +fn process_see_also_line(cursor: &LineCursor, nodes: &mut Vec, entry_indent: &mut Option) { let indent_cols = cursor.current_indent_columns(); if let Some(base) = *entry_indent { if indent_cols > base { @@ -539,22 +550,13 @@ impl ReturnsState { let range = self.range?; let mut children = Vec::new(); if let Some(rt) = self.return_type { - children.push(SyntaxElement::Token(SyntaxToken::new( - SyntaxKind::RETURN_TYPE, - rt, - ))); + children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::RETURN_TYPE, rt))); } if let Some(colon) = self.colon { - children.push(SyntaxElement::Token(SyntaxToken::new( - SyntaxKind::COLON, - colon, - ))); + children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::COLON, colon))); } if let Some(desc) = self.description { - children.push(SyntaxElement::Token(SyntaxToken::new( - SyntaxKind::DESCRIPTION, - desc, - ))); + children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::DESCRIPTION, desc))); } Some(SyntaxNode::new(kind, range, children)) } @@ -628,10 +630,7 @@ impl SectionBody { Self::Warns(nodes) => nodes, Self::SeeAlso(nodes) => nodes, Self::FreeText(range) => match range { - Some(r) => vec![SyntaxElement::Token(SyntaxToken::new( - SyntaxKind::BODY_TEXT, - r, - ))], + Some(r) => vec![SyntaxElement::Token(SyntaxToken::new(SyntaxKind::BODY_TEXT, r))], None => vec![], }, } @@ -669,11 +668,7 @@ pub fn parse_google(input: &str) -> Parsed { line_cursor.skip_blanks(); if line_cursor.is_eof() { - let root = SyntaxNode::new( - SyntaxKind::GOOGLE_DOCSTRING, - line_cursor.full_range(), - root_children, - ); + let root = SyntaxNode::new(SyntaxKind::GOOGLE_DOCSTRING, line_cursor.full_range(), root_children); return Parsed::new(input.to_string(), root); } @@ -703,7 +698,13 @@ pub fn parse_google(input: &str) -> Parsed { } // --- Detect section header --- - if let Some(header_info) = try_parse_section_header(&line_cursor) { + // Lines that are strictly more indented than the current section header + // are body entries (e.g., `b :` inside an Args block) and must never + // be mistaken for a new section header. + let may_be_header = current_header + .as_ref() + .is_none_or(|h| line_cursor.current_indent_columns() <= h.indent_columns); + if may_be_header && let Some(header_info) = try_parse_section_header(&line_cursor) { // Finalise pending pre-section content if !summary_done { if summary_first.is_some() { @@ -767,12 +768,7 @@ pub fn parse_google(input: &str) -> Parsed { // Flush final section if let Some(header) = current_header.take() { - flush_section( - &line_cursor, - &mut root_children, - header, - current_body.take().unwrap(), - ); + flush_section(&line_cursor, &mut root_children, header, current_body.take().unwrap()); } // Finalise at EOF @@ -789,11 +785,7 @@ pub fn parse_google(input: &str) -> Parsed { ))); } - let root = SyntaxNode::new( - SyntaxKind::GOOGLE_DOCSTRING, - line_cursor.full_range(), - root_children, - ); + let root = SyntaxNode::new(SyntaxKind::GOOGLE_DOCSTRING, line_cursor.full_range(), root_children); Parsed::new(input.to_string(), root) } @@ -871,10 +863,7 @@ mod tests { assert!(header.type_info.is_some()); let ti = header.type_info.unwrap(); assert_eq!(ti.r#type.unwrap().source_text(src), "int"); - assert_eq!( - header.first_description.unwrap().source_text(src), - "Description" - ); + assert_eq!(header.first_description.unwrap().source_text(src), "Description"); } #[test] @@ -894,10 +883,7 @@ mod tests { let header = header_from(src); assert_eq!(header.name.source_text(src), "name"); assert!(header.type_info.is_none()); - assert_eq!( - header.first_description.unwrap().source_text(src), - "Description" - ); + assert_eq!(header.first_description.unwrap().source_text(src), "Description"); } #[test] @@ -942,10 +928,7 @@ mod tests { let header = header_from(src); assert_eq!(header.name.source_text(src), "name"); assert!(header.type_info.is_none()); - assert_eq!( - header.first_description.unwrap().source_text(src), - "Description" - ); + assert_eq!(header.first_description.unwrap().source_text(src), "Description"); } #[test] @@ -954,10 +937,7 @@ mod tests { let header = header_from(src); assert_eq!(header.name.source_text(src), "name"); assert!(header.type_info.is_none()); - assert_eq!( - header.first_description.unwrap().source_text(src), - "Description" - ); + assert_eq!(header.first_description.unwrap().source_text(src), "Description"); } #[test] @@ -967,10 +947,7 @@ mod tests { // Strict mode: brackets without space are NOT treated as type assert_eq!(header.name.source_text(src), "name(int)"); assert!(header.type_info.is_none()); - assert_eq!( - header.first_description.unwrap().source_text(src), - "Description" - ); + assert_eq!(header.first_description.unwrap().source_text(src), "Description"); } #[test] @@ -982,10 +959,7 @@ mod tests { assert!(header.type_info.is_some()); let ti = header.type_info.unwrap(); assert_eq!(ti.r#type.unwrap().source_text(src), "int"); - assert_eq!( - header.first_description.unwrap().source_text(src), - "Description" - ); + assert_eq!(header.first_description.unwrap().source_text(src), "Description"); } #[test] diff --git a/src/parse/google/to_model.rs b/src/parse/google/to_model.rs index 8611189..a43c49d 100644 --- a/src/parse/google/to_model.rs +++ b/src/parse/google/to_model.rs @@ -1,8 +1,7 @@ //! Convert a Google-style AST into the style-independent [`Docstring`] model. use crate::model::{ - Attribute, Docstring, ExceptionEntry, FreeSectionKind, Method, Parameter, Return, Section, - SeeAlsoEntry, + Attribute, Docstring, ExceptionEntry, FreeSectionKind, Method, Parameter, Return, Section, SeeAlsoEntry, }; use crate::parse::google::kind::GoogleSectionKind; use crate::parse::google::nodes::{GoogleDocstring, GoogleSection}; @@ -18,10 +17,7 @@ pub fn to_model(parsed: &Parsed) -> Option { let summary = root.summary().map(|t| t.text(source).to_owned()); let extended_summary = root.extended_summary().map(|t| t.text(source).to_owned()); - let sections = root - .sections() - .map(|s| convert_section(&s, source)) - .collect(); + let sections = root.sections().map(|s| convert_section(&s, source)).collect(); Some(Docstring { summary, @@ -65,12 +61,9 @@ fn convert_section(section: &GoogleSection<'_>, source: &str) -> Section { _ => unreachable!(), } } - GoogleSectionKind::Raises => Section::Raises( - section - .exceptions() - .map(|e| convert_exception(&e, source)) - .collect(), - ), + GoogleSectionKind::Raises => { + Section::Raises(section.exceptions().map(|e| convert_exception(&e, source)).collect()) + } GoogleSectionKind::Warns => Section::Warns( section .warnings() @@ -128,15 +121,10 @@ fn convert_section(section: &GoogleSection<'_>, source: &str) -> Section { GoogleSectionKind::Hint => FreeSectionKind::Hint, GoogleSectionKind::Important => FreeSectionKind::Important, GoogleSectionKind::Tip => FreeSectionKind::Tip, - GoogleSectionKind::Unknown => { - FreeSectionKind::Unknown(section.header().name().text(source).to_owned()) - } + GoogleSectionKind::Unknown => FreeSectionKind::Unknown(section.header().name().text(source).to_owned()), _ => unreachable!(), }; - Section::FreeText { - kind: free_kind, - body, - } + Section::FreeText { kind: free_kind, body } } } } @@ -151,10 +139,7 @@ fn convert_arg(arg: &crate::parse::google::nodes::GoogleArg<'_>, source: &str) - } } -fn convert_exception( - exc: &crate::parse::google::nodes::GoogleException<'_>, - source: &str, -) -> ExceptionEntry { +fn convert_exception(exc: &crate::parse::google::nodes::GoogleException<'_>, source: &str) -> ExceptionEntry { ExceptionEntry { type_name: exc.r#type().text(source).to_owned(), description: exc.description().map(|t| t.text(source).to_owned()), diff --git a/src/parse/numpy.rs b/src/parse/numpy.rs index fff35e3..c619895 100644 --- a/src/parse/numpy.rs +++ b/src/parse/numpy.rs @@ -9,7 +9,7 @@ pub mod to_model; pub use kind::NumPySectionKind; pub use nodes::{ - NumPyAttribute, NumPyDeprecation, NumPyDocstring, NumPyException, NumPyMethod, NumPyParameter, - NumPyReference, NumPyReturns, NumPySection, NumPySectionHeader, NumPySeeAlsoItem, NumPyWarning, + NumPyAttribute, NumPyDeprecation, NumPyDocstring, NumPyException, NumPyMethod, NumPyParameter, NumPyReference, + NumPyReturns, NumPySection, NumPySectionHeader, NumPySeeAlsoItem, NumPyWarning, }; pub use parser::parse_numpy; diff --git a/src/parse/numpy/kind.rs b/src/parse/numpy/kind.rs index 1e7dc92..967c698 100644 --- a/src/parse/numpy/kind.rs +++ b/src/parse/numpy/kind.rs @@ -64,19 +64,21 @@ impl NumPySectionKind { pub fn from_name(name: &str) -> Self { match name { "parameters" | "parameter" | "params" | "param" => Self::Parameters, + "arguments" | "argument" | "args" | "arg" => Self::Parameters, "returns" | "return" => Self::Returns, "yields" | "yield" => Self::Yields, "receives" | "receive" => Self::Receives, "other parameters" | "other parameter" | "other params" | "other param" => Self::OtherParameters, + "other arguments" | "other argument" | "other args" | "other arg" => Self::OtherParameters, "raises" | "raise" => Self::Raises, "warns" | "warn" => Self::Warns, "warnings" | "warning" => Self::Warnings, "see also" => Self::SeeAlso, "notes" | "note" => Self::Notes, - "references" => Self::References, + "references" | "reference" => Self::References, "examples" | "example" => Self::Examples, - "attributes" => Self::Attributes, - "methods" => Self::Methods, + "attributes" | "attribute" => Self::Attributes, + "methods" | "method" => Self::Methods, _ => Self::Unknown, } } @@ -106,10 +108,7 @@ impl NumPySectionKind { /// Whether this section kind has free-text body. pub fn is_freetext(&self) -> bool { - matches!( - self, - Self::Notes | Self::Examples | Self::Warnings | Self::Unknown - ) + matches!(self, Self::Notes | Self::Examples | Self::Warnings | Self::Unknown) } } diff --git a/src/parse/numpy/nodes.rs b/src/parse/numpy/nodes.rs index ec57194..228d474 100644 --- a/src/parse/numpy/nodes.rs +++ b/src/parse/numpy/nodes.rs @@ -56,9 +56,7 @@ impl<'a> NumPyDocstring<'a> { /// Iterate over all section nodes. pub fn sections(&self) -> impl Iterator> { - self.0 - .nodes(SyntaxKind::NUMPY_SECTION) - .filter_map(NumPySection::cast) + self.0.nodes(SyntaxKind::NUMPY_SECTION).filter_map(NumPySection::cast) } /// Iterate over stray line tokens. @@ -99,9 +97,7 @@ impl<'a> NumPySection<'a> { /// Iterate over returns entry nodes. pub fn returns(&self) -> impl Iterator> { - self.0 - .nodes(SyntaxKind::NUMPY_RETURNS) - .filter_map(NumPyReturns::cast) + self.0.nodes(SyntaxKind::NUMPY_RETURNS).filter_map(NumPyReturns::cast) } /// Iterate over exception entry nodes. @@ -113,9 +109,7 @@ impl<'a> NumPySection<'a> { /// Iterate over warning entry nodes. pub fn warnings(&self) -> impl Iterator> { - self.0 - .nodes(SyntaxKind::NUMPY_WARNING) - .filter_map(NumPyWarning::cast) + self.0.nodes(SyntaxKind::NUMPY_WARNING).filter_map(NumPyWarning::cast) } /// Iterate over see-also item nodes. @@ -141,9 +135,7 @@ impl<'a> NumPySection<'a> { /// Iterate over method entry nodes. pub fn methods(&self) -> impl Iterator> { - self.0 - .nodes(SyntaxKind::NUMPY_METHOD) - .filter_map(NumPyMethod::cast) + self.0.nodes(SyntaxKind::NUMPY_METHOD).filter_map(NumPyMethod::cast) } /// Free-text body content, if this is a free-text section. diff --git a/src/parse/numpy/parser.rs b/src/parse/numpy/parser.rs index 3c75917..f562d03 100644 --- a/src/parse/numpy/parser.rs +++ b/src/parse/numpy/parser.rs @@ -5,9 +5,7 @@ use crate::cursor::{LineCursor, indent_columns, indent_len}; use crate::parse::numpy::kind::NumPySectionKind; -use crate::parse::utils::{ - find_entry_colon, find_matching_close, split_comma_parts, try_parse_bracket_entry, -}; +use crate::parse::utils::{find_entry_colon, find_matching_close, split_comma_parts, try_parse_bracket_entry}; use crate::syntax::{Parsed, SyntaxElement, SyntaxKind, SyntaxNode, SyntaxToken}; use crate::text::TextRange; @@ -109,12 +107,7 @@ struct ParamHeaderParts { first_description: Option, } -fn parse_name_and_type( - text: &str, - line_idx: usize, - col_base: usize, - cursor: &LineCursor, -) -> ParamHeaderParts { +fn parse_name_and_type(text: &str, line_idx: usize, col_base: usize, cursor: &LineCursor) -> ParamHeaderParts { // --- Google-style bracket pattern: `name (type): desc` --- if let Some(result) = try_parse_google_style_entry(text, line_idx, col_base, cursor) { return result; @@ -171,8 +164,7 @@ fn parse_name_and_type( } if seg == "optional" { - let seg_abs = - type_abs_start + seg_offset + (seg_raw.len() - seg_raw.trim_start().len()); + let seg_abs = type_abs_start + seg_offset + (seg_raw.len() - seg_raw.trim_start().len()); optional = Some(TextRange::from_offset_len(seg_abs, "optional".len())); } else if let Some(stripped) = seg.strip_prefix("default") { let ws_lead = seg_raw.len() - seg_raw.trim_start().len(); @@ -245,11 +237,7 @@ fn try_parse_google_style_entry( let names = parse_name_list(entry.name, line_idx, col_base, cursor); let param_type = if !entry.clean_type.is_empty() { - Some(cursor.make_line_range( - line_idx, - col_base + entry.type_offset, - entry.clean_type.len(), - )) + Some(cursor.make_line_range(line_idx, col_base + entry.type_offset, entry.clean_type.len())) } else { None }; @@ -258,9 +246,7 @@ fn try_parse_google_style_entry( .optional_offset .map(|r| cursor.make_line_range(line_idx, col_base + r, "optional".len())); - let colon = entry - .colon - .map(|c| cursor.make_line_range(line_idx, col_base + c, 1)); + let colon = entry.colon.map(|c| cursor.make_line_range(line_idx, col_base + c, 1)); let first_description = entry .description_offset @@ -278,12 +264,7 @@ fn try_parse_google_style_entry( }) } -fn parse_name_list( - text: &str, - line_idx: usize, - col_base: usize, - cursor: &LineCursor, -) -> Vec { +fn parse_name_list(text: &str, line_idx: usize, col_base: usize, cursor: &LineCursor) -> Vec { let mut names = Vec::new(); let mut byte_pos = 0usize; @@ -315,31 +296,19 @@ fn build_section_header_node(info: &SectionHeaderInfo) -> SyntaxNode { fn build_parameter_node(parts: &ParamHeaderParts, range: TextRange) -> SyntaxNode { let mut children = Vec::new(); for name in &parts.names { - children.push(SyntaxElement::Token(SyntaxToken::new( - SyntaxKind::NAME, - *name, - ))); + children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::NAME, *name))); } if let Some(colon) = parts.colon { - children.push(SyntaxElement::Token(SyntaxToken::new( - SyntaxKind::COLON, - colon, - ))); + children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::COLON, colon))); } if let Some(t) = parts.param_type { children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::TYPE, t))); } if let Some(opt) = parts.optional { - children.push(SyntaxElement::Token(SyntaxToken::new( - SyntaxKind::OPTIONAL, - opt, - ))); + children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::OPTIONAL, opt))); } if let Some(dk) = parts.default_keyword { - children.push(SyntaxElement::Token(SyntaxToken::new( - SyntaxKind::DEFAULT_KEYWORD, - dk, - ))); + children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::DEFAULT_KEYWORD, dk))); } if let Some(ds) = parts.default_separator { children.push(SyntaxElement::Token(SyntaxToken::new( @@ -348,16 +317,10 @@ fn build_parameter_node(parts: &ParamHeaderParts, range: TextRange) -> SyntaxNod ))); } if let Some(dv) = parts.default_value { - children.push(SyntaxElement::Token(SyntaxToken::new( - SyntaxKind::DEFAULT_VALUE, - dv, - ))); + children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::DEFAULT_VALUE, dv))); } if let Some(desc) = parts.first_description { - children.push(SyntaxElement::Token(SyntaxToken::new( - SyntaxKind::DESCRIPTION, - desc, - ))); + children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::DESCRIPTION, desc))); } SyntaxNode::new(SyntaxKind::NUMPY_PARAMETER, range, children) } @@ -376,10 +339,7 @@ fn build_returns_node( children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::COLON, c))); } if let Some(rt) = return_type { - children.push(SyntaxElement::Token(SyntaxToken::new( - SyntaxKind::RETURN_TYPE, - rt, - ))); + children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::RETURN_TYPE, rt))); } SyntaxNode::new(SyntaxKind::NUMPY_RETURNS, range, children) } @@ -391,18 +351,12 @@ fn build_exception_node( range: TextRange, ) -> SyntaxNode { let mut children = Vec::new(); - children.push(SyntaxElement::Token(SyntaxToken::new( - SyntaxKind::TYPE, - exc_type, - ))); + children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::TYPE, exc_type))); if let Some(c) = colon { children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::COLON, c))); } if let Some(d) = first_desc { - children.push(SyntaxElement::Token(SyntaxToken::new( - SyntaxKind::DESCRIPTION, - d, - ))); + children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::DESCRIPTION, d))); } SyntaxNode::new(SyntaxKind::NUMPY_EXCEPTION, range, children) } @@ -414,18 +368,12 @@ fn build_warning_node( range: TextRange, ) -> SyntaxNode { let mut children = Vec::new(); - children.push(SyntaxElement::Token(SyntaxToken::new( - SyntaxKind::TYPE, - warn_type, - ))); + children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::TYPE, warn_type))); if let Some(c) = colon { children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::COLON, c))); } if let Some(d) = first_desc { - children.push(SyntaxElement::Token(SyntaxToken::new( - SyntaxKind::DESCRIPTION, - d, - ))); + children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::DESCRIPTION, d))); } SyntaxNode::new(SyntaxKind::NUMPY_WARNING, range, children) } @@ -442,19 +390,13 @@ fn build_see_also_node( let mut children = Vec::new(); let names = parse_name_list(names_str, names_line, names_col, cursor); for name in &names { - children.push(SyntaxElement::Token(SyntaxToken::new( - SyntaxKind::NAME, - *name, - ))); + children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::NAME, *name))); } if let Some(c) = colon { children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::COLON, c))); } if let Some(d) = description { - children.push(SyntaxElement::Token(SyntaxToken::new( - SyntaxKind::DESCRIPTION, - d, - ))); + children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::DESCRIPTION, d))); } SyntaxNode::new(SyntaxKind::NUMPY_SEE_ALSO_ITEM, range, children) } @@ -463,16 +405,10 @@ fn build_attribute_node(parts: &ParamHeaderParts, range: TextRange) -> SyntaxNod let mut children = Vec::new(); // Attributes use the first name only. if let Some(name) = parts.names.first() { - children.push(SyntaxElement::Token(SyntaxToken::new( - SyntaxKind::NAME, - *name, - ))); + children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::NAME, *name))); } if let Some(colon) = parts.colon { - children.push(SyntaxElement::Token(SyntaxToken::new( - SyntaxKind::COLON, - colon, - ))); + children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::COLON, colon))); } if let Some(t) = parts.param_type { children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::TYPE, t))); @@ -482,10 +418,7 @@ fn build_attribute_node(parts: &ParamHeaderParts, range: TextRange) -> SyntaxNod fn build_method_node(name: TextRange, colon: Option, range: TextRange) -> SyntaxNode { let mut children = Vec::new(); - children.push(SyntaxElement::Token(SyntaxToken::new( - SyntaxKind::NAME, - name, - ))); + children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::NAME, name))); if let Some(c) = colon { children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::COLON, c))); } @@ -510,29 +443,20 @@ fn build_reference_node_rst( open_bracket, ))); if let Some(n) = number { - children.push(SyntaxElement::Token(SyntaxToken::new( - SyntaxKind::NUMBER, - n, - ))); + children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::NUMBER, n))); } children.push(SyntaxElement::Token(SyntaxToken::new( SyntaxKind::CLOSE_BRACKET, close_bracket, ))); if let Some(c) = content { - children.push(SyntaxElement::Token(SyntaxToken::new( - SyntaxKind::CONTENT, - c, - ))); + children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::CONTENT, c))); } SyntaxNode::new(SyntaxKind::NUMPY_REFERENCE, range, children) } fn build_reference_node_plain(content: TextRange, range: TextRange) -> SyntaxNode { - let children = vec![SyntaxElement::Token(SyntaxToken::new( - SyntaxKind::CONTENT, - content, - ))]; + let children = vec![SyntaxElement::Token(SyntaxToken::new(SyntaxKind::CONTENT, content))]; SyntaxNode::new(SyntaxKind::NUMPY_REFERENCE, range, children) } @@ -553,10 +477,7 @@ fn extend_last_node_description(nodes: &mut [SyntaxElement], cont: TextRange) { } } if !found_desc { - node.push_child(SyntaxElement::Token(SyntaxToken::new( - SyntaxKind::DESCRIPTION, - cont, - ))); + node.push_child(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::DESCRIPTION, cont))); } node.extend_range_to(cont.end()); } @@ -576,20 +497,13 @@ fn extend_last_ref_content(nodes: &mut [SyntaxElement], cont: TextRange) { } } if !found_content { - node.push_child(SyntaxElement::Token(SyntaxToken::new( - SyntaxKind::CONTENT, - cont, - ))); + node.push_child(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::CONTENT, cont))); } node.extend_range_to(cont.end()); } } -fn process_parameter_line( - cursor: &LineCursor, - nodes: &mut Vec, - entry_indent: &mut Option, -) { +fn process_parameter_line(cursor: &LineCursor, nodes: &mut Vec, entry_indent: &mut Option) { let indent_cols = cursor.current_indent_columns(); if let Some(base) = *entry_indent { if indent_cols > base { @@ -605,17 +519,10 @@ fn process_parameter_line( let trimmed = cursor.current_trimmed(); let parts = parse_name_and_type(trimmed, cursor.line, col, cursor); let entry_range = cursor.current_trimmed_range(); - nodes.push(SyntaxElement::Node(build_parameter_node( - &parts, - entry_range, - ))); + nodes.push(SyntaxElement::Node(build_parameter_node(&parts, entry_range))); } -fn process_returns_line( - cursor: &LineCursor, - nodes: &mut Vec, - entry_indent: &mut Option, -) { +fn process_returns_line(cursor: &LineCursor, nodes: &mut Vec, entry_indent: &mut Option) { let indent_cols = cursor.current_indent_columns(); if let Some(base) = *entry_indent { if indent_cols > base { @@ -659,11 +566,7 @@ fn process_returns_line( ))); } -fn process_raises_line( - cursor: &LineCursor, - nodes: &mut Vec, - entry_indent: &mut Option, -) { +fn process_raises_line(cursor: &LineCursor, nodes: &mut Vec, entry_indent: &mut Option) { let indent_cols = cursor.current_indent_columns(); if let Some(base) = *entry_indent { if indent_cols > base { @@ -706,11 +609,7 @@ fn process_raises_line( ))); } -fn process_warning_line( - cursor: &LineCursor, - nodes: &mut Vec, - entry_indent: &mut Option, -) { +fn process_warning_line(cursor: &LineCursor, nodes: &mut Vec, entry_indent: &mut Option) { let indent_cols = cursor.current_indent_columns(); if let Some(base) = *entry_indent { if indent_cols > base { @@ -753,11 +652,7 @@ fn process_warning_line( ))); } -fn process_see_also_line( - cursor: &LineCursor, - nodes: &mut Vec, - entry_indent: &mut Option, -) { +fn process_see_also_line(cursor: &LineCursor, nodes: &mut Vec, entry_indent: &mut Option) { let indent_cols = cursor.current_indent_columns(); if let Some(base) = *entry_indent { if indent_cols > base { @@ -802,11 +697,7 @@ fn process_see_also_line( ))); } -fn process_attribute_line( - cursor: &LineCursor, - nodes: &mut Vec, - entry_indent: &mut Option, -) { +fn process_attribute_line(cursor: &LineCursor, nodes: &mut Vec, entry_indent: &mut Option) { let indent_cols = cursor.current_indent_columns(); if let Some(base) = *entry_indent { if indent_cols > base { @@ -822,17 +713,10 @@ fn process_attribute_line( let trimmed = cursor.current_trimmed(); let parts = parse_name_and_type(trimmed, cursor.line, col, cursor); let entry_range = cursor.current_trimmed_range(); - nodes.push(SyntaxElement::Node(build_attribute_node( - &parts, - entry_range, - ))); + nodes.push(SyntaxElement::Node(build_attribute_node(&parts, entry_range))); } -fn process_method_line( - cursor: &LineCursor, - nodes: &mut Vec, - entry_indent: &mut Option, -) { +fn process_method_line(cursor: &LineCursor, nodes: &mut Vec, entry_indent: &mut Option) { let indent_cols = cursor.current_indent_columns(); if let Some(base) = *entry_indent { if indent_cols > base { @@ -858,18 +742,10 @@ fn process_method_line( }; let entry_range = cursor.current_trimmed_range(); - nodes.push(SyntaxElement::Node(build_method_node( - name, - colon, - entry_range, - ))); + nodes.push(SyntaxElement::Node(build_method_node(name, colon, entry_range))); } -fn process_reference_line( - cursor: &LineCursor, - nodes: &mut Vec, - entry_indent: &mut Option, -) { +fn process_reference_line(cursor: &LineCursor, nodes: &mut Vec, entry_indent: &mut Option) { let indent_cols = cursor.current_indent_columns(); if let Some(base) = *entry_indent { if indent_cols > base { @@ -900,10 +776,8 @@ fn process_reference_line( } else { None }; - let line_end_offset = - cursor.substr_offset(cursor.current_line_text()) + cursor.current_line_text().len(); - let after_on_line = - &cursor.source()[abs_close + 1..line_end_offset.min(cursor.source().len())]; + let line_end_offset = cursor.substr_offset(cursor.current_line_text()) + cursor.current_line_text().len(); + let after_on_line = &cursor.source()[abs_close + 1..line_end_offset.min(cursor.source().len())]; let content_str = after_on_line.trim(); let content = if !content_str.is_empty() { Some(TextRange::from_offset_len( @@ -999,10 +873,7 @@ impl SectionBody { Self::Methods(nodes) => nodes, Self::FreeText(range) => { if let Some(r) = range { - vec![SyntaxElement::Token(SyntaxToken::new( - SyntaxKind::BODY_TEXT, - r, - ))] + vec![SyntaxElement::Token(SyntaxToken::new(SyntaxKind::BODY_TEXT, r))] } else { vec![] } @@ -1042,11 +913,7 @@ pub fn parse_numpy(input: &str) -> Parsed { cursor.skip_blanks(); if cursor.is_eof() { - let root = SyntaxNode::new( - SyntaxKind::NUMPY_DOCSTRING, - cursor.full_range(), - root_children, - ); + let root = SyntaxNode::new(SyntaxKind::NUMPY_DOCSTRING, cursor.full_range(), root_children); return Parsed::new(input.to_string(), root); } @@ -1074,10 +941,7 @@ pub fn parse_numpy(input: &str) -> Parsed { let last_col = indent_len(last_text) + last_text.trim().len(); let range = cursor.make_range(start_line, start_col, last_line, last_col); if !range.is_empty() { - root_children.push(SyntaxElement::Token(SyntaxToken::new( - SyntaxKind::SUMMARY, - range, - ))); + root_children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::SUMMARY, range))); } } } @@ -1126,10 +990,7 @@ pub fn parse_numpy(input: &str) -> Parsed { let desc_range = collect_description(&mut cursor, indent_columns(line)); if let Some(desc) = desc_range { - dep_children.push(SyntaxElement::Token(SyntaxToken::new( - SyntaxKind::DESCRIPTION, - desc, - ))); + dep_children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::DESCRIPTION, desc))); } // Compute deprecation span @@ -1194,8 +1055,7 @@ pub fn parse_numpy(input: &str) -> Parsed { if let Some(header_info) = try_detect_header(&cursor) { // Flush previous section if let Some(prev_header) = current_header.take() { - let section_node = - flush_section(&cursor, prev_header, current_body.take().unwrap()); + let section_node = flush_section(&cursor, prev_header, current_body.take().unwrap()); root_children.push(SyntaxElement::Node(section_node)); } @@ -1225,11 +1085,7 @@ pub fn parse_numpy(input: &str) -> Parsed { root_children.push(SyntaxElement::Node(section_node)); } - let root = SyntaxNode::new( - SyntaxKind::NUMPY_DOCSTRING, - cursor.full_range(), - root_children, - ); + let root = SyntaxNode::new(SyntaxKind::NUMPY_DOCSTRING, cursor.full_range(), root_children); Parsed::new(input.to_string(), root) } @@ -1265,10 +1121,7 @@ mod tests { fn test_try_detect_header() { let c1 = LineCursor::new("Parameters\n----------"); assert!(try_detect_header(&c1).is_some()); - assert_eq!( - try_detect_header(&c1).unwrap().kind, - NumPySectionKind::Parameters - ); + assert_eq!(try_detect_header(&c1).unwrap().kind, NumPySectionKind::Parameters); let c2 = LineCursor::new("just text\nmore text"); assert!(try_detect_header(&c2).is_none()); @@ -1283,10 +1136,7 @@ mod tests { assert!(try_detect_header(&c5).is_some()); c5.line = 3; assert!(try_detect_header(&c5).is_some()); - assert_eq!( - try_detect_header(&c5).unwrap().kind, - NumPySectionKind::Returns - ); + assert_eq!(try_detect_header(&c5).unwrap().kind, NumPySectionKind::Returns); } #[test] diff --git a/src/parse/numpy/to_model.rs b/src/parse/numpy/to_model.rs index 590bb12..5ec7e0f 100644 --- a/src/parse/numpy/to_model.rs +++ b/src/parse/numpy/to_model.rs @@ -1,8 +1,8 @@ //! Convert a NumPy-style AST into the style-independent [`Docstring`] model. use crate::model::{ - Attribute, Deprecation, Docstring, ExceptionEntry, FreeSectionKind, Method, Parameter, - Reference, Return, Section, SeeAlsoEntry, + Attribute, Deprecation, Docstring, ExceptionEntry, FreeSectionKind, Method, Parameter, Reference, Return, Section, + SeeAlsoEntry, }; use crate::parse::numpy::kind::NumPySectionKind; use crate::parse::numpy::nodes::{NumPyDocstring, NumPySection}; @@ -23,10 +23,7 @@ pub fn to_model(parsed: &Parsed) -> Option { description: dep.description().map(|t| t.text(source).to_owned()), }); - let sections = root - .sections() - .map(|s| convert_section(&s, source)) - .collect(); + let sections = root.sections().map(|s| convert_section(&s, source)).collect(); Some(Docstring { summary, @@ -41,22 +38,16 @@ fn convert_section(section: &NumPySection<'_>, source: &str) -> Section { match kind { NumPySectionKind::Parameters | NumPySectionKind::Receives => { - let entries = section - .parameters() - .map(|p| convert_parameter(&p, source)) - .collect(); + let entries = section.parameters().map(|p| convert_parameter(&p, source)).collect(); match kind { NumPySectionKind::Parameters => Section::Parameters(entries), NumPySectionKind::Receives => Section::Receives(entries), _ => unreachable!(), } } - NumPySectionKind::OtherParameters => Section::OtherParameters( - section - .parameters() - .map(|p| convert_parameter(&p, source)) - .collect(), - ), + NumPySectionKind::OtherParameters => { + Section::OtherParameters(section.parameters().map(|p| convert_parameter(&p, source)).collect()) + } NumPySectionKind::Returns | NumPySectionKind::Yields => { let entries: Vec = section .returns() @@ -138,23 +129,15 @@ fn convert_section(section: &NumPySection<'_>, source: &str) -> Section { NumPySectionKind::Notes => FreeSectionKind::Notes, NumPySectionKind::Examples => FreeSectionKind::Examples, NumPySectionKind::Warnings => FreeSectionKind::Warnings, - NumPySectionKind::Unknown => { - FreeSectionKind::Unknown(section.header().name().text(source).to_owned()) - } + NumPySectionKind::Unknown => FreeSectionKind::Unknown(section.header().name().text(source).to_owned()), _ => unreachable!(), }; - Section::FreeText { - kind: free_kind, - body, - } + Section::FreeText { kind: free_kind, body } } } } -fn convert_parameter( - param: &crate::parse::numpy::nodes::NumPyParameter<'_>, - source: &str, -) -> Parameter { +fn convert_parameter(param: &crate::parse::numpy::nodes::NumPyParameter<'_>, source: &str) -> Parameter { Parameter { names: param.names().map(|n| n.text(source).to_owned()).collect(), type_annotation: param.r#type().map(|t| t.text(source).to_owned()), diff --git a/src/parse/utils.rs b/src/parse/utils.rs index fbc4008..6d69749 100644 --- a/src/parse/utils.rs +++ b/src/parse/utils.rs @@ -117,10 +117,6 @@ pub(crate) fn strip_optional(type_content: &str) -> (&str, Option) { pub(crate) struct BracketEntry<'a> { /// Name text before the bracket (end-trimmed). pub name: &'a str, - /// Byte offset of the opening bracket. - pub open_bracket: usize, - /// Byte offset of the closing bracket. - pub close_bracket: usize, /// Clean type text (optional stripped) inside brackets. pub clean_type: &'a str, /// Byte offset of the type text start. @@ -168,14 +164,9 @@ pub(crate) fn try_parse_bracket_entry(text: &str) -> Option> { let name = text[..bracket_pos].trim_end(); - let type_raw = &text[bracket_pos + 1..close_pos]; - let type_trimmed = type_raw.trim(); - let leading_ws = type_raw.len() - type_raw.trim_start().len(); - let type_offset = bracket_pos + 1 + leading_ws; - - let (clean_type, opt_rel) = strip_optional(type_trimmed); - let optional_offset = opt_rel.map(|r| type_offset + r); - + // Determine colon, description, and description_offset first, + // since we need to know the colon position to compute the type range + // (when colon is inside brackets, the type ends at the colon). let (colon, description, description_offset) = if after_close.starts_with(':') { let colon_byte = text[close_pos + 1..].find(':').unwrap() + close_pos + 1; let after_colon = &text[colon_byte + 1..]; @@ -190,10 +181,24 @@ pub(crate) fn try_parse_bracket_entry(text: &str) -> Option> { (None, None, None) }; + // Determine where the type portion ends. Normally at close_pos, + // but if a colon is inside the brackets, it ends at the colon. + let type_end = if let Some(c) = colon { + if c > bracket_pos && c < close_pos { c } else { close_pos } + } else { + close_pos + }; + + let type_raw = &text[bracket_pos + 1..type_end]; + let type_trimmed = type_raw.trim(); + let leading_ws = type_raw.len() - type_raw.trim_start().len(); + let type_offset = bracket_pos + 1 + leading_ws; + + let (clean_type, opt_rel) = strip_optional(type_trimmed); + let optional_offset = opt_rel.map(|r| type_offset + r); + Some(BracketEntry { name, - open_bracket: bracket_pos, - close_bracket: close_pos, clean_type, type_offset, optional_offset, @@ -203,6 +208,48 @@ pub(crate) fn try_parse_bracket_entry(text: &str) -> Option> { }) } +/// Find the byte offset of the first `:` in `text[start..]` that is not inside +/// `[]`, `{}`, or `<>` brackets. Unlike [`find_entry_colon`] this does **not** +/// track `()` depth, which is useful when parsing inside an unclosed `(`. +/// +/// Returns an absolute byte offset into `text`. +pub(crate) fn find_colon_ignoring_parens(text: &str, start: usize) -> Option { + let mut depth: u32 = 0; + for (i, b) in text[start..].bytes().enumerate() { + match b { + b'[' | b'{' | b'<' => depth += 1, + b']' | b'}' | b'>' => depth = depth.saturating_sub(1), + b':' if depth == 0 => return Some(start + i), + _ => {} + } + } + None +} + +/// Try to find an opening bracket for a bracket-style entry. +/// +/// Returns `Some(bracket_pos)` when there is a `(`, `[`, `{`, or `<` after at +/// least one character, and that bracket appears before any top-level colon. +/// Returns `None` otherwise. +pub(crate) fn find_entry_open_bracket(text: &str) -> Option { + let bracket_pos = text.bytes().enumerate().find_map(|(i, b)| { + if i > 0 && matches!(b, b'(' | b'[' | b'{' | b'<') { + Some(i) + } else { + None + } + })?; + + // The bracket must appear before any top-level colon. + if let Some(colon_pos) = find_entry_colon(text) { + if colon_pos < bracket_pos { + return None; + } + } + + Some(bracket_pos) +} + #[cfg(test)] mod tests { use super::*; @@ -284,10 +331,7 @@ mod tests { fn test_strip_optional_basic() { assert_eq!(strip_optional("int, optional"), ("int", Some(5))); assert_eq!(strip_optional("int"), ("int", None)); - assert_eq!( - strip_optional("Dict[str, int], optional"), - ("Dict[str, int]", Some(16)) - ); + assert_eq!(strip_optional("Dict[str, int], optional"), ("Dict[str, int]", Some(16))); assert_eq!(strip_optional("optional"), ("", Some(0))); assert_eq!(strip_optional("int,optional"), ("int", Some(4))); assert_eq!(strip_optional("int, optional"), ("int", Some(6))); @@ -358,4 +402,52 @@ mod tests { // `name (int) not_colon` — non-colon text after bracket. assert!(try_parse_bracket_entry("name (int) not_colon").is_none()); } + + // ---- find_colon_ignoring_parens ---- + + #[test] + fn test_find_colon_ignoring_parens_basic() { + assert_eq!(find_colon_ignoring_parens("int : desc", 0), Some(4)); + } + + #[test] + fn test_find_colon_ignoring_parens_inside_brackets() { + // `:` inside `[]` is skipped. + assert_eq!(find_colon_ignoring_parens("Dict[k: v] : desc", 0), Some(11)); + } + + #[test] + fn test_find_colon_ignoring_parens_inside_parens() { + // Parens are NOT tracked, so `:` inside `(` is found. + assert_eq!(find_colon_ignoring_parens("(int : desc", 1), Some(5)); + } + + #[test] + fn test_find_colon_ignoring_parens_none() { + assert_eq!(find_colon_ignoring_parens("int desc", 0), None); + } + + // ---- find_entry_open_bracket ---- + + #[test] + fn test_find_entry_open_bracket_basic() { + assert_eq!(find_entry_open_bracket("name (int)"), Some(5)); + } + + #[test] + fn test_find_entry_open_bracket_colon_first() { + // Colon before bracket → None. + assert_eq!(find_entry_open_bracket("name : (int)"), None); + } + + #[test] + fn test_find_entry_open_bracket_no_bracket() { + assert_eq!(find_entry_open_bracket("name : int"), None); + } + + #[test] + fn test_find_entry_open_bracket_at_start() { + // Bracket at position 0 is not valid (no name before it). + assert_eq!(find_entry_open_bracket("(int)"), None); + } } diff --git a/src/syntax.rs b/src/syntax.rs index 786a6dc..921a411 100644 --- a/src/syntax.rs +++ b/src/syntax.rs @@ -239,11 +239,7 @@ pub struct SyntaxNode { impl SyntaxNode { /// Creates a new node with the given kind, range, and children. pub fn new(kind: SyntaxKind, range: TextRange, children: Vec) -> Self { - Self { - kind, - range, - children, - } + Self { kind, range, children } } /// The kind of this node. @@ -276,10 +272,21 @@ impl SyntaxNode { self.range = TextRange::new(self.range.start(), end); } - /// Find the first token child with the given kind. + /// Find the first present (non-missing) token child with the given kind. + /// + /// Zero-length tokens are considered missing and are excluded. + /// Use [`find_missing`](Self::find_missing) to find missing tokens. pub fn find_token(&self, kind: SyntaxKind) -> Option<&SyntaxToken> { self.children.iter().find_map(|c| match c { - SyntaxElement::Token(t) if t.kind() == kind => Some(t), + SyntaxElement::Token(t) if t.kind() == kind && !t.is_missing() => Some(t), + _ => None, + }) + } + + /// Find the first missing (zero-length) token child with the given kind. + pub fn find_missing(&self, kind: SyntaxKind) -> Option<&SyntaxToken> { + self.children.iter().find_map(|c| match c { + SyntaxElement::Token(t) if t.kind() == kind && t.is_missing() => Some(t), _ => None, }) } @@ -363,6 +370,11 @@ impl SyntaxToken { &self.range } + /// Whether this token is missing from the source (zero-length placeholder). + pub fn is_missing(&self) -> bool { + self.range.is_empty() + } + /// Extract the corresponding text slice from source. pub fn text<'a>(&self, source: &'a str) -> &'a str { self.range.source_text(source) @@ -378,13 +390,7 @@ impl SyntaxToken { for _ in 0..indent { out.push_str(" "); } - let _ = writeln!( - out, - "{}: {:?}@{}", - self.kind.name(), - self.text(src), - self.range - ); + let _ = writeln!(out, "{}: {:?}@{}", self.kind.name(), self.text(src), self.range); } } @@ -520,10 +526,7 @@ mod tests { #[test] fn test_syntax_token_text() { let source = "hello world"; - let token = SyntaxToken::new( - SyntaxKind::NAME, - TextRange::new(TextSize::new(0), TextSize::new(5)), - ); + let token = SyntaxToken::new(SyntaxKind::NAME, TextRange::new(TextSize::new(0), TextSize::new(5))); assert_eq!(token.text(source), "hello"); } @@ -570,11 +573,7 @@ mod tests { vec![SyntaxElement::Node(child)], ); - assert!( - parent - .find_node(SyntaxKind::GOOGLE_SECTION_HEADER) - .is_some() - ); + assert!(parent.find_node(SyntaxKind::GOOGLE_SECTION_HEADER).is_some()); assert!(parent.find_node(SyntaxKind::GOOGLE_ARG).is_none()); assert_eq!(parent.nodes(SyntaxKind::GOOGLE_SECTION_HEADER).count(), 1); } @@ -617,10 +616,7 @@ mod tests { )), SyntaxElement::Token(SyntaxToken::new( SyntaxKind::DESCRIPTION, - TextRange::new( - TextSize::new(13), - TextSize::new(source.len() as u32), - ), + TextRange::new(TextSize::new(13), TextSize::new(source.len() as u32)), )), ], )), @@ -667,10 +663,7 @@ mod tests { } } - let mut counter = Counter { - nodes: 0, - tokens: 0, - }; + let mut counter = Counter { nodes: 0, tokens: 0 }; walk(&root, &mut counter); assert_eq!(counter.nodes, 1); assert_eq!(counter.tokens, 1); diff --git a/tests/google/args.rs b/tests/google/args.rs index c0416af..3d98c7f 100644 --- a/tests/google/args.rs +++ b/tests/google/args.rs @@ -12,10 +12,7 @@ fn test_args_basic() { assert_eq!(a.len(), 1); assert_eq!(a[0].name().text(result.source()), "x"); assert_eq!(a[0].r#type().unwrap().text(result.source()), "int"); - assert_eq!( - a[0].description().unwrap().text(result.source()), - "The value." - ); + assert_eq!(a[0].description().unwrap().text(result.source()), "The value."); } #[test] @@ -37,10 +34,7 @@ fn test_args_no_type() { let a = args(&result); assert_eq!(a[0].name().text(result.source()), "x"); assert!(a[0].r#type().is_none()); - assert_eq!( - a[0].description().unwrap().text(result.source()), - "The value." - ); + assert_eq!(a[0].description().unwrap().text(result.source()), "The value."); } /// Colon with no space after it: `name:description` @@ -50,10 +44,7 @@ fn test_args_no_space_after_colon() { let result = parse_google(docstring); let a = args(&result); assert_eq!(a[0].name().text(result.source()), "x"); - assert_eq!( - a[0].description().unwrap().text(result.source()), - "The value." - ); + assert_eq!(a[0].description().unwrap().text(result.source()), "The value."); } /// Colon with extra spaces: `name: description` @@ -63,10 +54,7 @@ fn test_args_extra_spaces_after_colon() { let result = parse_google(docstring); let a = args(&result); assert_eq!(a[0].name().text(result.source()), "x"); - assert_eq!( - a[0].description().unwrap().text(result.source()), - "The value." - ); + assert_eq!(a[0].description().unwrap().text(result.source()), "The value."); } #[test] @@ -101,14 +89,10 @@ fn test_args_tuple_type() { #[test] fn test_args_multiline_description() { - let docstring = - "Summary.\n\nArgs:\n x (int): First line.\n Second line.\n Third line."; + let docstring = "Summary.\n\nArgs:\n x (int): First line.\n Second line.\n Third line."; let result = parse_google(docstring); assert_eq!( - args(&result)[0] - .description() - .unwrap() - .text(result.source()), + args(&result)[0].description().unwrap().text(result.source()), "First line.\n Second line.\n Third line." ); } @@ -120,10 +104,7 @@ fn test_args_description_on_next_line() { let a = args(&result); assert_eq!(a[0].name().text(result.source()), "x"); assert_eq!(a[0].r#type().unwrap().text(result.source()), "int"); - assert_eq!( - a[0].description().unwrap().text(result.source()), - "The description." - ); + assert_eq!(a[0].description().unwrap().text(result.source()), "The description."); } #[test] @@ -133,15 +114,9 @@ fn test_args_varargs() { let a = args(&result); assert_eq!(a.len(), 2); assert_eq!(a[0].name().text(result.source()), "*args"); - assert_eq!( - a[0].description().unwrap().text(result.source()), - "Positional args." - ); + assert_eq!(a[0].description().unwrap().text(result.source()), "Positional args."); assert_eq!(a[1].name().text(result.source()), "**kwargs"); - assert_eq!( - a[1].description().unwrap().text(result.source()), - "Keyword args." - ); + assert_eq!(a[1].description().unwrap().text(result.source()), "Keyword args."); } #[test] @@ -173,10 +148,7 @@ fn test_parameters_alias() { assert_eq!(args(&result).len(), 1); assert_eq!(args(&result)[0].name().text(result.source()), "x"); assert_eq!( - all_sections(&result)[0] - .header() - .name() - .text(result.source()), + all_sections(&result)[0].header().name().text(result.source()), "Parameters" ); } @@ -186,13 +158,7 @@ fn test_params_alias() { let docstring = "Summary.\n\nParams:\n x (int): The value."; let result = parse_google(docstring); assert_eq!(args(&result).len(), 1); - assert_eq!( - all_sections(&result)[0] - .header() - .name() - .text(result.source()), - "Params" - ); + assert_eq!(all_sections(&result)[0].header().name().text(result.source()), "Params"); } // ============================================================================= @@ -207,10 +173,7 @@ fn test_args_name_span() { let name = arg.name(); // "x" starts at byte offset 20 (line 3, col 4) assert_eq!(name.range().start(), TextSize::new(20)); - assert_eq!( - name.range().end(), - TextSize::new(name.range().start().raw() + 1) - ); + assert_eq!(name.range().end(), TextSize::new(name.range().start().raw() + 1)); assert_eq!(name.text(result.source()), "x"); } @@ -344,7 +307,8 @@ fn test_complex_optional_type() { #[test] fn test_keyword_args_basic() { - let docstring = "Summary.\n\nKeyword Args:\n timeout (int): Timeout in seconds.\n retries (int): Number of retries."; + let docstring = + "Summary.\n\nKeyword Args:\n timeout (int): Timeout in seconds.\n retries (int): Number of retries."; let result = parse_google(docstring); let ka = keyword_args(&result); assert_eq!(ka.len(), 2); @@ -359,10 +323,7 @@ fn test_keyword_arguments_alias() { let result = parse_google(docstring); assert_eq!(keyword_args(&result).len(), 1); assert_eq!( - all_sections(&result)[0] - .header() - .name() - .text(result.source()), + all_sections(&result)[0].header().name().text(result.source()), "Keyword Arguments" ); } @@ -426,10 +387,7 @@ fn test_receive_alias() { let result = parse_google(docstring); assert_eq!(receives(&result).len(), 1); assert_eq!( - all_sections(&result)[0] - .header() - .name() - .text(result.source()), + all_sections(&result)[0].header().name().text(result.source()), "Receive" ); } diff --git a/tests/google/edge_cases.rs b/tests/google/edge_cases.rs index e7e2a50..2db9414 100644 --- a/tests/google/edge_cases.rs +++ b/tests/google/edge_cases.rs @@ -8,10 +8,7 @@ use super::*; fn test_indented_docstring() { let docstring = " Summary.\n\n Args:\n x (int): Value."; let result = parse_google(docstring); - assert_eq!( - doc(&result).summary().unwrap().text(result.source()), - "Summary." - ); + assert_eq!(doc(&result).summary().unwrap().text(result.source()), "Summary."); let a = args(&result); assert_eq!(a.len(), 1); assert_eq!(a[0].name().text(result.source()), "x"); @@ -41,13 +38,7 @@ fn test_section_header_space_before_colon() { assert_eq!(a.len(), 1, "expected 1 arg from 'Args :'"); assert_eq!(a[0].name().text(result.source()), "x"); - assert_eq!( - all_sections(&result)[0] - .header() - .name() - .text(result.source()), - "Args" - ); + assert_eq!(all_sections(&result)[0].header().name().text(result.source()), "Args"); assert!(all_sections(&result)[0].header().colon().is_some()); } @@ -61,6 +52,7 @@ fn test_returns_space_before_colon() { } /// Colonless `Args` should be parsed as Args section. +/// The section header should contain a missing COLON token. #[test] fn test_section_header_no_colon() { let input = "Summary.\n\nArgs\n x (int): The value."; @@ -69,14 +61,12 @@ fn test_section_header_no_colon() { assert_eq!(a.len(), 1, "expected 1 arg from colonless 'Args'"); assert_eq!(a[0].name().text(result.source()), "x"); - assert_eq!( - all_sections(&result)[0] - .header() - .name() - .text(result.source()), - "Args" - ); - assert!(all_sections(&result)[0].header().colon().is_none()); + let header = all_sections(&result)[0].header(); + assert_eq!(header.name().text(result.source()), "Args"); + assert!(header.colon().is_none(), "no COLON token for colonless header"); + let missing = header.syntax().find_missing(SyntaxKind::COLON); + assert!(missing.is_some(), "colonless header should have a missing COLON"); + assert!(missing.unwrap().is_missing()); } /// Colonless `Returns` should be parsed as Returns section. @@ -131,15 +121,9 @@ fn test_tab_indented_args() { let a = args(&result); assert_eq!(a.len(), 2); assert_eq!(a[0].name().text(result.source()), "x"); - assert_eq!( - a[0].description().unwrap().text(result.source()), - "The value." - ); + assert_eq!(a[0].description().unwrap().text(result.source()), "The value."); assert_eq!(a[1].name().text(result.source()), "y"); - assert_eq!( - a[1].description().unwrap().text(result.source()), - "Another value." - ); + assert_eq!(a[1].description().unwrap().text(result.source()), "Another value."); } /// Args entries with tab indent and descriptions with deeper tab+space indent. @@ -164,10 +148,7 @@ fn test_tab_indented_returns() { assert!(r.is_some()); let r = r.unwrap(); assert_eq!(r.return_type().unwrap().text(result.source()), "int"); - assert_eq!( - r.description().unwrap().text(result.source()), - "The result." - ); + assert_eq!(r.description().unwrap().text(result.source()), "The result."); } /// Raises section with tab-indented entries. @@ -190,3 +171,143 @@ fn test_tab_indented_section_header() { assert_eq!(a.len(), 1); assert_eq!(a[0].name().text(result.source()), "x"); } + +// ============================================================================= +// Missing token tests +// ============================================================================= + +/// `arg1 (int : desc.` — missing close bracket. +/// Parser should preserve type info with a missing CLOSE_BRACKET. +#[test] +fn test_missing_close_bracket() { + let input = "Args:\n arg1 (int : desc."; + let result = parse_google(input); + let a = args(&result); + assert_eq!(a.len(), 1); + assert_eq!(a[0].name().text(result.source()), "arg1"); + assert!(a[0].open_bracket().is_some()); + assert_eq!(a[0].r#type().unwrap().text(result.source()), "int"); + assert!( + a[0].close_bracket().is_none(), + "no CLOSE_BRACKET when bracket is unmatched" + ); + // Missing CLOSE_BRACKET token should be present. + let missing = a[0].syntax().find_missing(SyntaxKind::CLOSE_BRACKET); + assert!(missing.is_some(), "should have a missing CLOSE_BRACKET token"); + assert!(missing.unwrap().is_missing()); + assert_eq!(a[0].description().unwrap().text(result.source()), "desc."); +} + +/// `arg1 (int) desc` — close bracket present but colon missing before description. +#[test] +fn test_missing_colon_after_bracket() { + let input = "Args:\n arg1 (int) description here."; + let result = parse_google(input); + let a = args(&result); + assert_eq!(a.len(), 1); + assert_eq!(a[0].name().text(result.source()), "arg1"); + assert_eq!(a[0].r#type().unwrap().text(result.source()), "int"); + assert!(a[0].open_bracket().is_some()); + assert!(a[0].close_bracket().is_some()); + assert!(a[0].colon().is_none(), "no COLON token"); + // Missing COLON token. + let missing = a[0].syntax().find_missing(SyntaxKind::COLON); + assert!(missing.is_some(), "should have a missing COLON token"); + assert!(missing.unwrap().is_missing()); + assert_eq!(a[0].description().unwrap().text(result.source()), "description here."); +} + +/// `arg1 (int` — missing close bracket and no colon/description. +#[test] +fn test_missing_close_bracket_no_colon() { + let input = "Args:\n arg1 (int"; + let result = parse_google(input); + let a = args(&result); + assert_eq!(a.len(), 1); + assert_eq!(a[0].name().text(result.source()), "arg1"); + assert_eq!(a[0].r#type().unwrap().text(result.source()), "int"); + assert!(a[0].open_bracket().is_some()); + assert!(a[0].close_bracket().is_none()); + assert!(a[0].colon().is_none()); + // Missing CLOSE_BRACKET but no missing COLON (no description). + assert!(a[0].syntax().find_missing(SyntaxKind::CLOSE_BRACKET).is_some()); + assert!(a[0].syntax().find_missing(SyntaxKind::COLON).is_none()); +} + +/// `arg1 (int desc.` — no close bracket and no colon. +/// Entire content after `(` is TYPE; no colon/description. +#[test] +fn test_missing_bracket_no_colon_no_split() { + let input = "Args:\n arg1 (int desc."; + let result = parse_google(input); + let a = args(&result); + assert_eq!(a.len(), 1); + assert_eq!(a[0].name().text(result.source()), "arg1"); + assert_eq!(a[0].r#type().unwrap().text(result.source()), "int desc."); + assert!(a[0].close_bracket().is_none()); + assert!(a[0].colon().is_none()); + assert!(a[0].description().is_none()); +} + +/// `arg1 (int:desc.)` — colon inside brackets. +/// Entire bracket content is TYPE; colon inside brackets is not treated as separator. +#[test] +fn test_colon_inside_brackets() { + let input = "Args:\n arg1 (int:desc.)"; + let result = parse_google(input); + let a = args(&result); + assert_eq!(a.len(), 1); + assert_eq!(a[0].name().text(result.source()), "arg1"); + assert_eq!(a[0].r#type().unwrap().text(result.source()), "int:desc."); + assert!(a[0].open_bracket().is_some()); + assert!(a[0].close_bracket().is_some()); + assert!(a[0].colon().is_none()); + assert!(a[0].description().is_none()); +} + +/// `arg1 (Dict[str:int])` — colon inside nested brackets should NOT split. +#[test] +fn test_colon_inside_nested_brackets_no_split() { + let input = "Args:\n arg1 (Dict[str:int])"; + let result = parse_google(input); + let a = args(&result); + assert_eq!(a.len(), 1); + assert_eq!(a[0].name().text(result.source()), "arg1"); + assert_eq!(a[0].r#type().unwrap().text(result.source()), "Dict[str:int]"); + assert!(a[0].description().is_none()); +} + +// ============================================================================= +// Arg entry with no description — must not become a section header +// ============================================================================= + +/// `b :` (space before colon, no description) must be parsed as an arg entry, +/// not mistaken for a section header. Regression test for the case where any +/// `word:` pattern inside a section body was mis-classified as a new section. +#[test] +fn test_arg_no_description_space_before_colon_not_header() { + let input = "Args:\n a (int): An integer parameter.\n b :\n c : A parameter."; + let result = parse_google(input); + + // Only one section (Args), not three. + let sections = all_sections(&result); + assert_eq!(sections.len(), 1, "b : should not be a section header"); + + let a = args(&result); + assert_eq!(a.len(), 3, "expected 3 arg entries"); + + assert_eq!(a[0].name().text(result.source()), "a"); + assert_eq!(a[0].r#type().unwrap().text(result.source()), "int"); + assert_eq!( + a[0].description().unwrap().text(result.source()), + "An integer parameter." + ); + + assert_eq!(a[1].name().text(result.source()), "b"); + assert!(a[1].r#type().is_none()); + assert!(a[1].description().is_none()); + + assert_eq!(a[2].name().text(result.source()), "c"); + assert!(a[2].r#type().is_none()); + assert_eq!(a[2].description().unwrap().text(result.source()), "A parameter."); +} diff --git a/tests/google/freetext.rs b/tests/google/freetext.rs index 491d7f7..c2fac86 100644 --- a/tests/google/freetext.rs +++ b/tests/google/freetext.rs @@ -8,20 +8,14 @@ use super::*; fn test_note_section() { let docstring = "Summary.\n\nNote:\n This is a note."; let result = parse_google(docstring); - assert_eq!( - notes(&result).unwrap().text(result.source()), - "This is a note." - ); + assert_eq!(notes(&result).unwrap().text(result.source()), "This is a note."); } #[test] fn test_notes_alias() { let docstring = "Summary.\n\nNotes:\n This is also a note."; let result = parse_google(docstring); - assert_eq!( - notes(&result).unwrap().text(result.source()), - "This is also a note." - ); + assert_eq!(notes(&result).unwrap().text(result.source()), "This is also a note."); } // ============================================================================= @@ -32,10 +26,7 @@ fn test_notes_alias() { fn test_example_section() { let docstring = "Summary.\n\nExample:\n >>> func(1)\n 1"; let result = parse_google(docstring); - assert_eq!( - examples(&result).unwrap().text(result.source()), - ">>> func(1)\n 1" - ); + assert_eq!(examples(&result).unwrap().text(result.source()), ">>> func(1)\n 1"); } #[test] @@ -94,8 +85,7 @@ fn test_todo_without_bullets() { #[test] fn test_todo_multiline() { - let docstring = - "Summary.\n\nTodo:\n * Item one that\n continues here.\n * Item two."; + let docstring = "Summary.\n\nTodo:\n * Item one that\n continues here.\n * Item two."; let result = parse_google(docstring); let t = todo(&result).unwrap(); assert!(t.text(result.source()).contains("Item one that")); @@ -112,10 +102,7 @@ fn test_attention_section() { let docstring = "Summary.\n\nAttention:\n This requires careful handling."; let result = parse_google(docstring); let sections = all_sections(&result); - assert_eq!( - sections[0].section_kind(result.source()), - GoogleSectionKind::Attention - ); + assert_eq!(sections[0].section_kind(result.source()), GoogleSectionKind::Attention); assert_eq!( sections[0].body_text().unwrap().text(result.source()), "This requires careful handling." @@ -127,14 +114,8 @@ fn test_caution_section() { let docstring = "Summary.\n\nCaution:\n Use with care."; let result = parse_google(docstring); let sections = all_sections(&result); - assert_eq!( - sections[0].section_kind(result.source()), - GoogleSectionKind::Caution - ); - assert_eq!( - sections[0].body_text().unwrap().text(result.source()), - "Use with care." - ); + assert_eq!(sections[0].section_kind(result.source()), GoogleSectionKind::Caution); + assert_eq!(sections[0].body_text().unwrap().text(result.source()), "Use with care."); } #[test] @@ -142,10 +123,7 @@ fn test_danger_section() { let docstring = "Summary.\n\nDanger:\n May cause data loss."; let result = parse_google(docstring); let sections = all_sections(&result); - assert_eq!( - sections[0].section_kind(result.source()), - GoogleSectionKind::Danger - ); + assert_eq!(sections[0].section_kind(result.source()), GoogleSectionKind::Danger); assert_eq!( sections[0].body_text().unwrap().text(result.source()), "May cause data loss." @@ -157,10 +135,7 @@ fn test_error_section() { let docstring = "Summary.\n\nError:\n Known issue with large inputs."; let result = parse_google(docstring); let sections = all_sections(&result); - assert_eq!( - sections[0].section_kind(result.source()), - GoogleSectionKind::Error - ); + assert_eq!(sections[0].section_kind(result.source()), GoogleSectionKind::Error); assert_eq!( sections[0].body_text().unwrap().text(result.source()), "Known issue with large inputs." @@ -172,10 +147,7 @@ fn test_hint_section() { let docstring = "Summary.\n\nHint:\n Try using a smaller batch size."; let result = parse_google(docstring); let sections = all_sections(&result); - assert_eq!( - sections[0].section_kind(result.source()), - GoogleSectionKind::Hint - ); + assert_eq!(sections[0].section_kind(result.source()), GoogleSectionKind::Hint); assert_eq!( sections[0].body_text().unwrap().text(result.source()), "Try using a smaller batch size." @@ -187,10 +159,7 @@ fn test_important_section() { let docstring = "Summary.\n\nImportant:\n Must be called before init()."; let result = parse_google(docstring); let sections = all_sections(&result); - assert_eq!( - sections[0].section_kind(result.source()), - GoogleSectionKind::Important - ); + assert_eq!(sections[0].section_kind(result.source()), GoogleSectionKind::Important); assert_eq!( sections[0].body_text().unwrap().text(result.source()), "Must be called before init()." @@ -202,10 +171,7 @@ fn test_tip_section() { let docstring = "Summary.\n\nTip:\n Use vectorized operations for speed."; let result = parse_google(docstring); let sections = all_sections(&result); - assert_eq!( - sections[0].section_kind(result.source()), - GoogleSectionKind::Tip - ); + assert_eq!(sections[0].section_kind(result.source()), GoogleSectionKind::Tip); assert_eq!( sections[0].body_text().unwrap().text(result.source()), "Use vectorized operations for speed." diff --git a/tests/google/main.rs b/tests/google/main.rs index 440a873..9bc82b4 100644 --- a/tests/google/main.rs +++ b/tests/google/main.rs @@ -1,10 +1,10 @@ //! Integration tests for Google-style docstring parser. pub use pydocstring::parse::google::{ - GoogleArg, GoogleAttribute, GoogleDocstring, GoogleException, GoogleMethod, GoogleReturns, - GoogleSection, GoogleSectionKind, GoogleSeeAlsoItem, GoogleWarning, parse_google, + GoogleArg, GoogleAttribute, GoogleDocstring, GoogleException, GoogleMethod, GoogleReturns, GoogleSection, + GoogleSectionKind, GoogleSeeAlsoItem, GoogleWarning, parse_google, }; -pub use pydocstring::syntax::{Parsed, SyntaxToken}; +pub use pydocstring::syntax::{Parsed, SyntaxKind, SyntaxToken}; pub use pydocstring::text::TextSize; mod args; @@ -62,12 +62,7 @@ pub fn raises<'a>(result: &'a Parsed) -> Vec> { pub fn attributes<'a>(result: &'a Parsed) -> Vec> { doc(result) .sections() - .filter(|s| { - matches!( - s.section_kind(result.source()), - GoogleSectionKind::Attributes - ) - }) + .filter(|s| matches!(s.section_kind(result.source()), GoogleSectionKind::Attributes)) .flat_map(|s| s.attributes().collect::>()) .collect() } @@ -75,12 +70,7 @@ pub fn attributes<'a>(result: &'a Parsed) -> Vec> { pub fn keyword_args<'a>(result: &'a Parsed) -> Vec> { doc(result) .sections() - .filter(|s| { - matches!( - s.section_kind(result.source()), - GoogleSectionKind::KeywordArgs - ) - }) + .filter(|s| matches!(s.section_kind(result.source()), GoogleSectionKind::KeywordArgs)) .flat_map(|s| s.args().collect::>()) .collect() } @@ -88,12 +78,7 @@ pub fn keyword_args<'a>(result: &'a Parsed) -> Vec> { pub fn other_parameters<'a>(result: &'a Parsed) -> Vec> { doc(result) .sections() - .filter(|s| { - matches!( - s.section_kind(result.source()), - GoogleSectionKind::OtherParameters - ) - }) + .filter(|s| matches!(s.section_kind(result.source()), GoogleSectionKind::OtherParameters)) .flat_map(|s| s.args().collect::>()) .collect() } @@ -154,12 +139,7 @@ pub fn todo(result: &Parsed) -> Option<&SyntaxToken> { pub fn references(result: &Parsed) -> Option<&SyntaxToken> { doc(result) .sections() - .find(|s| { - matches!( - s.section_kind(result.source()), - GoogleSectionKind::References - ) - }) + .find(|s| matches!(s.section_kind(result.source()), GoogleSectionKind::References)) .and_then(|s| s.body_text()) } diff --git a/tests/google/raises.rs b/tests/google/raises.rs index b5b156d..04abacc 100644 --- a/tests/google/raises.rs +++ b/tests/google/raises.rs @@ -19,8 +19,7 @@ fn test_raises_single() { #[test] fn test_raises_multiple() { - let docstring = - "Summary.\n\nRaises:\n ValueError: If invalid.\n TypeError: If wrong type."; + let docstring = "Summary.\n\nRaises:\n ValueError: If invalid.\n TypeError: If wrong type."; let result = parse_google(docstring); let r = raises(&result); assert_eq!(r.len(), 2); @@ -33,10 +32,7 @@ fn test_raises_multiline_description() { let docstring = "Summary.\n\nRaises:\n ValueError: If the\n input is invalid."; let result = parse_google(docstring); assert_eq!( - raises(&result)[0] - .description() - .unwrap() - .text(result.source()), + raises(&result)[0].description().unwrap().text(result.source()), "If the\n input is invalid." ); } @@ -45,10 +41,7 @@ fn test_raises_multiline_description() { fn test_raises_exception_type_span() { let docstring = "Summary.\n\nRaises:\n ValueError: If bad."; let result = parse_google(docstring); - assert_eq!( - raises(&result)[0].r#type().text(result.source()), - "ValueError" - ); + assert_eq!(raises(&result)[0].r#type().text(result.source()), "ValueError"); } /// Raises entry with no space after colon. @@ -58,10 +51,7 @@ fn test_raises_no_space_after_colon() { let result = parse_google(docstring); let r = raises(&result); assert_eq!(r[0].r#type().text(result.source()), "ValueError"); - assert_eq!( - r[0].description().unwrap().text(result.source()), - "If invalid." - ); + assert_eq!(r[0].description().unwrap().text(result.source()), "If invalid."); } /// Raises entry with extra spaces after colon. @@ -71,10 +61,7 @@ fn test_raises_extra_spaces_after_colon() { let result = parse_google(docstring); let r = raises(&result); assert_eq!(r[0].r#type().text(result.source()), "ValueError"); - assert_eq!( - r[0].description().unwrap().text(result.source()), - "If invalid." - ); + assert_eq!(r[0].description().unwrap().text(result.source()), "If invalid."); } #[test] @@ -84,13 +71,7 @@ fn test_raise_alias() { let r = raises(&result); assert_eq!(r.len(), 1); assert_eq!(r[0].r#type().text(result.source()), "ValueError"); - assert_eq!( - all_sections(&result)[0] - .header() - .name() - .text(result.source()), - "Raise" - ); + assert_eq!(all_sections(&result)[0].header().name().text(result.source()), "Raise"); } #[test] @@ -112,27 +93,17 @@ fn test_warns_basic() { let result = parse_google(docstring); let w = warns(&result); assert_eq!(w.len(), 1); - assert_eq!( - w[0].warning_type().text(result.source()), - "DeprecationWarning" - ); - assert_eq!( - w[0].description().unwrap().text(result.source()), - "If using old API." - ); + assert_eq!(w[0].warning_type().text(result.source()), "DeprecationWarning"); + assert_eq!(w[0].description().unwrap().text(result.source()), "If using old API."); } #[test] fn test_warns_multiple() { - let docstring = - "Summary.\n\nWarns:\n DeprecationWarning: Old API.\n UserWarning: Bad config."; + let docstring = "Summary.\n\nWarns:\n DeprecationWarning: Old API.\n UserWarning: Bad config."; let result = parse_google(docstring); let w = warns(&result); assert_eq!(w.len(), 2); - assert_eq!( - w[0].warning_type().text(result.source()), - "DeprecationWarning" - ); + assert_eq!(w[0].warning_type().text(result.source()), "DeprecationWarning"); assert_eq!(w[1].warning_type().text(result.source()), "UserWarning"); } @@ -141,13 +112,7 @@ fn test_warn_alias() { let docstring = "Summary.\n\nWarn:\n FutureWarning: Will change."; let result = parse_google(docstring); assert_eq!(warns(&result).len(), 1); - assert_eq!( - all_sections(&result)[0] - .header() - .name() - .text(result.source()), - "Warn" - ); + assert_eq!(all_sections(&result)[0].header().name().text(result.source()), "Warn"); } #[test] @@ -155,10 +120,7 @@ fn test_warns_multiline_description() { let docstring = "Summary.\n\nWarns:\n UserWarning: First line.\n Second line."; let result = parse_google(docstring); assert_eq!( - warns(&result)[0] - .description() - .unwrap() - .text(result.source()), + warns(&result)[0].description().unwrap().text(result.source()), "First line.\n Second line." ); } @@ -168,10 +130,7 @@ fn test_warns_section_body_variant() { let docstring = "Summary.\n\nWarns:\n UserWarning: Desc."; let result = parse_google(docstring); let sections = all_sections(&result); - assert_eq!( - sections[0].section_kind(result.source()), - GoogleSectionKind::Warns - ); + assert_eq!(sections[0].section_kind(result.source()), GoogleSectionKind::Warns); assert_eq!(sections[0].warnings().count(), 1); } @@ -183,15 +142,9 @@ fn test_warns_section_body_variant() { fn test_warning_singular_alias() { let docstring = "Summary.\n\nWarning:\n This is deprecated."; let result = parse_google(docstring); + assert_eq!(warnings(&result).unwrap().text(result.source()), "This is deprecated."); assert_eq!( - warnings(&result).unwrap().text(result.source()), - "This is deprecated." - ); - assert_eq!( - all_sections(&result)[0] - .header() - .name() - .text(result.source()), + all_sections(&result)[0].header().name().text(result.source()), "Warning" ); } diff --git a/tests/google/returns.rs b/tests/google/returns.rs index ba9a8f1..063656e 100644 --- a/tests/google/returns.rs +++ b/tests/google/returns.rs @@ -10,10 +10,7 @@ fn test_returns_with_type() { let result = parse_google(docstring); let r = returns(&result).unwrap(); assert_eq!(r.return_type().unwrap().text(result.source()), "int"); - assert_eq!( - r.description().unwrap().text(result.source()), - "The result." - ); + assert_eq!(r.description().unwrap().text(result.source()), "The result."); } #[test] @@ -34,10 +31,7 @@ fn test_returns_without_type() { let result = parse_google(docstring); let r = returns(&result).unwrap(); assert!(r.return_type().is_none()); - assert_eq!( - r.description().unwrap().text(result.source()), - "The computed result." - ); + assert_eq!(r.description().unwrap().text(result.source()), "The computed result."); } #[test] @@ -45,11 +39,7 @@ fn test_returns_multiline_description() { let docstring = "Summary.\n\nReturns:\n int: The result\n of the computation."; let result = parse_google(docstring); assert_eq!( - returns(&result) - .unwrap() - .description() - .unwrap() - .text(result.source()), + returns(&result).unwrap().description().unwrap().text(result.source()), "The result\n of the computation." ); } @@ -68,10 +58,7 @@ fn test_returns_no_space_after_colon() { let result = parse_google(docstring); let r = returns(&result).unwrap(); assert_eq!(r.return_type().unwrap().text(result.source()), "int"); - assert_eq!( - r.description().unwrap().text(result.source()), - "The result." - ); + assert_eq!(r.description().unwrap().text(result.source()), "The result."); } /// Returns entry with extra spaces after colon. @@ -81,10 +68,7 @@ fn test_returns_extra_spaces_after_colon() { let result = parse_google(docstring); let r = returns(&result).unwrap(); assert_eq!(r.return_type().unwrap().text(result.source()), "int"); - assert_eq!( - r.description().unwrap().text(result.source()), - "The result." - ); + assert_eq!(r.description().unwrap().text(result.source()), "The result."); } #[test] @@ -105,10 +89,7 @@ fn test_yields() { let result = parse_google(docstring); let y = yields(&result).unwrap(); assert_eq!(y.return_type().unwrap().text(result.source()), "int"); - assert_eq!( - y.description().unwrap().text(result.source()), - "The next value." - ); + assert_eq!(y.description().unwrap().text(result.source()), "The next value."); } #[test] diff --git a/tests/google/sections.rs b/tests/google/sections.rs index 929b051..b66c9fb 100644 --- a/tests/google/sections.rs +++ b/tests/google/sections.rs @@ -72,10 +72,7 @@ fn test_section_header_span() { let result = parse_google(docstring); let header = all_sections(&result)[0].header(); assert_eq!(header.name().text(result.source()), "Args"); - assert_eq!( - header.syntax().range().source_text(result.source()), - "Args:" - ); + assert_eq!(header.syntax().range().source_text(result.source()), "Args:"); } #[test] @@ -100,10 +97,7 @@ fn test_unknown_section_preserved() { let sections = all_sections(&result); assert_eq!(sections.len(), 1); assert_eq!(sections[0].header().name().text(result.source()), "Custom"); - assert_eq!( - sections[0].section_kind(result.source()), - GoogleSectionKind::Unknown - ); + assert_eq!(sections[0].section_kind(result.source()), GoogleSectionKind::Unknown); assert_eq!( sections[0].body_text().unwrap().text(result.source()), "Some custom content." @@ -112,8 +106,7 @@ fn test_unknown_section_preserved() { #[test] fn test_unknown_section_with_known() { - let docstring = - "Summary.\n\nArgs:\n x: Value.\n\nCustom:\n Content.\n\nReturns:\n int: Result."; + let docstring = "Summary.\n\nArgs:\n x: Value.\n\nCustom:\n Content.\n\nReturns:\n int: Result."; let result = parse_google(docstring); let sections = all_sections(&result); assert_eq!(sections.len(), 3); @@ -130,14 +123,8 @@ fn test_multiple_unknown_sections() { let result = parse_google(docstring); let sections = all_sections(&result); assert_eq!(sections.len(), 2); - assert_eq!( - sections[0].header().name().text(result.source()), - "Custom One" - ); - assert_eq!( - sections[1].header().name().text(result.source()), - "Custom Two" - ); + assert_eq!(sections[0].header().name().text(result.source()), "Custom One"); + assert_eq!(sections[1].header().name().text(result.source()), "Custom Two"); } // ============================================================================= @@ -218,21 +205,11 @@ fn test_span_source_text_round_trip() { let docstring = "Summary.\n\nArgs:\n x (int): Value.\n\nReturns:\n bool: Success."; let result = parse_google(docstring); - assert_eq!( - doc(&result).summary().unwrap().text(result.source()), - "Summary." - ); + assert_eq!(doc(&result).summary().unwrap().text(result.source()), "Summary."); assert_eq!(args(&result)[0].name().text(result.source()), "x"); + assert_eq!(args(&result)[0].r#type().unwrap().text(result.source()), "int"); assert_eq!( - args(&result)[0].r#type().unwrap().text(result.source()), - "int" - ); - assert_eq!( - returns(&result) - .unwrap() - .return_type() - .unwrap() - .text(result.source()), + returns(&result).unwrap().return_type().unwrap().text(result.source()), "bool" ); } diff --git a/tests/google/structured.rs b/tests/google/structured.rs index 7d492dc..5c8d6b8 100644 --- a/tests/google/structured.rs +++ b/tests/google/structured.rs @@ -30,10 +30,7 @@ fn test_attribute_singular_alias() { let result = parse_google(docstring); assert_eq!(attributes(&result).len(), 1); assert_eq!( - all_sections(&result)[0] - .header() - .name() - .text(result.source()), + all_sections(&result)[0].header().name().text(result.source()), "Attribute" ); } @@ -49,10 +46,7 @@ fn test_methods_basic() { let m = methods(&result); assert_eq!(m.len(), 2); assert_eq!(m[0].name().text(result.source()), "reset()"); - assert_eq!( - m[0].description().unwrap().text(result.source()), - "Reset the state." - ); + assert_eq!(m[0].description().unwrap().text(result.source()), "Reset the state."); assert_eq!(m[1].name().text(result.source()), "update(data)"); } @@ -74,10 +68,7 @@ fn test_methods_section_body_variant() { let docstring = "Summary.\n\nMethods:\n foo(): Does bar."; let result = parse_google(docstring); let sections = all_sections(&result); - assert_eq!( - sections[0].section_kind(result.source()), - GoogleSectionKind::Methods - ); + assert_eq!(sections[0].section_kind(result.source()), GoogleSectionKind::Methods); assert_eq!(sections[0].methods().count(), 1); } @@ -133,9 +124,6 @@ fn test_see_also_section_body_variant() { let docstring = "Summary.\n\nSee Also:\n func_a: Desc."; let result = parse_google(docstring); let sections = all_sections(&result); - assert_eq!( - sections[0].section_kind(result.source()), - GoogleSectionKind::SeeAlso - ); + assert_eq!(sections[0].section_kind(result.source()), GoogleSectionKind::SeeAlso); assert_eq!(sections[0].see_also_items().count(), 1); } diff --git a/tests/google/summary.rs b/tests/google/summary.rs index eeddbc3..8f6ac0c 100644 --- a/tests/google/summary.rs +++ b/tests/google/summary.rs @@ -38,14 +38,10 @@ fn test_whitespace_only_docstring() { #[test] fn test_summary_with_description() { - let docstring = - "Brief summary.\n\nExtended description that provides\nmore details about the function."; + let docstring = "Brief summary.\n\nExtended description that provides\nmore details about the function."; let result = parse_google(docstring); - assert_eq!( - doc(&result).summary().unwrap().text(result.source()), - "Brief summary." - ); + assert_eq!(doc(&result).summary().unwrap().text(result.source()), "Brief summary."); let desc = doc(&result).extended_summary().unwrap(); assert_eq!( desc.text(result.source()), @@ -61,10 +57,7 @@ First paragraph of description. Second paragraph of description."#; let result = parse_google(docstring); - assert_eq!( - doc(&result).summary().unwrap().text(result.source()), - "Brief summary." - ); + assert_eq!(doc(&result).summary().unwrap().text(result.source()), "Brief summary."); let desc = doc(&result).extended_summary().unwrap(); assert!(desc.text(result.source()).contains("First paragraph")); assert!(desc.text(result.source()).contains("Second paragraph")); @@ -116,10 +109,7 @@ fn test_section_only_no_summary() { fn test_leading_blank_lines() { let docstring = "\n\n\nSummary.\n\nArgs:\n x: Value."; let result = parse_google(docstring); - assert_eq!( - doc(&result).summary().unwrap().text(result.source()), - "Summary." - ); + assert_eq!(doc(&result).summary().unwrap().text(result.source()), "Summary."); assert_eq!(args(&result).len(), 1); } @@ -127,8 +117,5 @@ fn test_leading_blank_lines() { fn test_docstring_like_summary() { let docstring = "Summary."; let result = parse_google(docstring); - assert_eq!( - doc(&result).summary().unwrap().text(result.source()), - "Summary." - ); + assert_eq!(doc(&result).summary().unwrap().text(result.source()), "Summary."); } diff --git a/tests/model.rs b/tests/model.rs index e02719b..e28ed97 100644 --- a/tests/model.rs +++ b/tests/model.rs @@ -227,8 +227,7 @@ fn numpy_parameters_basic() { #[test] fn numpy_parameters_optional() { - let parsed = - parse_numpy("Summary.\n\nParameters\n----------\nx : int, optional\n The value."); + let parsed = parse_numpy("Summary.\n\nParameters\n----------\nx : int, optional\n The value."); let doc = numpy_to_model(&parsed).unwrap(); match &doc.sections[0] { Section::Parameters(params) => { @@ -252,8 +251,7 @@ fn numpy_parameters_multiple_names() { #[test] fn numpy_parameters_default_value() { - let parsed = - parse_numpy("Summary.\n\nParameters\n----------\nx : int, default: 0\n The value."); + let parsed = parse_numpy("Summary.\n\nParameters\n----------\nx : int, default: 0\n The value."); let doc = numpy_to_model(&parsed).unwrap(); match &doc.sections[0] { Section::Parameters(params) => { @@ -388,8 +386,7 @@ fn numpy_google_style_entry_to_model() { #[test] fn numpy_google_style_optional_to_model() { - let parsed = - parse_numpy("Summary.\n\nParameters\n----------\nname (str, optional): The name.\n"); + let parsed = parse_numpy("Summary.\n\nParameters\n----------\nname (str, optional): The name.\n"); let doc = numpy_to_model(&parsed).unwrap(); let params = match &doc.sections[0] { Section::Parameters(p) => p, diff --git a/tests/numpy/edge_cases.rs b/tests/numpy/edge_cases.rs index 8828e79..6ef8bd8 100644 --- a/tests/numpy/edge_cases.rs +++ b/tests/numpy/edge_cases.rs @@ -9,17 +9,12 @@ fn test_indented_docstring() { let docstring = " Summary line.\n\n Parameters\n ----------\n x : int\n Description of x.\n y : str, optional\n Description of y.\n\n Returns\n -------\n bool\n The result.\n"; let result = parse_numpy(docstring); - assert_eq!( - doc(&result).summary().unwrap().text(result.source()), - "Summary line." - ); + assert_eq!(doc(&result).summary().unwrap().text(result.source()), "Summary line."); assert_eq!(parameters(&result).len(), 2); let names0: Vec<_> = parameters(&result)[0].names().collect(); assert_eq!(names0[0].text(result.source()), "x"); assert_eq!( - parameters(&result)[0] - .r#type() - .map(|t| t.text(result.source())), + parameters(&result)[0].r#type().map(|t| t.text(result.source())), Some("int") ); let names1: Vec<_> = parameters(&result)[1].names().collect(); @@ -27,25 +22,14 @@ fn test_indented_docstring() { assert!(parameters(&result)[1].optional().is_some()); assert_eq!(returns(&result).len(), 1); assert_eq!( - returns(&result)[0] - .return_type() - .map(|t| t.text(result.source())), + returns(&result)[0].return_type().map(|t| t.text(result.source())), Some("bool") ); - assert_eq!( - doc(&result).summary().unwrap().text(result.source()), - "Summary line." - ); + assert_eq!(doc(&result).summary().unwrap().text(result.source()), "Summary line."); let names0b: Vec<_> = parameters(&result)[0].names().collect(); assert_eq!(names0b[0].text(result.source()), "x"); - assert_eq!( - parameters(&result)[0] - .r#type() - .unwrap() - .text(result.source()), - "int" - ); + assert_eq!(parameters(&result)[0].r#type().unwrap().text(result.source()), "int"); } #[test] @@ -53,22 +37,13 @@ fn test_deeply_indented_docstring() { let docstring = " Brief.\n\n Parameters\n ----------\n a : float\n The value.\n\n Raises\n ------\n ValueError\n If bad.\n"; let result = parse_numpy(docstring); - assert_eq!( - doc(&result).summary().unwrap().text(result.source()), - "Brief." - ); + assert_eq!(doc(&result).summary().unwrap().text(result.source()), "Brief."); assert_eq!(parameters(&result).len(), 1); let names: Vec<_> = parameters(&result)[0].names().collect(); assert_eq!(names[0].text(result.source()), "a"); assert_eq!(raises(&result).len(), 1); - assert_eq!( - raises(&result)[0].r#type().text(result.source()), - "ValueError" - ); - assert_eq!( - raises(&result)[0].r#type().text(result.source()), - "ValueError" - ); + assert_eq!(raises(&result)[0].r#type().text(result.source()), "ValueError"); + assert_eq!(raises(&result)[0].r#type().text(result.source()), "ValueError"); } #[test] @@ -76,18 +51,10 @@ fn test_indented_with_deprecation() { let docstring = " Summary.\n\n .. deprecated:: 2.0.0\n Use new_func instead.\n\n Parameters\n ----------\n x : int\n Desc.\n"; let result = parse_numpy(docstring); - assert_eq!( - doc(&result).summary().unwrap().text(result.source()), - "Summary." - ); + assert_eq!(doc(&result).summary().unwrap().text(result.source()), "Summary."); let dep = doc(&result).deprecation().expect("should have deprecation"); assert_eq!(dep.version().text(result.source()), "2.0.0"); - assert!( - dep.description() - .unwrap() - .text(result.source()) - .contains("new_func") - ); + assert!(dep.description().unwrap().text(result.source()).contains("new_func")); assert_eq!(parameters(&result).len(), 1); let names: Vec<_> = parameters(&result)[0].names().collect(); assert_eq!(names[0].text(result.source()), "x"); @@ -95,22 +62,15 @@ fn test_indented_with_deprecation() { #[test] fn test_mixed_indent_first_line() { - let docstring = - "Summary.\n\n Parameters\n ----------\n x : int\n Description.\n"; + let docstring = "Summary.\n\n Parameters\n ----------\n x : int\n Description.\n"; let result = parse_numpy(docstring); - assert_eq!( - doc(&result).summary().unwrap().text(result.source()), - "Summary." - ); + assert_eq!(doc(&result).summary().unwrap().text(result.source()), "Summary."); assert_eq!(parameters(&result).len(), 1); let names: Vec<_> = parameters(&result)[0].names().collect(); assert_eq!(names[0].text(result.source()), "x"); assert_eq!( - parameters(&result)[0] - .description() - .unwrap() - .text(result.source()), + parameters(&result)[0].description().unwrap().text(result.source()), "Description." ); } diff --git a/tests/numpy/freetext.rs b/tests/numpy/freetext.rs index 119bdc4..5aa99d2 100644 --- a/tests/numpy/freetext.rs +++ b/tests/numpy/freetext.rs @@ -15,12 +15,7 @@ This is an important note about the function. let result = parse_numpy(docstring); assert!(notes(&result).is_some()); - assert!( - notes(&result) - .unwrap() - .text(result.source()) - .contains("important note") - ); + assert!(notes(&result).unwrap().text(result.source()).contains("important note")); } /// `Note` alias for Notes. @@ -29,13 +24,7 @@ fn test_note_alias() { let docstring = "Summary.\n\nNote\n----\nThis is a note.\n"; let result = parse_numpy(docstring); assert!(notes(&result).is_some()); - assert_eq!( - all_sections(&result)[0] - .header() - .name() - .text(result.source()), - "Note" - ); + assert_eq!(all_sections(&result)[0].header().name().text(result.source()), "Note"); assert_eq!( all_sections(&result)[0].section_kind(result.source()), NumPySectionKind::Notes @@ -73,10 +62,7 @@ fn test_warning_alias() { let result = parse_numpy(docstring); assert!(warnings_text(&result).is_some()); assert_eq!( - all_sections(&result)[0] - .header() - .name() - .text(result.source()), + all_sections(&result)[0].header().name().text(result.source()), "Warning" ); assert_eq!( @@ -115,10 +101,7 @@ fn test_example_alias() { let result = parse_numpy(docstring); assert!(examples(&result).is_some()); assert_eq!( - all_sections(&result)[0] - .header() - .name() - .text(result.source()), + all_sections(&result)[0].header().name().text(result.source()), "Example" ); assert_eq!( @@ -196,17 +179,13 @@ fn test_see_also_no_space_before_colon() { /// See Also with multiple items with descriptions. #[test] fn test_see_also_multiple_with_descriptions() { - let docstring = - "Summary.\n\nSee Also\n--------\nfunc_a : First function.\nfunc_b : Second function.\n"; + let docstring = "Summary.\n\nSee Also\n--------\nfunc_a : First function.\nfunc_b : Second function.\n"; let result = parse_numpy(docstring); let sa = see_also(&result); assert_eq!(sa.len(), 2); let names0: Vec<_> = sa[0].names().collect(); assert_eq!(names0[0].text(result.source()), "func_a"); - assert_eq!( - sa[0].description().unwrap().text(result.source()), - "First function." - ); + assert_eq!(sa[0].description().unwrap().text(result.source()), "First function."); let names1: Vec<_> = sa[1].names().collect(); assert_eq!(names1[0].text(result.source()), "func_b"); } @@ -239,21 +218,9 @@ References let refs = references(&result); assert_eq!(refs.len(), 2); assert_eq!(refs[0].number().unwrap().text(result.source()), "1"); - assert!( - refs[0] - .content() - .unwrap() - .text(result.source()) - .contains("Author A") - ); + assert!(refs[0].content().unwrap().text(result.source()).contains("Author A")); assert_eq!(refs[1].number().unwrap().text(result.source()), "2"); - assert!( - refs[1] - .content() - .unwrap() - .text(result.source()) - .contains("Author B") - ); + assert!(refs[1].content().unwrap().text(result.source()).contains("Author B")); } /// References with directive markers. @@ -264,10 +231,7 @@ fn test_references_directive_markers() { let refs = references(&result); assert_eq!(refs.len(), 1); assert!(refs[0].directive_marker().is_some()); - assert_eq!( - refs[0].directive_marker().unwrap().text(result.source()), - ".." - ); + assert_eq!(refs[0].directive_marker().unwrap().text(result.source()), ".."); assert!(refs[0].open_bracket().is_some()); assert!(refs[0].close_bracket().is_some()); } diff --git a/tests/numpy/main.rs b/tests/numpy/main.rs index 11be074..510b7d2 100644 --- a/tests/numpy/main.rs +++ b/tests/numpy/main.rs @@ -3,8 +3,8 @@ pub use pydocstring::parse::numpy::{ kind::NumPySectionKind, nodes::{ - NumPyAttribute, NumPyDeprecation, NumPyDocstring, NumPyException, NumPyMethod, - NumPyParameter, NumPyReference, NumPyReturns, NumPySection, NumPySeeAlsoItem, NumPyWarning, + NumPyAttribute, NumPyDeprecation, NumPyDocstring, NumPyException, NumPyMethod, NumPyParameter, NumPyReference, + NumPyReturns, NumPySection, NumPySeeAlsoItem, NumPyWarning, }, parse_numpy, }; @@ -37,12 +37,7 @@ pub fn all_sections<'a>(result: &'a Parsed) -> Vec> { pub fn parameters<'a>(result: &'a Parsed) -> Vec> { doc(result) .sections() - .filter(|s| { - matches!( - s.section_kind(result.source()), - NumPySectionKind::Parameters - ) - }) + .filter(|s| matches!(s.section_kind(result.source()), NumPySectionKind::Parameters)) .flat_map(|s| s.parameters().collect::>()) .collect() } @@ -90,12 +85,7 @@ pub fn see_also<'a>(result: &'a Parsed) -> Vec> { pub fn references<'a>(result: &'a Parsed) -> Vec> { doc(result) .sections() - .filter(|s| { - matches!( - s.section_kind(result.source()), - NumPySectionKind::References - ) - }) + .filter(|s| matches!(s.section_kind(result.source()), NumPySectionKind::References)) .flat_map(|s| s.references().collect::>()) .collect() } @@ -125,12 +115,7 @@ pub fn receives<'a>(result: &'a Parsed) -> Vec> { pub fn other_parameters<'a>(result: &'a Parsed) -> Vec> { doc(result) .sections() - .filter(|s| { - matches!( - s.section_kind(result.source()), - NumPySectionKind::OtherParameters - ) - }) + .filter(|s| matches!(s.section_kind(result.source()), NumPySectionKind::OtherParameters)) .flat_map(|s| s.parameters().collect::>()) .collect() } @@ -138,12 +123,7 @@ pub fn other_parameters<'a>(result: &'a Parsed) -> Vec> { pub fn attributes<'a>(result: &'a Parsed) -> Vec> { doc(result) .sections() - .filter(|s| { - matches!( - s.section_kind(result.source()), - NumPySectionKind::Attributes - ) - }) + .filter(|s| matches!(s.section_kind(result.source()), NumPySectionKind::Attributes)) .flat_map(|s| s.attributes().collect::>()) .collect() } diff --git a/tests/numpy/parameters.rs b/tests/numpy/parameters.rs index d78b442..295393a 100644 --- a/tests/numpy/parameters.rs +++ b/tests/numpy/parameters.rs @@ -31,33 +31,24 @@ int let names0: Vec<_> = parameters(&result)[0].names().collect(); assert_eq!(names0[0].text(result.source()), "x"); assert_eq!( - parameters(&result)[0] - .r#type() - .map(|t| t.text(result.source())), + parameters(&result)[0].r#type().map(|t| t.text(result.source())), Some("int") ); assert_eq!( - parameters(&result)[0] - .description() - .unwrap() - .text(result.source()), + parameters(&result)[0].description().unwrap().text(result.source()), "The first number." ); let names1: Vec<_> = parameters(&result)[1].names().collect(); assert_eq!(names1[0].text(result.source()), "y"); assert_eq!( - parameters(&result)[1] - .r#type() - .map(|t| t.text(result.source())), + parameters(&result)[1].r#type().map(|t| t.text(result.source())), Some("int") ); assert!(!returns(&result).is_empty()); assert_eq!( - returns(&result)[0] - .return_type() - .map(|t| t.text(result.source())), + returns(&result)[0].return_type().map(|t| t.text(result.source())), Some("int") ); } @@ -79,9 +70,7 @@ optional : int, optional assert!(parameters(&result)[0].optional().is_none()); assert!(parameters(&result)[1].optional().is_some()); assert_eq!( - parameters(&result)[1] - .r#type() - .map(|t| t.text(result.source())), + parameters(&result)[1].r#type().map(|t| t.text(result.source())), Some("int") ); } @@ -104,13 +93,7 @@ y : str, optional assert_eq!(names0[0].text(result.source()), "x"); let names1: Vec<_> = parameters(&result)[1].names().collect(); assert_eq!(names1[0].text(result.source()), "y"); - assert_eq!( - parameters(&result)[0] - .r#type() - .unwrap() - .text(result.source()), - "int" - ); + assert_eq!(parameters(&result)[0].r#type().unwrap().text(result.source()), "int"); } /// Parameters with no space before colon: `x: int` @@ -123,10 +106,7 @@ fn test_parameters_no_space_before_colon() { let names: Vec<_> = p[0].names().collect(); assert_eq!(names[0].text(result.source()), "x"); assert_eq!(p[0].r#type().unwrap().text(result.source()), "int"); - assert_eq!( - p[0].description().unwrap().text(result.source()), - "The value." - ); + assert_eq!(p[0].description().unwrap().text(result.source()), "The value."); } /// Parameters with no space after colon: `x :int` @@ -204,10 +184,7 @@ x : int Second paragraph of x. "#; let result = parse_numpy(docstring); - let desc = parameters(&result)[0] - .description() - .unwrap() - .text(result.source()); + let desc = parameters(&result)[0].description().unwrap().text(result.source()); assert!(desc.contains("First paragraph of x.")); assert!(desc.contains("Second paragraph of x.")); assert!(desc.contains('\n')); @@ -219,8 +196,7 @@ x : int #[test] fn test_enum_type_as_string() { - let docstring = - "Summary.\n\nParameters\n----------\norder : {'C', 'F', 'A'}\n Memory layout."; + let docstring = "Summary.\n\nParameters\n----------\norder : {'C', 'F', 'A'}\n Memory layout."; let result = parse_numpy(docstring); let params = parameters(&result); assert_eq!(params.len(), 1); @@ -229,16 +205,12 @@ fn test_enum_type_as_string() { let names: Vec<_> = p.names().collect(); assert_eq!(names[0].text(result.source()), "order"); assert_eq!(p.r#type().unwrap().text(result.source()), "{'C', 'F', 'A'}"); - assert_eq!( - p.description().unwrap().text(result.source()), - "Memory layout." - ); + assert_eq!(p.description().unwrap().text(result.source()), "Memory layout."); } #[test] fn test_enum_type_with_optional() { - let docstring = - "Summary.\n\nParameters\n----------\norder : {'C', 'F'}, optional\n Memory layout."; + let docstring = "Summary.\n\nParameters\n----------\norder : {'C', 'F'}, optional\n Memory layout."; let result = parse_numpy(docstring); let params = parameters(&result); let p = ¶ms[0]; @@ -255,10 +227,7 @@ fn test_enum_type_with_default() { let p = ¶ms[0]; assert_eq!(p.r#type().unwrap().text(result.source()), "{'C', 'F', 'A'}"); - assert_eq!( - p.default_keyword().unwrap().text(result.source()), - "default" - ); + assert_eq!(p.default_keyword().unwrap().text(result.source()), "default"); assert!(p.default_separator().is_none()); assert_eq!(p.default_value().unwrap().text(result.source()), "'C'"); } @@ -276,13 +245,7 @@ fn test_params_alias() { assert_eq!(p.len(), 1); let names: Vec<_> = p[0].names().collect(); assert_eq!(names[0].text(result.source()), "x"); - assert_eq!( - all_sections(&result)[0] - .header() - .name() - .text(result.source()), - "Params" - ); + assert_eq!(all_sections(&result)[0].header().name().text(result.source()), "Params"); assert_eq!( all_sections(&result)[0].section_kind(result.source()), NumPySectionKind::Parameters @@ -295,13 +258,7 @@ fn test_param_alias() { let docstring = "Summary.\n\nParam\n-----\nx : int\n The value.\n"; let result = parse_numpy(docstring); assert_eq!(parameters(&result).len(), 1); - assert_eq!( - all_sections(&result)[0] - .header() - .name() - .text(result.source()), - "Param" - ); + assert_eq!(all_sections(&result)[0].header().name().text(result.source()), "Param"); } /// `Parameter` alias for Parameters. @@ -311,10 +268,7 @@ fn test_parameter_alias() { let result = parse_numpy(docstring); assert_eq!(parameters(&result).len(), 1); assert_eq!( - all_sections(&result)[0] - .header() - .name() - .text(result.source()), + all_sections(&result)[0].header().name().text(result.source()), "Parameter" ); } @@ -344,10 +298,7 @@ fn test_other_params_alias() { let result = parse_numpy(docstring); assert_eq!(other_parameters(&result).len(), 1); assert_eq!( - all_sections(&result)[0] - .header() - .name() - .text(result.source()), + all_sections(&result)[0].header().name().text(result.source()), "Other Params" ); assert_eq!( @@ -362,10 +313,7 @@ fn test_other_parameters_section_body_variant() { let docstring = "Summary.\n\nOther Parameters\n----------------\nx : int\n Extra.\n"; let result = parse_numpy(docstring); let s = &all_sections(&result)[0]; - assert_eq!( - s.section_kind(result.source()), - NumPySectionKind::OtherParameters - ); + assert_eq!(s.section_kind(result.source()), NumPySectionKind::OtherParameters); let params: Vec<_> = s.parameters().collect(); assert_eq!(params.len(), 1); } @@ -383,10 +331,7 @@ fn test_receives_basic() { let names: Vec<_> = r[0].names().collect(); assert_eq!(names[0].text(result.source()), "data"); assert_eq!(r[0].r#type().unwrap().text(result.source()), "bytes"); - assert_eq!( - r[0].description().unwrap().text(result.source()), - "The received data." - ); + assert_eq!(r[0].description().unwrap().text(result.source()), "The received data."); } #[test] @@ -408,10 +353,7 @@ fn test_receive_alias() { let result = parse_numpy(docstring); assert_eq!(receives(&result).len(), 1); assert_eq!( - all_sections(&result)[0] - .header() - .name() - .text(result.source()), + all_sections(&result)[0].header().name().text(result.source()), "Receive" ); assert_eq!( @@ -444,10 +386,7 @@ fn test_google_style_entry_in_numpy_section() { let names: Vec<_> = params[0].names().collect(); assert_eq!(names[0].text(result.source()), "name"); - assert_eq!( - params[0].r#type().map(|t| t.text(result.source())), - Some("str") - ); + assert_eq!(params[0].r#type().map(|t| t.text(result.source())), Some("str")); assert_eq!( params[0].description().map(|t| t.text(result.source())), Some("The name.") @@ -463,10 +402,7 @@ fn test_google_style_entry_with_optional() { let names: Vec<_> = params[0].names().collect(); assert_eq!(names[0].text(result.source()), "name"); - assert_eq!( - params[0].r#type().map(|t| t.text(result.source())), - Some("str") - ); + assert_eq!(params[0].r#type().map(|t| t.text(result.source())), Some("str")); assert!(params[0].optional().is_some()); assert_eq!( params[0].description().map(|t| t.text(result.source())), @@ -483,25 +419,18 @@ fn test_google_style_entry_no_description() { let names: Vec<_> = params[0].names().collect(); assert_eq!(names[0].text(result.source()), "name"); - assert_eq!( - params[0].r#type().map(|t| t.text(result.source())), - Some("int") - ); + assert_eq!(params[0].r#type().map(|t| t.text(result.source())), Some("int")); assert!(params[0].description().is_none()); } #[test] fn test_google_style_entry_with_continuation() { - let docstring = - "Summary.\n\nParameters\n----------\nname (str): The name.\n Continued here.\n"; + let docstring = "Summary.\n\nParameters\n----------\nname (str): The name.\n Continued here.\n"; let result = parse_numpy(docstring); let params = parameters(&result); assert_eq!(params.len(), 1); - assert_eq!( - params[0].r#type().map(|t| t.text(result.source())), - Some("str") - ); + assert_eq!(params[0].r#type().map(|t| t.text(result.source())), Some("str")); assert_eq!( params[0].description().map(|t| t.text(result.source())), Some("The name.\n Continued here.") @@ -534,21 +463,12 @@ fn test_google_style_mixed_with_numpy_style() { // Google-style entry assert_eq!(params[0].names().next().unwrap().text(result.source()), "x"); - assert_eq!( - params[0].r#type().map(|t| t.text(result.source())), - Some("int") - ); - assert_eq!( - params[0].description().map(|t| t.text(result.source())), - Some("First.") - ); + assert_eq!(params[0].r#type().map(|t| t.text(result.source())), Some("int")); + assert_eq!(params[0].description().map(|t| t.text(result.source())), Some("First.")); // NumPy-style entry assert_eq!(params[1].names().next().unwrap().text(result.source()), "y"); - assert_eq!( - params[1].r#type().map(|t| t.text(result.source())), - Some("str") - ); + assert_eq!(params[1].r#type().map(|t| t.text(result.source())), Some("str")); assert_eq!( params[1].description().map(|t| t.text(result.source())), Some("Second.") @@ -562,16 +482,7 @@ fn test_google_style_entry_no_colon_after_bracket() { let params = parameters(&result); assert_eq!(params.len(), 1); - assert_eq!( - params[0].names().next().unwrap().text(result.source()), - "name" - ); - assert_eq!( - params[0].r#type().map(|t| t.text(result.source())), - Some("int") - ); - assert_eq!( - params[0].description().map(|t| t.text(result.source())), - Some("Desc.") - ); + assert_eq!(params[0].names().next().unwrap().text(result.source()), "name"); + assert_eq!(params[0].r#type().map(|t| t.text(result.source())), Some("int")); + assert_eq!(params[0].description().map(|t| t.text(result.source())), Some("Desc.")); } diff --git a/tests/numpy/raises.rs b/tests/numpy/raises.rs index 42d40d1..2505a03 100644 --- a/tests/numpy/raises.rs +++ b/tests/numpy/raises.rs @@ -18,14 +18,8 @@ TypeError let result = parse_numpy(docstring); assert_eq!(raises(&result).len(), 2); - assert_eq!( - raises(&result)[0].r#type().text(result.source()), - "ValueError" - ); - assert_eq!( - raises(&result)[1].r#type().text(result.source()), - "TypeError" - ); + assert_eq!(raises(&result)[0].r#type().text(result.source()), "ValueError"); + assert_eq!(raises(&result)[1].r#type().text(result.source()), "TypeError"); } #[test] @@ -41,14 +35,8 @@ TypeError "#; let result = parse_numpy(docstring); assert_eq!(raises(&result).len(), 2); - assert_eq!( - raises(&result)[0].r#type().text(result.source()), - "ValueError" - ); - assert_eq!( - raises(&result)[1].r#type().text(result.source()), - "TypeError" - ); + assert_eq!(raises(&result)[0].r#type().text(result.source()), "ValueError"); + assert_eq!(raises(&result)[1].r#type().text(result.source()), "TypeError"); } // ============================================================================= @@ -58,7 +46,8 @@ TypeError /// Raises with colon separating type and description on the same line. #[test] fn test_raises_colon_split() { - let docstring = "Summary.\n\nRaises\n------\nValueError : If the input is invalid.\nTypeError : If the type is wrong."; + let docstring = + "Summary.\n\nRaises\n------\nValueError : If the input is invalid.\nTypeError : If the type is wrong."; let result = parse_numpy(docstring); let exc = raises(&result); assert_eq!(exc.len(), 2); @@ -112,13 +101,7 @@ fn test_raise_alias() { let result = parse_numpy(docstring); let exc = raises(&result); assert_eq!(exc.len(), 1); - assert_eq!( - all_sections(&result)[0] - .header() - .name() - .text(result.source()), - "Raise" - ); + assert_eq!(all_sections(&result)[0].header().name().text(result.source()), "Raise"); assert_eq!( all_sections(&result)[0].section_kind(result.source()), NumPySectionKind::Raises @@ -144,8 +127,7 @@ fn test_warns_basic() { #[test] fn test_warns_multiple() { - let docstring = - "Summary.\n\nWarns\n-----\nDeprecationWarning\n Old API.\nUserWarning\n Bad usage.\n"; + let docstring = "Summary.\n\nWarns\n-----\nDeprecationWarning\n Old API.\nUserWarning\n Bad usage.\n"; let result = parse_numpy(docstring); let w = warns(&result); assert_eq!(w.len(), 2); @@ -175,13 +157,7 @@ fn test_warn_alias() { let result = parse_numpy(docstring); let w = warns(&result); assert_eq!(w.len(), 1); - assert_eq!( - all_sections(&result)[0] - .header() - .name() - .text(result.source()), - "Warn" - ); + assert_eq!(all_sections(&result)[0].header().name().text(result.source()), "Warn"); assert_eq!( all_sections(&result)[0].section_kind(result.source()), NumPySectionKind::Warns diff --git a/tests/numpy/returns.rs b/tests/numpy/returns.rs index fd4b9ff..75702dd 100644 --- a/tests/numpy/returns.rs +++ b/tests/numpy/returns.rs @@ -17,27 +17,16 @@ y : float "#; let result = parse_numpy(docstring); assert_eq!(returns(&result).len(), 2); + assert_eq!(returns(&result)[0].name().map(|n| n.text(result.source())), Some("x")); assert_eq!( - returns(&result)[0].name().map(|n| n.text(result.source())), - Some("x") - ); - assert_eq!( - returns(&result)[0] - .return_type() - .map(|t| t.text(result.source())), + returns(&result)[0].return_type().map(|t| t.text(result.source())), Some("int") ); assert_eq!( - returns(&result)[0] - .description() - .unwrap() - .text(result.source()), + returns(&result)[0].description().unwrap().text(result.source()), "The first value." ); - assert_eq!( - returns(&result)[1].name().map(|n| n.text(result.source())), - Some("y") - ); + assert_eq!(returns(&result)[1].name().map(|n| n.text(result.source())), Some("y")); } /// Returns with no spaces around colon (named): `result:int` @@ -59,10 +48,7 @@ fn test_returns_type_only() { let r = returns(&result); assert_eq!(r.len(), 1); assert_eq!(r[0].return_type().unwrap().text(result.source()), "int"); - assert_eq!( - r[0].description().unwrap().text(result.source()), - "The result." - ); + assert_eq!(r[0].description().unwrap().text(result.source()), "The result."); } /// Returns — `Return` alias. @@ -72,13 +58,7 @@ fn test_return_alias() { let result = parse_numpy(docstring); let r = returns(&result); assert_eq!(r.len(), 1); - assert_eq!( - all_sections(&result)[0] - .header() - .name() - .text(result.source()), - "Return" - ); + assert_eq!(all_sections(&result)[0].header().name().text(result.source()), "Return"); assert_eq!( all_sections(&result)[0].section_kind(result.source()), NumPySectionKind::Returns @@ -88,8 +68,7 @@ fn test_return_alias() { /// Returns with multiline description. #[test] fn test_returns_multiline_description() { - let docstring = - "Summary.\n\nReturns\n-------\nresult : int\n First line.\n\n Second paragraph.\n"; + let docstring = "Summary.\n\nReturns\n-------\nresult : int\n First line.\n\n Second paragraph.\n"; let result = parse_numpy(docstring); let r = returns(&result); assert_eq!(r.len(), 1); @@ -109,10 +88,7 @@ fn test_yields_basic() { let y = yields(&result); assert_eq!(y.len(), 1); assert_eq!(y[0].return_type().unwrap().text(result.source()), "int"); - assert_eq!( - y[0].description().unwrap().text(result.source()), - "The next value." - ); + assert_eq!(y[0].description().unwrap().text(result.source()), "The next value."); } #[test] @@ -127,8 +103,7 @@ fn test_yields_named() { #[test] fn test_yields_multiple() { - let docstring = - "Summary.\n\nYields\n------\nindex : int\n The index.\nvalue : str\n The value.\n"; + let docstring = "Summary.\n\nYields\n------\nindex : int\n The index.\nvalue : str\n The value.\n"; let result = parse_numpy(docstring); let y = yields(&result); assert_eq!(y.len(), 2); @@ -143,13 +118,7 @@ fn test_yield_alias() { let result = parse_numpy(docstring); let y = yields(&result); assert_eq!(y.len(), 1); - assert_eq!( - all_sections(&result)[0] - .header() - .name() - .text(result.source()), - "Yield" - ); + assert_eq!(all_sections(&result)[0].header().name().text(result.source()), "Yield"); assert_eq!( all_sections(&result)[0].section_kind(result.source()), NumPySectionKind::Yields diff --git a/tests/numpy/sections.rs b/tests/numpy/sections.rs index 896b8a5..db4a55f 100644 --- a/tests/numpy/sections.rs +++ b/tests/numpy/sections.rs @@ -29,19 +29,10 @@ Some notes here. assert_eq!(returns(&result).len(), 1); assert!(notes(&result).is_some()); assert_eq!( - all_sections(&result)[0] - .header() - .name() - .text(result.source()), + all_sections(&result)[0].header().name().text(result.source()), "parameters" ); - assert_eq!( - all_sections(&result)[2] - .header() - .name() - .text(result.source()), - "NOTES" - ); + assert_eq!(all_sections(&result)[2].header().name().text(result.source()), "NOTES"); } // ============================================================================= @@ -80,14 +71,8 @@ x : int let src = result.source(); assert_eq!(doc(&result).summary().unwrap().text(src), "Summary line."); - assert_eq!( - all_sections(&result)[0].header().name().text(src), - "Parameters" - ); - let underline = all_sections(&result)[0] - .header() - .underline() - .text(result.source()); + assert_eq!(all_sections(&result)[0].header().name().text(src), "Parameters"); + let underline = all_sections(&result)[0].header().underline().text(result.source()); assert!(underline.chars().all(|c| c == '-')); let p = ¶meters(&result)[0]; @@ -114,9 +99,7 @@ x : int Desc. "#; let result = parse_numpy(docstring); - let dep = doc(&result) - .deprecation() - .expect("deprecation should be parsed"); + let dep = doc(&result).deprecation().expect("deprecation should be parsed"); assert_eq!(dep.version().text(result.source()), "1.6.0"); assert_eq!( dep.description().unwrap().text(result.source()), @@ -155,14 +138,8 @@ Some notes. let result = parse_numpy(docstring); let s = all_sections(&result); assert_eq!(s.len(), 4); - assert_eq!( - s[0].section_kind(result.source()), - NumPySectionKind::Parameters - ); - assert_eq!( - s[1].section_kind(result.source()), - NumPySectionKind::Returns - ); + assert_eq!(s[0].section_kind(result.source()), NumPySectionKind::Parameters); + assert_eq!(s[1].section_kind(result.source()), NumPySectionKind::Returns); assert_eq!(s[2].section_kind(result.source()), NumPySectionKind::Raises); assert_eq!(s[3].section_kind(result.source()), NumPySectionKind::Notes); } @@ -178,18 +155,14 @@ fn test_all_section_kinds_exist() { #[test] fn test_section_kind_from_name_unknown() { - assert_eq!( - NumPySectionKind::from_name("nonexistent"), - NumPySectionKind::Unknown - ); + assert_eq!(NumPySectionKind::from_name("nonexistent"), NumPySectionKind::Unknown); assert!(!NumPySectionKind::is_known("nonexistent")); assert!(NumPySectionKind::is_known("parameters")); } #[test] fn test_stray_lines() { - let docstring = - "Summary.\n\nThis line is not a section.\n\nParameters\n----------\nx : int\n Desc.\n"; + let docstring = "Summary.\n\nThis line is not a section.\n\nParameters\n----------\nx : int\n Desc.\n"; let result = parse_numpy(docstring); // The non-section line might be treated as extended summary or stray line // depending on parser behavior. Just verify parameters are still parsed. @@ -206,10 +179,7 @@ fn test_section_kind_display() { assert_eq!(format!("{}", NumPySectionKind::Returns), "Returns"); assert_eq!(format!("{}", NumPySectionKind::Yields), "Yields"); assert_eq!(format!("{}", NumPySectionKind::Receives), "Receives"); - assert_eq!( - format!("{}", NumPySectionKind::OtherParameters), - "Other Parameters" - ); + assert_eq!(format!("{}", NumPySectionKind::OtherParameters), "Other Parameters"); assert_eq!(format!("{}", NumPySectionKind::Raises), "Raises"); assert_eq!(format!("{}", NumPySectionKind::Warns), "Warns"); assert_eq!(format!("{}", NumPySectionKind::Warnings), "Warnings"); diff --git a/tests/numpy/structured.rs b/tests/numpy/structured.rs index 00af32a..da0b83a 100644 --- a/tests/numpy/structured.rs +++ b/tests/numpy/structured.rs @@ -6,17 +6,13 @@ use super::*; #[test] fn test_attributes_basic() { - let docstring = - "Summary.\n\nAttributes\n----------\nname : str\n The name.\nage : int\n The age.\n"; + let docstring = "Summary.\n\nAttributes\n----------\nname : str\n The name.\nage : int\n The age.\n"; let result = parse_numpy(docstring); let a = attributes(&result); assert_eq!(a.len(), 2); assert_eq!(a[0].name().text(result.source()), "name"); assert_eq!(a[0].r#type().unwrap().text(result.source()), "str"); - assert_eq!( - a[0].description().unwrap().text(result.source()), - "The name." - ); + assert_eq!(a[0].description().unwrap().text(result.source()), "The name."); assert_eq!(a[1].name().text(result.source()), "age"); assert_eq!(a[1].r#type().unwrap().text(result.source()), "int"); } @@ -29,10 +25,7 @@ fn test_attributes_no_type() { assert_eq!(a.len(), 1); assert_eq!(a[0].name().text(result.source()), "name"); assert!(a[0].r#type().is_none()); - assert_eq!( - a[0].description().unwrap().text(result.source()), - "The name." - ); + assert_eq!(a[0].description().unwrap().text(result.source()), "The name."); } #[test] @@ -51,10 +44,7 @@ fn test_attributes_section_body_variant() { let docstring = "Summary.\n\nAttributes\n----------\nx : int\n Value.\n"; let result = parse_numpy(docstring); let s = &all_sections(&result)[0]; - assert_eq!( - s.section_kind(result.source()), - NumPySectionKind::Attributes - ); + assert_eq!(s.section_kind(result.source()), NumPySectionKind::Attributes); let attrs: Vec<_> = s.attributes().collect(); assert_eq!(attrs.len(), 1); } @@ -76,15 +66,13 @@ fn test_attributes_section_kind() { #[test] fn test_methods_basic() { - let docstring = "Summary.\n\nMethods\n-------\nreset()\n Reset the state.\nupdate(data)\n Update with new data.\n"; + let docstring = + "Summary.\n\nMethods\n-------\nreset()\n Reset the state.\nupdate(data)\n Update with new data.\n"; let result = parse_numpy(docstring); let m = methods(&result); assert_eq!(m.len(), 2); assert_eq!(m[0].name().text(result.source()), "reset()"); - assert_eq!( - m[0].description().unwrap().text(result.source()), - "Reset the state." - ); + assert_eq!(m[0].description().unwrap().text(result.source()), "Reset the state."); assert_eq!(m[1].name().text(result.source()), "update(data)"); assert_eq!( m[1].description().unwrap().text(result.source()), @@ -151,10 +139,7 @@ fn test_unknown_section() { let result = parse_numpy(docstring); let s = all_sections(&result); assert_eq!(s.len(), 1); - assert_eq!( - s[0].section_kind(result.source()), - NumPySectionKind::Unknown - ); + assert_eq!(s[0].section_kind(result.source()), NumPySectionKind::Unknown); assert_eq!(s[0].header().name().text(result.source()), "CustomSection"); } @@ -166,26 +151,15 @@ fn test_unknown_section_body_variant() { assert_eq!(s.section_kind(result.source()), NumPySectionKind::Unknown); let text = s.body_text(); assert!(text.is_some()); - assert!( - text.unwrap() - .text(result.source()) - .contains("Some content.") - ); + assert!(text.unwrap().text(result.source()).contains("Some content.")); } #[test] fn test_unknown_section_with_known_sections() { - let docstring = - "Summary.\n\nParameters\n----------\nx : int\n Value.\n\nCustom\n------\nExtra info.\n"; + let docstring = "Summary.\n\nParameters\n----------\nx : int\n Value.\n\nCustom\n------\nExtra info.\n"; let result = parse_numpy(docstring); let s = all_sections(&result); assert_eq!(s.len(), 2); - assert_eq!( - s[0].section_kind(result.source()), - NumPySectionKind::Parameters - ); - assert_eq!( - s[1].section_kind(result.source()), - NumPySectionKind::Unknown - ); + assert_eq!(s[0].section_kind(result.source()), NumPySectionKind::Parameters); + assert_eq!(s[1].section_kind(result.source()), NumPySectionKind::Unknown); } diff --git a/tests/numpy/summary.rs b/tests/numpy/summary.rs index 98d5f97..71b22c2 100644 --- a/tests/numpy/summary.rs +++ b/tests/numpy/summary.rs @@ -25,14 +25,8 @@ fn test_parse_simple_span() { doc(&result).summary().unwrap().text(result.source()), "Brief description." ); - assert_eq!( - doc(&result).summary().unwrap().range().start(), - TextSize::new(0) - ); - assert_eq!( - doc(&result).summary().unwrap().range().end(), - TextSize::new(18) - ); + assert_eq!(doc(&result).summary().unwrap().range().start(), TextSize::new(0)); + assert_eq!(doc(&result).summary().unwrap().range().end(), TextSize::new(18)); assert_eq!( doc(&result).summary().unwrap().text(result.source()), "Brief description." @@ -48,10 +42,7 @@ more details about the function. "#; let result = parse_numpy(docstring); - assert_eq!( - doc(&result).summary().unwrap().text(result.source()), - "Brief summary." - ); + assert_eq!(doc(&result).summary().unwrap().text(result.source()), "Brief summary."); assert!(doc(&result).extended_summary().is_some()); } @@ -95,10 +86,7 @@ fn test_docstring_span_covers_entire_input() { let docstring = "First line.\n\nSecond line."; let result = parse_numpy(docstring); assert_eq!(doc(&result).syntax().range().start(), TextSize::new(0)); - assert_eq!( - doc(&result).syntax().range().end().raw() as usize, - docstring.len() - ); + assert_eq!(doc(&result).syntax().range().end().raw() as usize, docstring.len()); } // ============================================================================= @@ -119,10 +107,7 @@ b : int Second number. "#; let result = parse_numpy(docstring); - assert_eq!( - doc(&result).summary().unwrap().text(result.source()), - "add(a, b)" - ); + assert_eq!(doc(&result).summary().unwrap().text(result.source()), "add(a, b)"); assert_eq!(parameters(&result).len(), 2); }