use crate::diagnostics::{BuildDiagnostics, Spanned};
use crate::langtype::BuiltinElement;
use crate::parser::{SyntaxKind, SyntaxNode, identifier_text, syntax_nodes};
use smol_str::SmolStr;
#[derive(Debug, Clone)]
pub enum ElementDocEntry {
Text(String),
Member(SmolStr),
}
fn strip_doc_prefix<'a>(line: &'a str, prefix: &str) -> Option<&'a str> {
let rest = line.strip_prefix(prefix)?;
match rest.strip_prefix(' ') {
Some(content) => Some(content),
None if rest.is_empty() => Some(""),
None => None,
}
}
fn collect_before(anchor: &SyntaxNode) -> Option<String> {
let mut lines = Vec::new();
let mut cursor = anchor.node.prev_sibling_or_token();
while let Some(cur) = cursor {
match cur.kind() {
SyntaxKind::Whitespace => {}
SyntaxKind::Comment => {
let text = cur.as_token().unwrap().text();
if let Some(content) = strip_doc_prefix(text, "///") {
lines.push(content.to_string());
} else if text.starts_with("//") {
} else {
break;
}
}
SyntaxKind::ExportsList => {
if let Some(list) = cur.as_node() {
let mut last = list.last_child_or_token();
while let Some(child) = last {
match child.kind() {
SyntaxKind::Whitespace => {}
SyntaxKind::Comment => {
let t = child.as_token().unwrap().text();
if let Some(content) = strip_doc_prefix(t, "///") {
lines.push(content.to_string());
} else if t.starts_with("//") {
} else {
break;
}
}
_ => break,
}
last = child.prev_sibling_or_token();
}
}
break;
}
_ => break,
}
cursor = cur.prev_sibling_or_token();
}
if lines.is_empty() {
return None;
}
lines.reverse();
Some(lines.join("\n"))
}
pub(crate) fn doc_comment(anchor: &SyntaxNode) -> Option<String> {
if let Some(doc) = collect_before(anchor) {
return Some(doc);
}
if let Some(parent) = anchor.parent()
&& parent.kind() == SyntaxKind::ExportsList
{
return collect_before(&parent);
}
None
}
pub(crate) fn element_doc_entries(
component: &SyntaxNode,
element: &syntax_nodes::Element,
diag: &mut BuildDiagnostics,
) -> Vec<ElementDocEntry> {
let description = doc_comment(component).unwrap_or_default();
let mut entries = vec![ElementDocEntry::Text(description)];
let mut section_lines: Vec<String> = Vec::new();
let flush_section = |lines: &mut Vec<String>, entries: &mut Vec<ElementDocEntry>| {
if !lines.is_empty() {
entries.push(ElementDocEntry::Text(lines.join("\n")));
lines.clear();
}
};
let mut doc_comment_span = None;
for child in element.children_with_tokens() {
match child.kind() {
SyntaxKind::Whitespace => {}
SyntaxKind::Comment => {
if let Some(t) = child.as_token() {
let text = t.text();
if strip_doc_prefix(text, "///").is_some() {
doc_comment_span = Some(child.to_source_location());
} else if let Some(content) = strip_doc_prefix(text, "//!") {
if let Some(span) = doc_comment_span.take() {
diag.push_warning_with_span(
"`///` doc comment not attached to a declaration".into(),
span,
);
}
section_lines.push(content.to_string());
}
}
}
SyntaxKind::PropertyDeclaration => {
doc_comment_span = None;
flush_section(&mut section_lines, &mut entries);
let p = syntax_nodes::PropertyDeclaration::from(child.into_node().unwrap());
let name = identifier_text(&p.DeclaredIdentifier()).unwrap();
entries.push(ElementDocEntry::Member(name));
}
SyntaxKind::CallbackDeclaration => {
doc_comment_span = None;
flush_section(&mut section_lines, &mut entries);
let cb = syntax_nodes::CallbackDeclaration::from(child.into_node().unwrap());
let name = identifier_text(&cb.DeclaredIdentifier()).unwrap();
entries.push(ElementDocEntry::Member(name));
}
SyntaxKind::Function => {
doc_comment_span = None;
let f = syntax_nodes::Function::from(child.into_node().unwrap());
flush_section(&mut section_lines, &mut entries);
let name = identifier_text(&f.DeclaredIdentifier()).unwrap();
entries.push(ElementDocEntry::Member(name));
}
_ => {
if let Some(span) = doc_comment_span.take() {
diag.push_warning_with_span(
"`///` doc comment not attached to a declaration".into(),
span,
);
}
}
}
}
if let Some(span) = doc_comment_span.take() {
diag.push_warning_with_span("`///` doc comment not attached to a declaration".into(), span);
}
flush_section(&mut section_lines, &mut entries);
entries
}
pub(crate) fn assemble(
mut entries: Vec<ElementDocEntry>,
parent: Option<&BuiltinElement>,
) -> Vec<ElementDocEntry> {
let skip_inherited = matches!(entries.first(), Some(ElementDocEntry::Text(desc)) if desc.contains("\\skip_inherited"));
if !skip_inherited && let Some(parent) = parent {
entries.splice(1..1, parent.docs[1..].iter().cloned());
}
entries
}
#[cfg(test)]
mod tests {
use super::*;
use crate::diagnostics::BuildDiagnostics;
use crate::parser::{self, syntax_nodes};
fn parse_component(source: &str) -> (SyntaxNode, syntax_nodes::Element, BuildDiagnostics) {
let mut diag = BuildDiagnostics::default();
let node = parser::parse(source.into(), None, &mut diag);
assert!(!diag.has_errors(), "parse errors: {:?}", diag.to_string_vec());
let doc: syntax_nodes::Document = node.into();
let comp = doc.Component().next().expect("no component found");
let elem = comp.Element();
(comp.into(), elem, BuildDiagnostics::default())
}
#[test]
fn test_strip_doc_prefix() {
assert_eq!(strip_doc_prefix("/// hello", "///"), Some("hello"));
assert_eq!(strip_doc_prefix("///", "///"), Some(""));
assert_eq!(strip_doc_prefix("////", "///"), None);
assert_eq!(strip_doc_prefix("//! section", "//!"), Some("section"));
assert_eq!(strip_doc_prefix("//!", "//!"), Some(""));
assert_eq!(strip_doc_prefix("//!!", "//!"), None);
}
#[test]
fn test_doc_comment_before_component() {
let (comp, _, _) = parse_component("/// My component\ncomponent Foo inherits Rectangle {}");
assert_eq!(doc_comment(&comp), Some("My component".into()));
}
#[test]
fn test_element_doc_entries_basic() {
let (comp, elem, mut diag) =
parse_component("/// Description\ncomponent Foo {\n in property <int> bar;\n}");
let entries = element_doc_entries(&comp, &elem, &mut diag);
assert!(diag.is_empty(), "unexpected diag: {:?}", diag.to_string_vec());
assert!(matches!(&entries[0], ElementDocEntry::Text(t) if t == "Description"));
assert!(matches!(&entries[1], ElementDocEntry::Member(n) if n == "bar"));
}
#[test]
fn test_element_doc_entries_section_text() {
let (comp, elem, mut diag) =
parse_component("component Foo {\n //! section\n in property <int> x;\n}");
let entries = element_doc_entries(&comp, &elem, &mut diag);
assert!(diag.is_empty(), "unexpected diag: {:?}", diag.to_string_vec());
assert!(matches!(&entries[0], ElementDocEntry::Text(t) if t.is_empty()));
assert!(matches!(&entries[1], ElementDocEntry::Text(t) if t == "section"));
assert!(matches!(&entries[2], ElementDocEntry::Member(n) if n == "x"));
}
#[test]
fn test_element_doc_entries_warns_orphan_doc_comment() {
let (comp, elem, mut diag) = parse_component("component Foo {\n /// orphan\n}");
let _entries = element_doc_entries(&comp, &elem, &mut diag);
assert!(
diag.to_string_vec().iter().any(|m| m.contains("not attached to a declaration")),
"expected warning about orphan doc comment, got: {:?}",
diag.to_string_vec(),
);
}
#[test]
fn test_element_doc_entries_callback_and_function() {
let (comp, elem, mut diag) =
parse_component("component Foo {\n callback clicked();\n function do-stuff() {}\n}");
let entries = element_doc_entries(&comp, &elem, &mut diag);
assert!(diag.is_empty(), "unexpected diag: {:?}", diag.to_string_vec());
assert!(matches!(&entries[1], ElementDocEntry::Member(n) if n == "clicked"));
assert!(matches!(&entries[2], ElementDocEntry::Member(n) if n == "do-stuff"));
}
}