use crate::cursor::{LineCursor, indent_len};
use crate::parse::google::kind::GoogleSectionKind;
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;
fn extract_section_name(trimmed: &str) -> (&str, bool) {
if let Some(stripped) = trimmed.strip_suffix(':') {
(stripped.trim_end(), true)
} else {
(trimmed, false)
}
}
struct TypeInfo {
open_bracket: TextRange,
r#type: Option<TextRange>,
close_bracket: Option<TextRange>,
optional: Option<TextRange>,
}
struct EntryHeader {
range: TextRange,
name: TextRange,
type_info: Option<TypeInfo>,
colon: Option<TextRange>,
first_description: Option<TextRange>,
}
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);
if parse_type {
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) => {
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(':') {
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() {
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 {
(&trimmed[bracket_pos + 1..cp], cb, None, None)
}
}
None => {
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 = opt_rel.map(|r| TextRange::from_offset_len(entry_start + type_offset + r, "optional".len()));
let type_info = Some(TypeInfo {
open_bracket,
r#type: type_span,
close_bracket,
optional: opt_span,
});
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),
name: name_span,
type_info,
colon,
first_description,
};
}
}
if let Some(colon_rel) = find_entry_colon(trimmed) {
let name = trimmed[..colon_rel].trim_end();
let after_colon = &trimmed[colon_rel + 1..];
let desc = after_colon.trim_start();
let ws_after = after_colon.len() - desc.len();
let desc_start = entry_start + colon_rel + 1 + ws_after;
let colon_span = TextRange::from_offset_len(entry_start + colon_rel, 1);
let first_description = if desc.is_empty() {
None
} else {
Some(TextRange::from_offset_len(desc_start, desc.len()))
};
let range_end = if let Some(ref d) = first_description {
d.end()
} else {
colon_span.end()
};
let name_span = TextRange::from_offset_len(entry_start, name.len());
return EntryHeader {
range: TextRange::new(name_span.start(), range_end),
name: name_span,
type_info: None,
colon: Some(colon_span),
first_description,
};
}
let name_span = TextRange::from_offset_len(entry_start, trimmed.len());
EntryHeader {
range: name_span,
name: name_span,
type_info: None,
colon: None,
first_description: None,
}
}
struct SectionHeaderInfo {
range: TextRange,
kind: GoogleSectionKind,
name: TextRange,
colon: Option<TextRange>,
indent_columns: usize,
}
fn try_parse_section_header(cursor: &LineCursor) -> Option<SectionHeaderInfo> {
let trimmed = cursor.current_trimmed();
let (name, has_colon) = extract_section_name(trimmed);
if name.is_empty() || !name.starts_with(|c: char| c.is_ascii_alphabetic()) {
return None;
}
let is_header = if has_colon {
!name.contains(':') && name.chars().all(|c| c.is_alphanumeric() || c.is_ascii_whitespace())
} else {
GoogleSectionKind::is_known(&name.to_ascii_lowercase())
};
if !is_header {
return None;
}
let col = cursor.current_indent();
let header_name = name.trim_end();
let colon = if has_colon {
let colon_col = col + trimmed.len() - 1;
Some(cursor.make_line_range(cursor.line, colon_col, 1))
} else {
None
};
let normalized = header_name.to_ascii_lowercase();
let kind = GoogleSectionKind::from_name(&normalized);
Some(SectionHeaderInfo {
range: cursor.current_trimmed_range(),
kind,
name: cursor.make_line_range(cursor.line, col, header_name.len()),
colon,
indent_columns: cursor.current_indent_columns(),
})
}
fn build_section_header_node(info: &SectionHeaderInfo) -> SyntaxNode {
let mut children = Vec::new();
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 {
children.push(SyntaxElement::Token(SyntaxToken::new(
SyntaxKind::COLON,
TextRange::new(info.name.end(), info.name.end()),
)));
}
SyntaxNode::new(SyntaxKind::GOOGLE_SECTION_HEADER, info.range, children)
}
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)));
if let Some(ti) = &header.type_info {
children.push(SyntaxElement::Token(SyntaxToken::new(
SyntaxKind::OPEN_BRACKET,
ti.open_bracket,
)));
if let Some(t) = ti.r#type {
children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::TYPE, t)));
} else {
let missing_pos = ti.open_bracket.end();
children.push(SyntaxElement::Token(SyntaxToken::new(
SyntaxKind::TYPE,
TextRange::new(missing_pos, missing_pos),
)));
}
if let Some(cb) = ti.close_bracket {
children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::CLOSE_BRACKET, cb)));
} else {
let missing_pos = ti.r#type.map(|t| t.end()).unwrap_or(ti.open_bracket.end());
children.push(SyntaxElement::Token(SyntaxToken::new(
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() {
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,
TextRange::new(missing_pos, missing_pos),
)));
}
if let Some(desc) = header.first_description {
children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::DESCRIPTION, desc)));
} else if let Some(colon) = header.colon {
children.push(SyntaxElement::Token(SyntaxToken::new(
SyntaxKind::DESCRIPTION,
TextRange::new(colon.end(), colon.end()),
)));
}
children.sort_by_key(|c| c.range().start());
SyntaxNode::new(kind, range, children)
}
fn build_exception_node(header: &EntryHeader, range: TextRange) -> SyntaxNode {
let mut children = Vec::new();
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)));
}
if let Some(desc) = header.first_description {
children.push(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::DESCRIPTION, desc)));
} else if let Some(colon) = header.colon {
children.push(SyntaxElement::Token(SyntaxToken::new(
SyntaxKind::DESCRIPTION,
TextRange::new(colon.end(), colon.end()),
)));
}
SyntaxNode::new(SyntaxKind::GOOGLE_EXCEPTION, range, children)
}
fn build_warning_node(header: &EntryHeader, range: TextRange) -> SyntaxNode {
let mut children = Vec::new();
children.push(SyntaxElement::Token(SyntaxToken::new(
SyntaxKind::WARNING_TYPE,
header.name,
)));
if let Some(colon) = header.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)));
} else if let Some(colon) = header.colon {
children.push(SyntaxElement::Token(SyntaxToken::new(
SyntaxKind::DESCRIPTION,
TextRange::new(colon.end(), colon.end()),
)));
}
SyntaxNode::new(SyntaxKind::GOOGLE_WARNING, range, children)
}
fn build_see_also_node(header: &EntryHeader, range: TextRange, source: &str) -> SyntaxNode {
let mut children = Vec::new();
let name_text = header.name.source_text(source);
let base = header.name.start().raw() as usize;
let mut offset = 0;
for part in name_text.split(',') {
let name = part.trim();
if !name.is_empty() {
let lead = part.len() - part.trim_start().len();
children.push(SyntaxElement::Token(SyntaxToken::new(
SyntaxKind::NAME,
TextRange::from_offset_len(base + offset + lead, name.len()),
)));
}
offset += part.len() + 1;
}
if let Some(colon) = header.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)));
} else if let Some(colon) = header.colon {
children.push(SyntaxElement::Token(SyntaxToken::new(
SyntaxKind::DESCRIPTION,
TextRange::new(colon.end(), colon.end()),
)));
}
SyntaxNode::new(SyntaxKind::GOOGLE_SEE_ALSO_ITEM, range, children)
}
fn parse_entry(cursor: &LineCursor, parse_type: bool) -> (EntryHeader, TextRange) {
let header = parse_entry_header(cursor, parse_type);
let entry_col = cursor.current_indent();
let range_end = header
.first_description
.as_ref()
.map_or(header.range.end(), |d| d.end());
let (end_line, end_col_pos) = cursor.offset_to_line_col(range_end.raw() as usize);
let entry_range = cursor.make_range(cursor.line, entry_col, end_line, end_col_pos);
(header, entry_range)
}
fn build_content_range(cursor: &LineCursor, first: Option<usize>, last: usize) -> Option<TextRange> {
first.map(|f| {
let first_line = cursor.line_text(f);
let first_col = indent_len(first_line);
let last_line = cursor.line_text(last);
let last_col = indent_len(last_line) + last_line.trim().len();
cursor.make_range(f, first_col, last, last_col)
})
}
fn extend_last_node_description(nodes: &mut [SyntaxElement], cont: TextRange) {
if let Some(SyntaxElement::Node(node)) = nodes.last_mut() {
let mut found_desc = false;
for child in node.children_mut() {
if let SyntaxElement::Token(t) = child {
if t.kind() == SyntaxKind::DESCRIPTION {
if t.is_missing() {
*t = SyntaxToken::new(SyntaxKind::DESCRIPTION, cont);
} else {
t.extend_range(cont);
}
found_desc = true;
break;
}
}
}
if !found_desc {
node.push_child(SyntaxElement::Token(SyntaxToken::new(SyntaxKind::DESCRIPTION, cont)));
}
node.extend_range_to(cont.end());
}
}
fn process_arg_line(
cursor: &LineCursor,
node_kind: SyntaxKind,
nodes: &mut Vec<SyntaxElement>,
entry_indent: &mut Option<usize>,
) {
let indent_cols = cursor.current_indent_columns();
if let Some(base) = *entry_indent {
if indent_cols > base {
extend_last_node_description(nodes, cursor.current_trimmed_range());
return;
}
}
if entry_indent.is_none() {
*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)));
}
fn process_exception_line(cursor: &LineCursor, nodes: &mut Vec<SyntaxElement>, entry_indent: &mut Option<usize>) {
let indent_cols = cursor.current_indent_columns();
if let Some(base) = *entry_indent {
if indent_cols > base {
extend_last_node_description(nodes, cursor.current_trimmed_range());
return;
}
}
if entry_indent.is_none() {
*entry_indent = Some(indent_cols);
}
let (header, entry_range) = parse_entry(cursor, false);
nodes.push(SyntaxElement::Node(build_exception_node(&header, entry_range)));
}
fn process_warning_line(cursor: &LineCursor, nodes: &mut Vec<SyntaxElement>, entry_indent: &mut Option<usize>) {
let indent_cols = cursor.current_indent_columns();
if let Some(base) = *entry_indent {
if indent_cols > base {
extend_last_node_description(nodes, cursor.current_trimmed_range());
return;
}
}
if entry_indent.is_none() {
*entry_indent = Some(indent_cols);
}
let (header, entry_range) = parse_entry(cursor, false);
nodes.push(SyntaxElement::Node(build_warning_node(&header, entry_range)));
}
fn process_see_also_line(cursor: &LineCursor, nodes: &mut Vec<SyntaxElement>, entry_indent: &mut Option<usize>) {
let indent_cols = cursor.current_indent_columns();
if let Some(base) = *entry_indent {
if indent_cols > base {
extend_last_node_description(nodes, cursor.current_trimmed_range());
return;
}
}
if entry_indent.is_none() {
*entry_indent = Some(indent_cols);
}
let (header, entry_range) = parse_entry(cursor, false);
nodes.push(SyntaxElement::Node(build_see_also_node(
&header,
entry_range,
cursor.source(),
)));
}
struct ReturnsState {
range: Option<TextRange>,
return_type: Option<TextRange>,
colon: Option<TextRange>,
description: Option<TextRange>,
}
impl ReturnsState {
fn new() -> Self {
Self {
range: None,
return_type: None,
colon: None,
description: None,
}
}
fn process_line(&mut self, cursor: &LineCursor) {
let trimmed_range = cursor.current_trimmed_range();
if self.range.is_none() {
self.range = Some(trimmed_range);
let trimmed = cursor.current_trimmed();
let col = cursor.current_indent();
if let Some(colon_pos) = find_entry_colon(trimmed) {
let type_str = trimmed[..colon_pos].trim_end();
let after_colon = &trimmed[colon_pos + 1..];
let desc_str = after_colon.trim_start();
let ws_after = after_colon.len() - desc_str.len();
self.return_type = Some(cursor.make_line_range(cursor.line, col, type_str.len()));
self.colon = Some(cursor.make_line_range(cursor.line, col + colon_pos, 1));
let desc_start = col + colon_pos + 1 + ws_after;
self.description = if desc_str.is_empty() {
None
} else {
Some(cursor.make_line_range(cursor.line, desc_start, desc_str.len()))
};
} else {
self.description = Some(trimmed_range);
}
} else {
match self.description {
Some(ref mut desc) => desc.extend(trimmed_range),
None => self.description = Some(trimmed_range),
}
if let Some(ref mut r) = self.range {
r.extend(trimmed_range);
}
}
}
fn into_node(self, kind: SyntaxKind) -> Option<SyntaxNode> {
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)));
}
if let Some(colon) = self.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)));
}
Some(SyntaxNode::new(kind, range, children))
}
}
enum SectionBody {
Args(SyntaxKind, Vec<SyntaxElement>),
Returns(SyntaxKind, ReturnsState),
Raises(Vec<SyntaxElement>),
Warns(Vec<SyntaxElement>),
SeeAlso(Vec<SyntaxElement>),
FreeText(Option<TextRange>),
}
impl SectionBody {
#[rustfmt::skip]
fn new(kind: GoogleSectionKind) -> Self {
match kind {
GoogleSectionKind::Args => Self::Args(SyntaxKind::GOOGLE_ARG, Vec::new()),
GoogleSectionKind::KeywordArgs => Self::Args(SyntaxKind::GOOGLE_ARG, Vec::new()),
GoogleSectionKind::OtherParameters => Self::Args(SyntaxKind::GOOGLE_ARG, Vec::new()),
GoogleSectionKind::Receives => Self::Args(SyntaxKind::GOOGLE_ARG, Vec::new()),
GoogleSectionKind::Attributes => Self::Args(SyntaxKind::GOOGLE_ATTRIBUTE, Vec::new()),
GoogleSectionKind::Methods => Self::Args(SyntaxKind::GOOGLE_METHOD, Vec::new()),
GoogleSectionKind::Returns => Self::Returns(SyntaxKind::GOOGLE_RETURNS, ReturnsState::new()),
GoogleSectionKind::Yields => Self::Returns(SyntaxKind::GOOGLE_YIELDS, ReturnsState::new()),
GoogleSectionKind::Raises => Self::Raises(Vec::new()),
GoogleSectionKind::Warns => Self::Warns(Vec::new()),
GoogleSectionKind::SeeAlso => Self::SeeAlso(Vec::new()),
_ => Self::FreeText(None),
}
}
#[rustfmt::skip]
fn process_line(&mut self, cursor: &LineCursor, entry_indent: &mut Option<usize>) {
match self {
Self::Args(node_kind, nodes) => process_arg_line(cursor, *node_kind, nodes, entry_indent),
Self::Returns(_, state) => state.process_line(cursor),
Self::Raises(nodes) => process_exception_line(cursor, nodes, entry_indent),
Self::Warns(nodes) => process_warning_line(cursor, nodes, entry_indent),
Self::SeeAlso(nodes) => process_see_also_line(cursor, nodes, entry_indent),
Self::FreeText(range) => {
let r = cursor.current_trimmed_range();
match range {
Some(existing) => existing.extend(r),
None => *range = Some(r),
}
}
}
}
fn into_children(self) -> Vec<SyntaxElement> {
match self {
Self::Args(_, nodes) => nodes,
Self::Returns(kind, state) => match state.into_node(kind) {
Some(node) => vec![SyntaxElement::Node(node)],
None => vec![],
},
Self::Raises(nodes) => nodes,
Self::Warns(nodes) => nodes,
Self::SeeAlso(nodes) => nodes,
Self::FreeText(range) => match range {
Some(r) => vec![SyntaxElement::Token(SyntaxToken::new(SyntaxKind::BODY_TEXT, r))],
None => vec![],
},
}
}
}
pub fn parse_google(input: &str) -> Parsed {
let mut line_cursor = LineCursor::new(input);
let mut root_children: Vec<SyntaxElement> = Vec::new();
line_cursor.skip_blanks();
if line_cursor.is_eof() {
let root = SyntaxNode::new(SyntaxKind::GOOGLE_DOCSTRING, line_cursor.full_range(), root_children);
return Parsed::new(input.to_string(), root);
}
let mut summary_done = false;
let mut extended_done = false;
let mut summary_first: Option<usize> = None;
let mut summary_last: usize = 0;
let mut ext_first: Option<usize> = None;
let mut ext_last: usize = 0;
let mut current_header: Option<SectionHeaderInfo> = None;
let mut current_body: Option<SectionBody> = None;
let mut entry_indent: Option<usize> = None;
while !line_cursor.is_eof() {
if line_cursor.current_trimmed().is_empty() {
if !summary_done && summary_first.is_some() {
root_children.push(SyntaxElement::Token(SyntaxToken::new(
SyntaxKind::SUMMARY,
build_content_range(&line_cursor, summary_first, summary_last).unwrap(),
)));
summary_done = true;
}
line_cursor.advance();
continue;
}
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) {
if !summary_done {
if summary_first.is_some() {
root_children.push(SyntaxElement::Token(SyntaxToken::new(
SyntaxKind::SUMMARY,
build_content_range(&line_cursor, summary_first, summary_last).unwrap(),
)));
}
summary_done = true;
}
if !extended_done {
if ext_first.is_some() {
root_children.push(SyntaxElement::Token(SyntaxToken::new(
SyntaxKind::EXTENDED_SUMMARY,
build_content_range(&line_cursor, ext_first, ext_last).unwrap(),
)));
}
extended_done = true;
}
if let Some(prev_header) = current_header.take() {
flush_section(
&line_cursor,
&mut root_children,
prev_header,
current_body.take().unwrap(),
);
}
current_body = Some(SectionBody::new(header_info.kind));
current_header = Some(header_info);
entry_indent = None;
line_cursor.advance();
continue;
}
if let Some(ref mut body) = current_body {
body.process_line(&line_cursor, &mut entry_indent);
} else if !summary_done {
if summary_first.is_none() {
summary_first = Some(line_cursor.line);
}
summary_last = line_cursor.line;
} else if !extended_done {
if ext_first.is_none() {
ext_first = Some(line_cursor.line);
}
ext_last = line_cursor.line;
} else {
root_children.push(SyntaxElement::Token(SyntaxToken::new(
SyntaxKind::STRAY_LINE,
line_cursor.current_trimmed_range(),
)));
}
line_cursor.advance();
}
if let Some(header) = current_header.take() {
flush_section(&line_cursor, &mut root_children, header, current_body.take().unwrap());
}
if !summary_done && summary_first.is_some() {
root_children.push(SyntaxElement::Token(SyntaxToken::new(
SyntaxKind::SUMMARY,
build_content_range(&line_cursor, summary_first, summary_last).unwrap(),
)));
}
if !extended_done && ext_first.is_some() {
root_children.push(SyntaxElement::Token(SyntaxToken::new(
SyntaxKind::EXTENDED_SUMMARY,
build_content_range(&line_cursor, ext_first, ext_last).unwrap(),
)));
}
let root = SyntaxNode::new(SyntaxKind::GOOGLE_DOCSTRING, line_cursor.full_range(), root_children);
Parsed::new(input.to_string(), root)
}
fn flush_section(
cursor: &LineCursor,
root_children: &mut Vec<SyntaxElement>,
header_info: SectionHeaderInfo,
body: SectionBody,
) {
let header_start = header_info.range.start().raw() as usize;
let section_range = cursor.span_back_from_cursor(header_start);
let header_node = build_section_header_node(&header_info);
let mut section_children = vec![SyntaxElement::Node(header_node)];
section_children.extend(body.into_children());
root_children.push(SyntaxElement::Node(SyntaxNode::new(
SyntaxKind::GOOGLE_SECTION,
section_range,
section_children,
)));
}
#[cfg(test)]
mod tests {
use super::*;
fn is_header(text: &str) -> bool {
let cursor = LineCursor::new(text);
try_parse_section_header(&cursor).is_some()
}
#[test]
fn test_is_section_header() {
assert!(is_header("Args:"));
assert!(is_header("NotASection:"));
assert!(is_header("Returns:"));
assert!(is_header("Custom:"));
assert!(is_header("args:"));
assert!(is_header("RETURNS:"));
assert!(!is_header("key: value:"));
assert!(is_header(
"This is a very long line that should not be a section header:"
));
assert!(is_header("Args :"));
assert!(is_header("Returns :"));
assert!(is_header("Args"));
assert!(is_header("Returns"));
assert!(is_header("args"));
assert!(is_header("RETURNS"));
assert!(is_header("See Also"));
assert!(!is_header("NotASection"));
assert!(!is_header("SomeWord"));
}
fn header_from(text: &str) -> EntryHeader {
let cursor = LineCursor::new(text);
parse_entry_header(&cursor, false)
}
fn header_from_lenient(text: &str) -> EntryHeader {
let cursor = LineCursor::new(text);
parse_entry_header(&cursor, true)
}
#[test]
fn test_parse_entry_header_with_type() {
let src = "name (int): Description";
let header = header_from_lenient(src);
assert_eq!(header.name.source_text(src), "name");
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");
}
#[test]
fn test_parse_entry_header_optional() {
let src = "name (int, optional): Description";
let header = header_from_lenient(src);
assert_eq!(header.name.source_text(src), "name");
let ti = header.type_info.unwrap();
assert_eq!(ti.r#type.unwrap().source_text(src), "int");
assert!(ti.optional.is_some());
assert_eq!(ti.optional.unwrap().source_text(src), "optional");
}
#[test]
fn test_parse_entry_header_no_type() {
let src = "name: Description";
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");
}
#[test]
fn test_parse_entry_header_complex_type() {
let src = "data (Dict[str, List[int]]): Values";
let header = header_from_lenient(src);
assert_eq!(header.name.source_text(src), "data");
let ti = header.type_info.unwrap();
assert_eq!(ti.r#type.unwrap().source_text(src), "Dict[str, List[int]]");
assert_eq!(header.first_description.unwrap().source_text(src), "Values");
}
#[test]
fn test_parse_entry_header_colon_only() {
let src = "x:";
let header = header_from(src);
assert_eq!(header.name.source_text(src), "x");
assert!(header.type_info.is_none());
assert!(header.first_description.is_none());
}
#[test]
fn test_parse_entry_header_varargs() {
let src1 = "*args: Positional arguments";
let header = header_from(src1);
assert_eq!(header.name.source_text(src1), "*args");
assert_eq!(
header.first_description.unwrap().source_text(src1),
"Positional arguments"
);
let src2 = "**kwargs (dict): Keyword arguments";
let header = header_from_lenient(src2);
assert_eq!(header.name.source_text(src2), "**kwargs");
let ti = header.type_info.unwrap();
assert_eq!(ti.r#type.unwrap().source_text(src2), "dict");
}
#[test]
fn test_parse_entry_header_no_space_after_colon() {
let src = "name:Description";
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");
}
#[test]
fn test_parse_entry_header_extra_spaces_after_colon() {
let src = "name: Description";
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");
}
#[test]
fn test_parse_entry_header_no_space_before_bracket_strict() {
let src = "name(int): Description";
let header = header_from(src);
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");
}
#[test]
fn test_parse_entry_header_no_space_before_bracket_lenient() {
let src = "name(int): Description";
let header = header_from_lenient(src);
assert_eq!(header.name.source_text(src), "name");
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");
}
#[test]
fn test_parse_entry_header_no_space_complex_type_lenient() {
let src = "data(Dict[str, int]): Values";
let header = header_from_lenient(src);
assert_eq!(header.name.source_text(src), "data");
let ti = header.type_info.unwrap();
assert_eq!(ti.r#type.unwrap().source_text(src), "Dict[str, int]");
assert_eq!(header.first_description.unwrap().source_text(src), "Values");
}
}