use crate::checker::Checker;
use crate::lang_helpers::doc_comment::{node_bytes, outside_function_bodies};
use crate::node::{Ancestors, Node, NodeMemo};
#[inline]
#[must_use]
pub(crate) fn doxygen_member_after(s: &[u8]) -> bool {
[&b"/**<"[..], b"/*!<", b"///<", b"//!<"]
.iter()
.any(|marker| s.starts_with(marker))
}
#[must_use]
pub(crate) fn doxygen_standalone(s: &[u8]) -> bool {
const ALWAYS: &[&[u8]] = &[
b"file",
b"mainpage",
b"page",
b"dir",
b"example",
b"defgroup",
b"addtogroup",
b"weakgroup",
b"name",
b"package",
];
const NAMED: &[&[u8]] = &[
b"fn",
b"var",
b"def",
b"typedef",
b"property",
b"overload",
b"enum",
b"struct",
b"union",
b"class",
b"namespace",
b"interface",
b"protocol",
b"concept",
b"idlexcept",
b"module",
];
let mut at = 0;
while at < s.len() {
if let Some(end) = literal_end(s, at) {
at = end;
continue;
}
let Some(word) = doxygen_command(s, at) else {
at += 1;
continue;
};
at += 1 + word.len();
if ALWAYS.contains(&word) || (NAMED.contains(&word) && names_something(&s[at..])) {
return true;
}
}
false
}
fn literal_end(s: &[u8], at: usize) -> Option<usize> {
let rest = &s[at..];
if let Some(fenced) = rest.strip_prefix(b"```") {
let close = fenced.windows(3).position(|w| w == b"```");
return Some(close.map_or(s.len(), |close| at + 6 + close));
}
if let Some(span) = rest.strip_prefix(b"`") {
return code_span_close(span).map(|close| at + close + 2);
}
if rest.starts_with(b"<!--") {
return Some(html_comment_end(s, at + 4));
}
if matches!(rest, [b'@', b'@', ..] | [b'\\', b'\\', ..]) {
return Some(at + 2);
}
let end: &[u8] = match doxygen_command(s, at)? {
b"code" => b"endcode",
b"icode" => b"endicode",
b"verbatim" => b"endverbatim",
b"iverbatim" => b"endiverbatim",
b"iliteral" => b"endiliteral",
b"htmlonly" => b"endhtmlonly",
b"latexonly" => b"endlatexonly",
b"xmlonly" => b"endxmlonly",
b"rtfonly" => b"endrtfonly",
b"manonly" => b"endmanonly",
b"docbookonly" => b"enddocbookonly",
b"dot" => b"enddot",
b"msc" => b"endmsc",
b"startuml" => b"enduml",
_ => return None,
};
Some(block_end(s, at, end))
}
fn code_span_close(span: &[u8]) -> Option<usize> {
let mut start = 0;
for (index, line) in span.split(|&b| b == b'\n').enumerate() {
if let Some(close) = line.iter().position(|&b| b == b'`') {
return Some(start + close);
}
let text = line.trim_ascii();
if index > 0
&& text
.strip_prefix(b"*")
.unwrap_or(text)
.trim_ascii()
.is_empty()
{
return None;
}
start += line.len() + 1;
}
None
}
fn html_comment_end(s: &[u8], from: usize) -> usize {
(from..s.len())
.find(|&at| s[at..].starts_with(b"-->") && (at == from || s[at - 1] != b'-'))
.map_or(s.len(), |at| at + 3)
}
fn block_end(s: &[u8], at: usize, end: &[u8]) -> usize {
(at + 1..s.len())
.find(|&after| doxygen_command(s, after) == Some(end))
.map_or(s.len(), |after| after + 1 + end.len())
}
fn names_something(after: &[u8]) -> bool {
let line = after.split(|&b| b == b'\n').next().unwrap_or_default();
let line = line.trim_ascii();
!line
.strip_suffix(b"*/")
.unwrap_or(line)
.trim_ascii()
.is_empty()
}
fn doxygen_command(s: &[u8], at: usize) -> Option<&[u8]> {
if !matches!(s.get(at), Some(b'@' | b'\\')) {
return None;
}
let rest = &s[at + 1..];
let len = rest
.iter()
.take_while(|b| b.is_ascii_alphabetic() || **b == b'_')
.count();
if len == 0 {
return None;
}
let mail = s[at] == b'@'
&& at > 0
&& s[at - 1].is_ascii_alphanumeric()
&& rest
.iter()
.take_while(|b| b.is_ascii_alphanumeric() || matches!(b, b'-' | b'.'))
.skip(len)
.any(|&b| b == b'.');
(!mail).then(|| &rest[..len])
}
#[must_use]
pub(crate) fn preproc_conditional(node: &Node) -> bool {
matches!(
node.kind(),
"preproc_if" | "preproc_ifdef" | "preproc_else" | "preproc_elif" | "preproc_elifdef"
)
}
crate::observation::counter!(conditionals_read);
#[must_use]
pub(crate) fn conditional_declares(node: &Node, ancestors: Ancestors) -> bool {
let unshared = NodeMemo::default();
let memo = ancestors.node_memo().unwrap_or(&unshared);
if let Some(answer) = memo.get(node) {
return answer;
}
let mut stack = vec![(*node, false)];
while let Some((conditional, expanded)) = stack.pop() {
if memo.get(&conditional).is_some() {
continue;
}
if expanded {
conditionals_read::record();
let declares = conditional.children().any(|child| {
doxygen_entry(&child)
|| (preproc_conditional(&child) && memo.get(&child) == Some(true))
});
memo.set(&conditional, declares);
continue;
}
stack.push((conditional, true));
stack.extend(
conditional
.children()
.filter(|child| preproc_conditional(child) && memo.get(child).is_none())
.map(|child| (child, false)),
);
}
memo.get(node) == Some(true)
}
#[must_use]
pub(crate) fn doxygen_container(node: &Node) -> bool {
preproc_conditional(node)
|| matches!(
node.kind(),
"translation_unit"
| "template_parameter_list"
| "declaration_list"
| "field_declaration_list"
| "enumerator_list"
| "class_interface"
| "class_implementation"
| "protocol_declaration"
| "qualified_protocol_interface_declaration"
| "instance_variables"
)
}
#[must_use]
pub(crate) fn doxygen_skips(node: &Node, ancestors: Ancestors) -> bool {
match node.kind() {
"preproc_include"
| "preproc_call"
| "static_assert_declaration"
| "namespace_alias_definition"
| "access_specifier"
| "alone_macro"
| ":"
| ";" => true,
"expression_statement" => node.child_count() == 1 || unterminated(node),
"ERROR" => no_entry_inside(node),
_ => preproc_conditional(node) && !conditional_declares(node, ancestors),
}
}
fn no_entry_inside(error: &Node) -> bool {
let mut named = error
.children()
.filter(|child| child.is_named() && !child.is_error() && child.kind() != "comment");
match (named.next(), named.next()) {
(None, _) => true,
(Some(only), None) => !doxygen_entry(&only),
_ => false,
}
}
fn unterminated(statement: &Node) -> bool {
statement.children().last().is_some_and(|c| c.is_missing())
}
#[must_use]
pub(crate) fn doxygen_target(node: &Node, ancestors: Ancestors) -> bool {
doxygen_entry(node) || (preproc_conditional(node) && conditional_declares(node, ancestors))
}
fn doxygen_entry(node: &Node) -> bool {
let has_body = |specifier: &Node| specifier.child_by_field_name("body").is_some();
match node.kind() {
"declaration"
| "function_definition"
| "type_definition"
| "preproc_def"
| "preproc_function_def"
| "linkage_specification"
| "enumerator"
| "namespace_definition"
| "using_declaration"
| "alias_declaration"
| "friend_declaration"
| "template_declaration"
| "class_interface"
| "class_implementation"
| "protocol_declaration"
| "qualified_protocol_interface_declaration"
| "property_declaration"
| "method_declaration"
| "method_definition"
| "instance_variable" => true,
"ERROR" => !no_entry_inside(node),
"struct_specifier" | "union_specifier" | "enum_specifier" | "class_specifier" => {
has_body(node)
}
"field_declaration" => {
node.child_by_field_name("declarator").is_some()
|| node.child_by_field_name("type").is_none_or(|t| {
!matches!(
t.kind(),
"struct_specifier"
| "union_specifier"
| "enum_specifier"
| "class_specifier"
) || has_body(&t)
})
}
"expression_statement" => node.child_count() > 1 && !unterminated(node),
"implementation_definition" => node
.child(0)
.is_some_and(|d| d.kind() != "property_implementation"),
_ => false,
}
}
#[must_use]
pub(crate) fn doxygen_binds_forward<'a, T: Checker>(
comment: &Node<'a>,
code: &[u8],
ancestors: Ancestors<'a, '_>,
) -> bool {
let bytes = node_bytes(comment, code);
!doxygen_member_after(bytes)
&& !doxygen_standalone(bytes)
&& ancestors
.parent(comment)
.is_some_and(|parent| doxygen_container(&parent))
&& outside_function_bodies::<T>(comment, ancestors)
}
#[cfg(test)]
mod tests {
use super::*;
const STANDALONE: &[&[u8]] = &[
b"/** @file */",
b"/*! \\file x.c */",
b"/// @file",
b"/**\n *@file\n */",
b"/**\n * text\n * @file\n */",
b"/** <tt>@file</tt> */",
b"/** foo@file here */",
b"/** @mainpage */",
b"/** @page p Title */",
b"/** @dir */",
b"/** @example */",
b"/** @defgroup */",
b"/** @addtogroup g */",
b"/** @weakgroup */",
b"/** @name */",
b"/** @package */",
b"/** @class X */",
b"/** @struct S\n * d */",
b"/** @def M */",
b"/** @fn int f(void) */",
b"/** d @fn int f(void) */",
b"/** @namespace n */",
b"/** @concept K */",
b"/**\n * @code\n * x\n * @endcode\n * @file\n */",
b"/**\n * ```\n * x\n * ```\n * @file\n */",
b"/**\n * @verbatim x @endverbatim @file\n */",
b"/**\n * @htmlonly x @endhtmlonly\n * @file\n */",
b"/**\n * <!-- x -->\n * @file\n */",
b"/** `a` @file x `b` */",
b"/** `a\n *\n * @file x\n * b` */",
b"/** `a\n * @file x\n */",
b"/** @defgroup g.x G */",
b"/** (@defgroup g.x G) */",
b"/** a\\defgroup g.x G */",
b"/** a\\defgroup.g G */",
b"/** x @defgroup.g G */",
b"/** @var v */",
b"/** @typedef t */",
b"/** @property p */",
b"/** @overload void f(int) */",
b"/** @enum e */",
b"/** @union u */",
b"/** @interface I */",
b"/** @protocol P */",
b"/** @idlexcept E */",
b"/** @module m */",
b"/** @icode x @endicode @file */",
b"/** @iverbatim x @endiverbatim @file */",
b"/** @iliteral x @endiliteral @file */",
b"/** @latexonly x @endlatexonly @file */",
b"/** @xmlonly x @endxmlonly @file */",
b"/** @rtfonly x @endrtfonly @file */",
b"/** @manonly x @endmanonly @file */",
b"/** @docbookonly x @enddocbookonly @file */",
b"/** @dot x @enddot @file */",
b"/** @msc x @endmsc @file */",
b"/** @startuml x @enduml @file */",
b"/** <!-- x -->@file */",
];
const ATTACHED: &[&[u8]] = &[
b"/** d */",
b"/** @brief d */",
b"/** @ingroup g */",
b"/** @internal d */",
b"/** @copyright d */",
b"/** @File */",
b"/** @class */",
b"/** @fn\n * d */",
b"/** @struct */",
b"/** \\\\file x */",
b"/** @@file x */",
b"/** a@file.com */",
b"/** `@file` */",
b"/**\n * ```\n * @file\n * ```\n */",
b"/**\n * @code\n * @file\n * @endcode\n */",
b"/**\n * @verbatim\n * @file\n * @endverbatim\n */",
b"/** @filex */",
b"/** `a\n * @file x\n * b` */",
b"/** `\n * @file x\n * b` */",
b"/** a@file-x.com */",
b"/** @file_x */",
b"/**\n * @verbatim\n * @endcode @file\n * @endverbatim\n */",
b"/**\n * @htmlonly @file @endhtmlonly\n */",
b"/**\n * @htmlonly[block] @file @endhtmlonly\n */",
b"/**\n * @htmlonly\n * @endlatexonly @file\n * @endhtmlonly\n */",
b"/**\n * @latexonly\n * @file\n */",
b"/** @xmlonly @file @endxmlonly */",
b"/** @rtfonly @file @endrtfonly */",
b"/** @manonly @file @endmanonly */",
b"/** @docbookonly @file @enddocbookonly */",
b"/** @dot @file @enddot */",
b"/** @msc @file @endmsc */",
b"/** @startuml @file @enduml */",
b"/** @icode @file @endicode */",
b"/** @iverbatim @file @endiverbatim */",
b"/** @iliteral @file @endiliteral */",
b"/**\n * <!-- @file -->\n */",
b"/**\n * <!-- x --->\n * @file\n */",
b"/**\n * <!-- x --!>\n * @file\n */",
b"/**\n * <!-- x\n * @file\n */",
];
#[test]
fn doxygen_standalone_follows_commentscan() {
crate::test_support::assert_classifies(doxygen_standalone, STANDALONE, ATTACHED);
}
#[cfg(feature = "c")]
#[test]
fn conditional_declares_reads_each_conditional_once_per_walk() {
use crate::node::{FoldCache, Tree};
const DEPTH: usize = 40;
for (bottom, declares) in [("int x;\n", true), ("#include <x.h>\n", false)] {
let source = format!(
"{}{bottom}{}",
"/** d */\n#ifdef A\n".repeat(DEPTH),
"#endif\n".repeat(DEPTH)
);
let tree = Tree::new::<crate::langs::CCode>(source.as_bytes());
let nest: Vec<_> = tree
.get_root()
.preorder()
.filter(preproc_conditional)
.collect();
assert_eq!(nest.len(), DEPTH, "{bottom}");
let cache = FoldCache::default();
let walk = Ancestors::known(&[]).with_folds(&cache);
let before = conditionals_read::observed();
for level in &nest {
assert_eq!(conditional_declares(level, walk), declares, "{bottom}");
}
let memoised = conditionals_read::observed() - before;
let before = conditionals_read::observed();
for level in &nest {
assert_eq!(
conditional_declares(level, Ancestors::known(&[])),
declares,
"{bottom}"
);
}
let unmemoised = conditionals_read::observed() - before;
assert_eq!(memoised, DEPTH, "{bottom}");
if !declares {
assert_eq!(unmemoised, DEPTH * (DEPTH + 1) / 2, "{bottom}");
}
}
}
#[test]
fn doxygen_member_after_takes_the_four_markers() {
for after in [&b"/**< d */"[..], b"/*!< d */", b"///< d", b"//!< d"] {
assert!(doxygen_member_after(after));
}
for before in [&b"/** d */"[..], b"/// d", b"//! d", b"/*! d */"] {
assert!(!doxygen_member_after(before));
}
}
}