use crate::checker::Checker;
use crate::node::{Ancestors, FoldLane, Node};
#[derive(Clone, Copy, Debug)]
pub struct DocBinding {
pub kept: KeptDocs,
pub skips: for<'a, 'b> fn(&Node<'a>, Ancestors<'a, 'b>) -> bool,
pub forward: for<'a, 'b> fn(&Node<'a>, &[u8], Ancestors<'a, 'b>) -> bool,
pub gathers: Option<fn(&Node, &[u8]) -> bool>,
}
impl DocBinding {
#[must_use]
pub const fn new(kept: KeptDocs) -> Self {
Self {
kept,
skips: |_, _| false,
forward: |_, _, _| true,
gathers: None,
}
}
}
#[derive(Clone, Copy, Debug)]
pub enum KeptDocs {
All,
Last {
joins: fn(&Node, &Node, &[u8]) -> bool,
},
Nearest {
hidden: fn(&Node, &Node) -> bool,
},
Leading {
hidden: fn(&Node, &[u8]) -> bool,
joins: fn(&Node, &Node, &[u8]) -> bool,
after_code: fn(&Node) -> bool,
},
}
#[must_use]
pub(crate) fn never_joins(_: &Node, _: &Node, _: &[u8]) -> bool {
false
}
#[inline]
#[must_use]
pub(crate) fn node_bytes<'a>(node: &Node, code: &'a [u8]) -> &'a [u8] {
code.get(node.start_byte()..node.end_byte()).unwrap_or(&[])
}
#[inline]
#[must_use]
pub(crate) fn javadoc_block(s: &[u8]) -> bool {
matches!(s, [b'/', b'*', b'*', b, ..] if *b != b'*' && *b != b'/')
}
#[inline]
#[must_use]
pub(crate) fn tsdoc_block(s: &[u8]) -> bool {
matches!(s, [b'/', b'*', b'*', b, ..] if *b != b'/')
}
#[inline]
#[must_use]
fn triple_slash(s: &[u8]) -> bool {
matches!(s, [b'/', b'/', b'/', rest @ ..] if rest.first() != Some(&b'/'))
}
#[inline]
#[must_use]
pub(crate) fn doxygen(s: &[u8]) -> bool {
javadoc_block(s) || s.starts_with(b"/*!") || triple_slash(s) || s.starts_with(b"//!")
}
#[inline]
#[must_use]
pub(crate) fn java_doc(s: &[u8]) -> bool {
kotlin_doc(s) || triple_slash(s)
}
#[inline]
#[must_use]
pub(crate) fn kotlin_doc(s: &[u8]) -> bool {
s.starts_with(b"/**") && s != b"/**/"
}
#[inline]
#[must_use]
pub(crate) fn phpdoc(s: &[u8]) -> bool {
s.starts_with(b"/**") && s.get(3).is_some_and(u8::is_ascii_whitespace)
}
#[inline]
#[must_use]
pub(crate) fn csharp_doc(s: &[u8]) -> bool {
triple_slash(s) || javadoc_block(s)
}
fn jsdoc_line(line: &[u8]) -> &[u8] {
let line = line.trim_ascii_start();
let line = line.strip_prefix(b"*").unwrap_or(line);
line.strip_prefix(b" ").unwrap_or(line)
}
fn jsdoc_lines(s: &[u8]) -> impl Iterator<Item = &[u8]> + Clone {
let body = s.strip_prefix(b"/**").unwrap_or(s);
let body = body.strip_suffix(b"*/").unwrap_or(body);
let start = body.iter().position(|&b| b != b'*').unwrap_or(body.len());
let end = body
.iter()
.rposition(|&b| b != b'*')
.map_or(start, |at| at + 1);
body[start..end].split(|&b| b == b'\n').map(jsdoc_line)
}
fn jsdoc_tag_open(line: &[u8]) -> Option<(&[u8], &[u8])> {
let tag = line.trim_ascii_start().strip_prefix(b"@")?;
let title = tag.iter().take_while(|b| !b.is_ascii_whitespace()).count();
(title > 0).then(|| tag.split_at(title))
}
fn jsdoc_names<'a>(lines: impl IntoIterator<Item = &'a [u8]>) -> bool {
let mut bytes = lines
.into_iter()
.flat_map(|line| line.iter().copied())
.skip_while(u8::is_ascii_whitespace)
.peekable();
if bytes.peek() == Some(&b'{') {
let mut depth = 0_usize;
for b in bytes.by_ref() {
match b {
b'{' => depth += 1,
b'}' => depth -= 1,
_ => {}
}
if depth == 0 {
break;
}
}
}
bytes.any(|b| !b.is_ascii_whitespace())
}
#[must_use]
pub(crate) fn jsdoc_virtual(s: &[u8]) -> bool {
const ALWAYS: [&[u8]; 4] = [b"file", b"fileoverview", b"overview", b"module"];
const NAMING: [&[u8]; 18] = [
b"name",
b"function",
b"func",
b"method",
b"class",
b"constructor",
b"constant",
b"const",
b"member",
b"var",
b"namespace",
b"mixin",
b"interface",
b"event",
b"external",
b"host",
b"typedef",
b"callback",
];
let is = |title: &[u8], names: &[&[u8]]| names.iter().any(|n| title.eq_ignore_ascii_case(n));
let mut lines = jsdoc_lines(s);
while let Some(line) = lines.next() {
let Some((title, text)) = jsdoc_tag_open(line) else {
continue;
};
if is(title, &ALWAYS) {
return true;
}
let rest = lines.clone().take_while(|l| jsdoc_tag_open(l).is_none());
if is(title, &NAMING) && jsdoc_names(std::iter::once(text).chain(rest)) {
return true;
}
}
false
}
#[must_use]
pub(crate) fn tsdoc_module(s: &[u8]) -> bool {
jsdoc_lines(s)
.filter_map(jsdoc_tag_open)
.any(|(title, _)| title == b"module" || title == b"packageDocumentation")
}
#[must_use]
pub(crate) fn outside_function_bodies<'a, T: Checker>(
comment: &Node<'a>,
ancestors: Ancestors<'a, '_>,
) -> bool {
!held_in_body::<T>(comment, 0, ancestors)
}
fn held_in_body<'a, T: Checker>(
node: &Node<'a>,
skip: usize,
ancestors: Ancestors<'a, '_>,
) -> bool {
ancestors.fold_down(
node,
skip,
FoldLane::DocInBody,
0,
|held, ancestor, outer, below| {
u8::from(held != 0 || T::holds_doc_comment_in_body(ancestor, outer, below))
},
) != 0
}
#[must_use]
pub fn documents_where_it_sits<'a, T: Checker>(
node: &Node<'a>,
code: &[u8],
ancestors: Ancestors<'a, '_>,
) -> bool {
if T::DOC_COMMENTS_IN_FUNCTION_BODIES {
return true;
}
let mut below = *node;
for (ancestor, outer) in ancestors.iter(node).take(2) {
if T::holds_doc_comment_in_body(&ancestor, outer, &below)
&& !T::doc_comment_opens_body(node, &ancestor, &below, code)
{
return false;
}
below = ancestor;
}
!held_in_body::<T>(node, 2, ancestors)
}
#[must_use]
pub fn documents_unbound<'a, T: Checker>(
comment: &Node<'a>,
code: &[u8],
ancestors: Ancestors<'a, '_>,
) -> bool {
T::DOC_BINDING.is_none_or(|binding| !(binding.forward)(comment, code, ancestors))
}
pub fn bound_doc_run<'a, T: Checker>(
comment: &Node<'a>,
code: &[u8],
ancestors: Ancestors<'a, '_>,
mut credit: impl FnMut(&Node<'a>),
) {
let Some(binding) = T::DOC_BINDING else {
return;
};
let end = match stretch_end::<T>(comment, code, ancestors, binding.skips) {
StretchEnd::Undecided => return,
StretchEnd::Elsewhere if binding.gathers.is_none() => return,
end => end,
};
if !documents_where_it_sits::<T>(comment, code, ancestors) {
return;
}
let (stretch, code_before) = collect_stretch::<T>(comment, code, ancestors, binding.skips);
let forward =
|n: &Node<'a>| T::is_doc_comment(n, code) && (binding.forward)(n, code, ancestors);
let StretchEnd::AtTarget { next, target } = end else {
if let Some(gathers) = binding.gathers {
bind_gathered::<T>(&stretch, code, gathers, forward)
.iter()
.for_each(&mut credit);
}
return;
};
let kept = match binding.kept {
KeptDocs::All => stretch.iter().copied().filter(forward).collect(),
KeptDocs::Last { joins } => bind_last::<T>(&stretch, code, joins, forward),
KeptDocs::Nearest { hidden } => bind_nearest::<T>(&stretch, next, hidden, forward)
.into_iter()
.collect(),
KeptDocs::Leading {
hidden,
joins,
after_code,
} => {
let after_code_row = code_before
.filter(|_| !after_code(&target))
.map(|before| before.end_row());
let attaches = |n: &Node<'a>| forward(n) && Some(n.start_row()) != after_code_row;
bind_leading::<T>(&stretch, code, hidden, joins, attaches)
}
};
kept.iter().for_each(&mut credit);
}
enum StretchEnd<'a> {
Undecided,
AtTarget { next: Node<'a>, target: Node<'a> },
Elsewhere,
}
fn stretch_end<'a, T: Checker>(
comment: &Node<'a>,
code: &[u8],
ancestors: Ancestors<'a, '_>,
skips: for<'b> fn(&Node<'a>, Ancestors<'a, 'b>) -> bool,
) -> StretchEnd<'a> {
let mut after = ancestors.siblings_after(comment).peekable();
let Some(&next) = after.peek() else {
return StretchEnd::Elsewhere;
};
match after.find(|n| !skips(n, ancestors) && !read_past_error::<T>(n, code, ancestors)) {
Some(target) if T::is_comment(&target) => StretchEnd::Undecided,
Some(target)
if T::is_doc_target(&target, code, ancestors)
|| (target.is_error() && error_holds_target::<T>(&target, code, ancestors)) =>
{
StretchEnd::AtTarget { next, target }
}
_ => StretchEnd::Elsewhere,
}
}
fn collect_stretch<'a, T: Checker>(
comment: &Node<'a>,
code: &[u8],
ancestors: Ancestors<'a, '_>,
skips: for<'b> fn(&Node<'a>, Ancestors<'a, 'b>) -> bool,
) -> (Vec<Node<'a>>, Option<Node<'a>>) {
let mut stretch = vec![*comment];
let mut before = None;
for sibling in ancestors
.siblings_before(comment)
.filter(|sibling| !read_past_error::<T>(sibling, code, ancestors))
{
if T::is_comment(&sibling) || skips(&sibling, ancestors) {
stretch.push(sibling);
} else {
before = Some(sibling);
break;
}
}
stretch.reverse();
(stretch, before)
}
fn read_past_error<'a, T: Checker>(
node: &Node<'a>,
code: &[u8],
ancestors: Ancestors<'a, '_>,
) -> bool {
node.is_error()
&& !T::is_doc_target(node, code, ancestors)
&& !error_holds_target::<T>(node, code, ancestors)
}
fn error_holds_target<'a, T: Checker>(
error: &Node<'a>,
code: &[u8],
ancestors: Ancestors<'a, '_>,
) -> bool {
let chain = ancestors.children_chain(error);
let inside = chain
.as_deref()
.map_or_else(Ancestors::unknown, Ancestors::known);
error
.children()
.any(|child| child.is_named() && T::is_doc_target(&child, code, inside))
}
fn bind_gathered<'a, T: Checker>(
stretch: &[Node<'a>],
code: &[u8],
gathers: fn(&Node, &[u8]) -> bool,
forward: impl Fn(&Node<'a>) -> bool,
) -> Vec<Node<'a>> {
let Some(gatherer) = stretch
.iter()
.rposition(|n| T::is_doc_comment(n, code) && gathers(n, code))
else {
return Vec::new();
};
let line = |n: &Node| node_bytes(n, code).starts_with(b"//");
let gathers_below = line(&stretch[gatherer]);
let mut last = gatherer;
while let Some(below) = stretch.get(last + 1)
&& gathers_below
&& line(below)
&& T::is_doc_comment(below, code)
&& below.start_row() == stretch[last].end_row().saturating_add(1)
{
last += 1;
}
stretch[..=last].iter().copied().filter(forward).collect()
}
fn bind_last<'a, T: Checker>(
stretch: &[Node<'a>],
code: &[u8],
joins: fn(&Node, &Node, &[u8]) -> bool,
forward: impl Fn(&Node<'a>) -> bool,
) -> Vec<Node<'a>> {
let split = stretch
.iter()
.position(|n| !T::is_comment(n))
.unwrap_or(stretch.len());
let (before, after) = stretch.split_at(split);
let Some(last) = before.iter().rposition(&forward) else {
return after.iter().copied().find(forward).into_iter().collect();
};
joined_run(before, last, code, joins, forward)
}
fn bind_leading<'a, T: Checker>(
stretch: &[Node<'a>],
code: &[u8],
hidden: fn(&Node, &[u8]) -> bool,
joins: fn(&Node, &Node, &[u8]) -> bool,
forward: impl Fn(&Node<'a>) -> bool,
) -> Vec<Node<'a>> {
let first_token = stretch
.iter()
.position(|n| !T::is_comment(n) && !hidden(n, code))
.unwrap_or(stretch.len());
let before = &stretch[..first_token];
let Some(last) = before.iter().rposition(|n| !hidden(n, code)) else {
return Vec::new();
};
if !forward(&before[last]) {
return Vec::new();
}
joined_run(before, last, code, joins, forward)
}
fn joined_run<'a>(
before: &[Node<'a>],
last: usize,
code: &[u8],
joins: fn(&Node, &Node, &[u8]) -> bool,
forward: impl Fn(&Node<'a>) -> bool,
) -> Vec<Node<'a>> {
let mut first = last;
while first > 0
&& joins(&before[first - 1], &before[first], code)
&& forward(&before[first - 1])
{
first -= 1;
}
before[first..=last].to_vec()
}
fn bind_nearest<'a, T: Checker>(
stretch: &[Node<'a>],
next: Node<'a>,
hidden: fn(&Node, &Node) -> bool,
forward: impl Fn(&Node<'a>) -> bool,
) -> Option<Node<'a>> {
let mut later = next;
for member in stretch.iter().rev() {
if T::is_comment(member) && !hidden(member, &later) {
return Some(*member).filter(forward);
}
later = *member;
}
None
}
pub fn leading_doc_run<'a, T: Checker>(
comment: &Node<'a>,
code: &[u8],
ancestors: Ancestors<'a, '_>,
mut credit: impl FnMut(usize, usize),
) {
let next = ancestors.siblings_after(comment).next();
if next
.as_ref()
.is_some_and(|n| run_continues_to::<T>(comment, n, code))
{
return;
}
let Some(first) = run_first_member::<T>(comment, code, ancestors) else {
return;
};
let below = next.filter(|n| T::doc_target_start_row(n) == comment.end_row().saturating_add(1));
let documents = below.is_some_and(|n| T::is_doc_target(&n, code, ancestors))
|| T::is_doc_run_opener(&first, below.as_ref(), code, ancestors);
if !documents {
return;
}
for member in std::iter::once(first).chain(ancestors.siblings_after(&first)) {
if member.start_byte() > comment.start_byte() {
break;
}
if !T::is_doc_run_directive(&member, code) {
credit(member.start_row(), member.end_row());
}
}
}
pub(crate) fn run_continues_to<T: Checker>(comment: &Node, next: &Node, code: &[u8]) -> bool {
T::is_comment(next)
&& (next.start_row() <= comment.end_row()
|| (next.start_row() == comment.end_row().saturating_add(1)
&& !T::ends_doc_run(comment, code)))
}
fn run_first_member<'a, T: Checker>(
comment: &Node<'a>,
code: &[u8],
ancestors: Ancestors<'a, '_>,
) -> Option<Node<'a>> {
let mut before = ancestors.siblings_before(comment);
let mut first = None;
let mut row_first = *comment;
loop {
match before.next() {
Some(prev) if prev.end_row() >= row_first.start_row() => {
if !T::is_comment(&prev) {
return first;
}
row_first = prev;
}
Some(prev)
if T::is_comment(&prev)
&& prev.end_row().saturating_add(1) == row_first.start_row()
&& !T::ends_doc_run(&prev, code) =>
{
first = Some(row_first);
row_first = prev;
}
_ => return Some(row_first),
}
}
}
#[must_use]
pub(crate) fn go_directive(s: &[u8]) -> bool {
let Some(rest) = s.strip_prefix(b"//") else {
return false;
};
if rest.starts_with(b"line ") || rest.starts_with(b"extern ") || rest.starts_with(b"export ") {
return true;
}
let word = rest
.iter()
.take_while(|b| b.is_ascii_lowercase() || b.is_ascii_digit())
.count();
word > 0
&& rest.get(word) == Some(&b':')
&& rest
.get(word.saturating_add(1))
.is_some_and(|b| b.is_ascii_lowercase() || b.is_ascii_digit())
}
#[must_use]
pub(crate) fn ruby_directive_opener(s: &[u8]) -> bool {
if s.starts_with(b"##") && s.get(2) != Some(&b'#') {
return true;
}
s.strip_prefix(b"#").is_some_and(|rest| {
rest.iter()
.position(|b| *b != b' ' && *b != b'\t')
.is_some_and(|at| rest[at..].starts_with(b"@!"))
})
}
#[must_use]
pub(crate) fn ruby_magic_comment(s: &[u8]) -> bool {
if s.starts_with(b"#!") {
return true;
}
s.strip_prefix(b"#")
.map(<[u8]>::trim_ascii)
.is_some_and(|body| ruby_modeline(body) || ruby_magic_setting(body))
}
fn contains_ci(hay: &[u8], needle: &[u8]) -> bool {
hay.windows(needle.len())
.any(|window| window.eq_ignore_ascii_case(needle))
}
fn ruby_modeline(body: &[u8]) -> bool {
if body.len() >= 6 && body.starts_with(b"-*-") && body.ends_with(b"-*-") {
return contains_ci(body, b"coding") || contains_ci(body, b"frozen");
}
body.get(..4)
.is_some_and(|head| head.eq_ignore_ascii_case(b"vim:"))
&& contains_ci(body, b"coding")
}
fn ruby_magic_setting(body: &[u8]) -> bool {
let Some(separator) = body.iter().position(|b| matches!(b, b':' | b'=')) else {
return false;
};
let (key, value) = body.split_at(separator);
let value = value.get(1..).unwrap_or_default().trim_ascii_start();
[
&b"coding"[..],
b"encoding",
b"frozen_string_literal",
b"frozen-string-literal",
]
.iter()
.any(|known| key.trim_ascii_end().eq_ignore_ascii_case(known))
&& !value.is_empty()
&& value
.iter()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.'))
}
#[must_use]
pub(crate) fn pod_comment_region(s: &[u8]) -> bool {
enum Region {
Outside,
Paragraph,
Begin,
}
let mut region = Region::Outside;
let mut regions = 0_usize;
for line in s.split(|&byte| byte == b'\n') {
let blank = line.trim_ascii().is_empty();
match region {
Region::Paragraph if blank => region = Region::Outside,
Region::Begin if pod_command(line, b"=end").is_some_and(is_comment_format) => {
region = Region::Outside;
}
Region::Paragraph | Region::Begin => {}
Region::Outside if blank => {}
Region::Outside => {
if pod_command(line, b"=for").is_some_and(is_comment_format) {
region = Region::Paragraph;
} else if pod_command(line, b"=begin").is_some_and(is_comment_format) {
region = Region::Begin;
} else {
return regions > 0 && line.starts_with(b"=cut");
}
regions += 1;
}
}
}
regions > 0
}
fn pod_command<'s>(line: &'s [u8], command: &[u8]) -> Option<&'s [u8]> {
let rest = line.strip_prefix(command)?;
rest.first()
.is_some_and(u8::is_ascii_whitespace)
.then(|| rest.trim_ascii_start())
}
fn is_comment_format(format: &[u8]) -> bool {
format
.strip_prefix(b"comment")
.is_some_and(|after| after.first().is_none_or(u8::is_ascii_whitespace))
}
#[must_use]
pub(crate) fn ldoc_opener(s: &[u8]) -> bool {
if let Some(body) = lua_long_comment_body(s) {
return body.first() == Some(&b'-');
}
s.starts_with(b"---") && !ldoc_end_marker(s)
}
#[must_use]
pub(crate) fn lua_long_comment(s: &[u8]) -> bool {
lua_long_comment_body(s).is_some()
}
fn lua_long_comment_body(s: &[u8]) -> Option<&[u8]> {
let rest = s.strip_prefix(b"--[")?;
let levels = rest.iter().take_while(|b| **b == b'=').count();
rest.get(levels..)?.strip_prefix(b"[")
}
fn ldoc_end_marker(s: &[u8]) -> bool {
let trimmed = s.len() - s.iter().rev().take_while(|b| **b != b'-').count();
let dashes = s[..trimmed]
.iter()
.rev()
.take_while(|b| **b == b'-')
.count();
dashes >= 2 && trimmed > dashes
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "rust")]
#[test]
fn doc_binding_defaults_skip_nothing_and_bind_everything_forward() {
use crate::langs::RustCode;
use crate::node::Tree;
let source = b"/// d\nfn f() {}\n";
let tree = Tree::new::<RustCode>(source);
let comment = tree
.get_root()
.preorder()
.find(|node| RustCode::is_comment(node))
.expect("the fixture has a comment");
let binding = DocBinding::new(KeptDocs::All);
assert!(matches!(binding.kept, KeptDocs::All));
assert!(!(binding.skips)(&comment, Ancestors::unknown()));
assert!((binding.forward)(&comment, source, Ancestors::unknown()));
assert!(binding.gathers.is_none());
}
#[test]
fn ruby_directive_opener_accepts_rdoc_and_yard_forms() {
assert!(ruby_directive_opener(b"##"));
assert!(ruby_directive_opener(b"## Section"));
assert!(ruby_directive_opener(b"# @!method foo"));
assert!(ruby_directive_opener(b"#\t@!macro bar"));
assert!(!ruby_directive_opener(b"# plain"));
assert!(!ruby_directive_opener(b"# @param x"));
assert!(!ruby_directive_opener(b"#!/usr/bin/env ruby"));
assert!(!ruby_directive_opener(b"###"));
assert!(!ruby_directive_opener(b"########################"));
assert!(!ruby_directive_opener(b"##### Private helpers #####"));
}
#[cfg(feature = "rust")]
#[test]
fn both_scans_read_past_the_same_error() {
use crate::langs::RustCode;
use crate::node::Tree;
let source = b"unsafe extern \"C\" {\n /// a\n safe\n /// b\n fn g();\n}\n";
let tree = Tree::new::<RustCode>(source);
let comments: Vec<_> = tree
.get_root()
.preorder()
.filter(|node| RustCode::is_comment(node))
.collect();
assert_eq!(comments.len(), 2);
assert!(
tree.get_root().preorder().any(|node| node.is_error()),
"the fixture no longer parses `safe` as an error"
);
let mut credited = Vec::new();
for comment in &comments {
bound_doc_run::<RustCode>(comment, source, Ancestors::unknown(), |doc| {
credited.push(doc.start_row());
});
}
assert_eq!(credited, [1, 3]);
}
#[test]
fn javadoc_block_accepts_the_block_form_only() {
assert!(javadoc_block(b"/** doc */"));
assert!(javadoc_block(b"/**\n * doc\n */"));
assert!(!javadoc_block(b"/**/"), "empty block");
assert!(!javadoc_block(b"/*** banner */"), "three stars");
assert!(!javadoc_block(b"/* plain */"));
assert!(!javadoc_block(b"// plain"));
assert!(!javadoc_block(b"/**"), "nothing after the marker");
}
#[test]
fn triple_slash_rejects_a_fourth_slash() {
assert!(triple_slash(b"/// doc"));
assert!(triple_slash(b"///"), "bare marker still opens a doc line");
assert!(triple_slash(b"///< after member"));
assert!(!triple_slash(b"//// banner"));
assert!(!triple_slash(b"// plain"));
}
#[test]
fn doxygen_covers_the_four_markers_and_their_member_forms() {
for doc in [
&b"/** d */"[..],
b"/*! d */",
b"/// d",
b"//! d",
b"/**< d */",
b"/*!< d */",
b"///< d",
b"//!< d",
] {
assert!(doxygen(doc), "{}", String::from_utf8_lossy(doc));
}
for plain in [&b"/* d */"[..], b"// d", b"/**/", b"/*** d */", b"//// d"] {
assert!(!doxygen(plain), "{}", String::from_utf8_lossy(plain));
}
}
#[test]
fn java_doc_follows_javac_on_three_stars_and_jep_467() {
assert!(java_doc(b"/** d */"));
assert!(java_doc(b"/*** d */"), "javac discards leading asterisks");
assert!(java_doc(b"/// d"), "JEP 467 Markdown doc comment");
assert!(!java_doc(b"/**/"));
assert!(!java_doc(b"//// d"));
assert!(!java_doc(b"/* d */"));
assert!(!java_doc(b"// d"));
}
#[test]
fn kotlin_doc_is_any_block_but_the_empty_one() {
assert!(kotlin_doc(b"/** d */"));
assert!(kotlin_doc(b"/*** d */"));
assert!(!kotlin_doc(b"/**/"));
assert!(!kotlin_doc(b"/// d"), "KDoc has no line form");
}
#[test]
fn phpdoc_needs_whitespace_after_the_marker() {
assert!(phpdoc(b"/** d */"));
assert!(phpdoc(b"/**\n * d\n */"));
assert!(!phpdoc(b"/**/"));
assert!(!phpdoc(b"/*** d */"));
assert!(!phpdoc(b"/**d */"));
assert!(!phpdoc(b"# d"));
assert!(!phpdoc(b"// d"));
}
#[test]
fn csharp_doc_rejects_the_doxygen_only_spellings() {
assert!(csharp_doc(b"/// <summary>d</summary>"));
assert!(csharp_doc(b"/** d */"));
for plain in [
&b"//! d"[..],
b"/*! d */",
b"//// d",
b"/**/",
b"/*** d */",
b"// d",
] {
assert!(!csharp_doc(plain), "{}", String::from_utf8_lossy(plain));
}
}
#[test]
fn jsdoc_virtual_follows_jsdoc_naming_tags() {
crate::test_support::assert_classifies(
jsdoc_virtual,
&[
b"/** @file */",
b"/**\n * @fileoverview x\n */",
b"/** @overview */",
b"/** @module */",
b"/**\n * @typedef {Object} T\n */",
b"/**\n * @typedef {Object}\n * T\n */",
b"/**\n * @function f\n */",
b"/**\n * @callback cb\n */",
b"/** @FILE */",
b"/** @FileOverview */",
b"/** @Typedef {Object} T */",
b"/** @function f **/",
],
&[
b"/**\n * @license MIT\n */",
b"/**\n * @typedef {Object}\n */",
b"/**\n * @function\n */",
b"/** @function **/",
b"/**\n * @typedef {{a: number}}\n */",
b"/** x @file */",
b"/** d */",
],
);
assert!(tsdoc_module(b"/**\n * @packageDocumentation\n */"));
assert!(tsdoc_module(b"/**\n * @module m\n */"));
assert!(!tsdoc_module(b"/**\n * @license MIT\n */"));
}
#[test]
fn go_directive_matches_go_ast_is_directive() {
for directive in [
&b"//go:build linux"[..],
b"//go:generate go run x",
b"//line foo.go:12",
b"//extern f",
b"//export F",
b"//nolint:errcheck",
b"//x:1",
] {
assert!(
go_directive(directive),
"{}",
String::from_utf8_lossy(directive)
);
}
for text in [
&b"// go:build linux"[..],
b"//Note: not a directive",
b"//x:",
b"//exported F",
b"// d",
] {
assert!(!go_directive(text), "{}", String::from_utf8_lossy(text));
}
}
#[test]
fn pod_comment_region_is_the_comment_format_only() {
for region in [
&b"=begin comment\n\nold();\n\n=end comment\n\n=cut"[..],
b"=for comment\nold();\n\n=cut",
b"=begin comment\n=end comment\n=cut",
b"=for comment\nold();\n",
b"=begin comment\nold();\n",
b"=for comment a\n\n=for comment b\n\n=cut",
b"=begin comment\nx\n=end comment\n=for comment y\n\n=cut",
b"=for comment\r\nold();\r\n\r\n=cut\r\n",
] {
assert!(
pod_comment_region(region),
"{}",
String::from_utf8_lossy(region)
);
}
for doc in [
&b"=pod\n\nText.\n\n=cut"[..],
b"=head1 NAME\n\nFoo\n\n=cut",
b"=begin html\n\n<p>x</p>\n\n=end html\n\n=cut",
b"=begin comments\n=end comments\n=cut",
b"=beginning",
b"=for comment\nInternal note.\n\n=head1 NAME\n\nFoo - bar\n\n=cut",
b"=begin comment\n\nold();\n\n=end comment\n\n=head1 X\n\nDoc.\n\n=cut",
b"=for comment\nnote\n\nPlain text paragraph.\n\n=cut",
] {
assert!(!pod_comment_region(doc), "{}", String::from_utf8_lossy(doc));
}
}
#[test]
fn ruby_magic_comment_matches_whole_magic_lines_only() {
for magic in [
&b"#!/usr/bin/env ruby"[..],
b"# coding: utf-8",
b"# encoding: UTF-8",
b"# Encoding: binary",
b"#coding=utf-8",
b"# -*- coding: utf-8 -*-",
b"# -*- mode: ruby; encoding: utf-8 -*-",
b"# -*- frozen_string_literal: true -*-",
b"# vim: set fileencoding=utf-8 :",
b"# frozen_string_literal: true",
b"# frozen-string-literal: false",
] {
assert!(
ruby_magic_comment(magic),
"{}",
String::from_utf8_lossy(magic)
);
}
for doc in [
&b"# Encoding: defaults to UTF-8."[..],
b"# Handles transcoding: see below",
b"# Pass `frozen_string_literal: true` to opt in.",
b"# Sets the encoding= attribute",
b"# d",
b"#",
] {
assert!(!ruby_magic_comment(doc), "{}", String::from_utf8_lossy(doc));
}
}
}