1use crate::diagnostics::{BuildDiagnostics, Spanned};
7use crate::langtype::BuiltinElement;
8use crate::parser::{SyntaxKind, SyntaxNode, identifier_text, syntax_nodes};
9use smol_str::SmolStr;
10
11#[derive(Debug, Clone)]
14pub enum ElementDocEntry {
15 Text(String),
17 Member(SmolStr),
19}
20
21fn strip_doc_prefix<'a>(line: &'a str, prefix: &str) -> Option<&'a str> {
26 let rest = line.strip_prefix(prefix)?;
27 match rest.strip_prefix(' ') {
28 Some(content) => Some(content),
29 None if rest.is_empty() => Some(""),
30 None => None,
31 }
32}
33
34fn collect_before(anchor: &SyntaxNode) -> Option<String> {
39 let mut lines = Vec::new();
40 let mut cursor = anchor.node.prev_sibling_or_token();
41 while let Some(cur) = cursor {
42 match cur.kind() {
43 SyntaxKind::Whitespace => {}
44 SyntaxKind::Comment => {
45 let text = cur.as_token().unwrap().text();
46 if let Some(content) = strip_doc_prefix(text, "///") {
47 lines.push(content.to_string());
48 } else if text.starts_with("//") {
49 } else {
51 break;
52 }
53 }
54 SyntaxKind::ExportsList => {
55 if let Some(list) = cur.as_node() {
57 let mut last = list.last_child_or_token();
58 while let Some(child) = last {
59 match child.kind() {
60 SyntaxKind::Whitespace => {}
61 SyntaxKind::Comment => {
62 let t = child.as_token().unwrap().text();
63 if let Some(content) = strip_doc_prefix(t, "///") {
64 lines.push(content.to_string());
65 } else if t.starts_with("//") {
66 } else {
68 break;
69 }
70 }
71 _ => break,
72 }
73 last = child.prev_sibling_or_token();
74 }
75 }
76 break;
77 }
78 _ => break,
79 }
80 cursor = cur.prev_sibling_or_token();
81 }
82 if lines.is_empty() {
83 return None;
84 }
85 lines.reverse();
86 Some(lines.join("\n"))
87}
88
89pub(crate) fn doc_comment(anchor: &SyntaxNode) -> Option<String> {
92 if let Some(doc) = collect_before(anchor) {
93 return Some(doc);
94 }
95 if let Some(parent) = anchor.parent()
96 && parent.kind() == SyntaxKind::ExportsList
97 {
98 return collect_before(&parent);
99 }
100 None
101}
102
103pub(crate) fn element_doc_entries(
107 component: &SyntaxNode,
108 element: &syntax_nodes::Element,
109 diag: &mut BuildDiagnostics,
110) -> Vec<ElementDocEntry> {
111 let description = doc_comment(component).unwrap_or_default();
112
113 let mut entries = vec![ElementDocEntry::Text(description)];
114 let mut section_lines: Vec<String> = Vec::new();
115 let flush_section = |lines: &mut Vec<String>, entries: &mut Vec<ElementDocEntry>| {
116 if !lines.is_empty() {
117 entries.push(ElementDocEntry::Text(lines.join("\n")));
118 lines.clear();
119 }
120 };
121
122 let mut doc_comment_span = None;
123 for child in element.children_with_tokens() {
124 match child.kind() {
125 SyntaxKind::Whitespace => {}
126 SyntaxKind::Comment => {
127 if let Some(t) = child.as_token() {
128 let text = t.text();
129 if strip_doc_prefix(text, "///").is_some() {
130 doc_comment_span = Some(child.to_source_location());
131 } else if let Some(content) = strip_doc_prefix(text, "//!") {
132 if let Some(span) = doc_comment_span.take() {
133 diag.push_warning_with_span(
134 "`///` doc comment not attached to a declaration".into(),
135 span,
136 );
137 }
138 section_lines.push(content.to_string());
139 }
140 }
141 }
142 SyntaxKind::PropertyDeclaration => {
143 doc_comment_span = None;
144 flush_section(&mut section_lines, &mut entries);
145 let p = syntax_nodes::PropertyDeclaration::from(child.into_node().unwrap());
146 let name = identifier_text(&p.DeclaredIdentifier()).unwrap();
147 entries.push(ElementDocEntry::Member(name));
148 }
149 SyntaxKind::CallbackDeclaration => {
150 doc_comment_span = None;
151 flush_section(&mut section_lines, &mut entries);
152 let cb = syntax_nodes::CallbackDeclaration::from(child.into_node().unwrap());
153 let name = identifier_text(&cb.DeclaredIdentifier()).unwrap();
154 entries.push(ElementDocEntry::Member(name));
155 }
156 SyntaxKind::Function => {
157 doc_comment_span = None;
158 let f = syntax_nodes::Function::from(child.into_node().unwrap());
159 flush_section(&mut section_lines, &mut entries);
160 let name = identifier_text(&f.DeclaredIdentifier()).unwrap();
161 entries.push(ElementDocEntry::Member(name));
162 }
163 _ => {
164 if let Some(span) = doc_comment_span.take() {
165 diag.push_warning_with_span(
166 "`///` doc comment not attached to a declaration".into(),
167 span,
168 );
169 }
170 }
171 }
172 }
173 if let Some(span) = doc_comment_span.take() {
174 diag.push_warning_with_span("`///` doc comment not attached to a declaration".into(), span);
175 }
176 flush_section(&mut section_lines, &mut entries);
177 entries
178}
179
180pub(crate) fn assemble(
183 mut entries: Vec<ElementDocEntry>,
184 parent: Option<&BuiltinElement>,
185) -> Vec<ElementDocEntry> {
186 let skip_inherited = matches!(entries.first(), Some(ElementDocEntry::Text(desc)) if desc.contains("\\skip_inherited"));
187
188 if !skip_inherited && let Some(parent) = parent {
189 entries.splice(1..1, parent.docs[1..].iter().cloned());
192 }
193 entries
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199 use crate::diagnostics::BuildDiagnostics;
200 use crate::parser::{self, syntax_nodes};
201
202 fn parse_component(source: &str) -> (SyntaxNode, syntax_nodes::Element, BuildDiagnostics) {
205 let mut diag = BuildDiagnostics::default();
206 let node = parser::parse(source.into(), None, &mut diag);
207 assert!(!diag.has_errors(), "parse errors: {:?}", diag.to_string_vec());
208 let doc: syntax_nodes::Document = node.into();
209 let comp = doc.Component().next().expect("no component found");
210 let elem = comp.Element();
211 (comp.into(), elem, BuildDiagnostics::default())
212 }
213
214 #[test]
215 fn test_strip_doc_prefix() {
216 assert_eq!(strip_doc_prefix("/// hello", "///"), Some("hello"));
217 assert_eq!(strip_doc_prefix("///", "///"), Some(""));
218 assert_eq!(strip_doc_prefix("////", "///"), None);
219 assert_eq!(strip_doc_prefix("//! section", "//!"), Some("section"));
220 assert_eq!(strip_doc_prefix("//!", "//!"), Some(""));
221 assert_eq!(strip_doc_prefix("//!!", "//!"), None);
222 }
223
224 #[test]
225 fn test_doc_comment_before_component() {
226 let (comp, _, _) = parse_component("/// My component\ncomponent Foo inherits Rectangle {}");
227 assert_eq!(doc_comment(&comp), Some("My component".into()));
228 }
229
230 #[test]
231 fn test_element_doc_entries_basic() {
232 let (comp, elem, mut diag) =
233 parse_component("/// Description\ncomponent Foo {\n in property <int> bar;\n}");
234 let entries = element_doc_entries(&comp, &elem, &mut diag);
235 assert!(diag.is_empty(), "unexpected diag: {:?}", diag.to_string_vec());
236 assert!(matches!(&entries[0], ElementDocEntry::Text(t) if t == "Description"));
237 assert!(matches!(&entries[1], ElementDocEntry::Member(n) if n == "bar"));
238 }
239
240 #[test]
241 fn test_element_doc_entries_section_text() {
242 let (comp, elem, mut diag) =
243 parse_component("component Foo {\n //! section\n in property <int> x;\n}");
244 let entries = element_doc_entries(&comp, &elem, &mut diag);
245 assert!(diag.is_empty(), "unexpected diag: {:?}", diag.to_string_vec());
246 assert!(matches!(&entries[0], ElementDocEntry::Text(t) if t.is_empty()));
248 assert!(matches!(&entries[1], ElementDocEntry::Text(t) if t == "section"));
249 assert!(matches!(&entries[2], ElementDocEntry::Member(n) if n == "x"));
250 }
251
252 #[test]
253 fn test_element_doc_entries_warns_orphan_doc_comment() {
254 let (comp, elem, mut diag) = parse_component("component Foo {\n /// orphan\n}");
255 let _entries = element_doc_entries(&comp, &elem, &mut diag);
256 assert!(
257 diag.to_string_vec().iter().any(|m| m.contains("not attached to a declaration")),
258 "expected warning about orphan doc comment, got: {:?}",
259 diag.to_string_vec(),
260 );
261 }
262
263 #[test]
264 fn test_element_doc_entries_callback_and_function() {
265 let (comp, elem, mut diag) =
266 parse_component("component Foo {\n callback clicked();\n function do-stuff() {}\n}");
267 let entries = element_doc_entries(&comp, &elem, &mut diag);
268 assert!(diag.is_empty(), "unexpected diag: {:?}", diag.to_string_vec());
269 assert!(matches!(&entries[1], ElementDocEntry::Member(n) if n == "clicked"));
270 assert!(matches!(&entries[2], ElementDocEntry::Member(n) if n == "do-stuff"));
271 }
272}