#![allow(clippy::wildcard_imports, clippy::enum_glob_use)]
use std::sync::OnceLock;
use aho_corasick::AhoCorasick;
use regex::bytes::Regex;
use crate::cfg_predicate::attribute_marks_test as rust_attribute_marks_test;
use crate::macros::csharp_invocation_expr_kinds;
use crate::*;
static AHO_CORASICK: OnceLock<AhoCorasick> = OnceLock::new();
static RE: OnceLock<Regex> = OnceLock::new();
macro_rules! js_ancestor_walk {
(
$node:ident,
$ancestors:ident,
[$($up:ident)|+],
[$($stop:ident)|+],
$extra:expr $(,)?
) => {
$node.count_specific_ancestors::<Self>(
$ancestors,
|node| matches!(node.kind_id().into(), $($up)|+),
|node| matches!(node.kind_id().into(), $($stop)|+),
) > 0
|| $extra
};
}
macro_rules! check_if_func {
($node: ident, $ancestors: ident, $field_definition: ident) => {
js_ancestor_walk!(
$node,
$ancestors,
[VariableDeclarator
| AssignmentExpression
| LabeledStatement
| Pair
| $field_definition],
[StatementBlock | ReturnStatement | NewExpression | CallExpression | Arguments],
$node.is_child(Identifier as u16),
)
};
}
macro_rules! check_if_arrow_func {
($node: ident, $ancestors: ident, $field_definition: ident) => {
js_ancestor_walk!(
$node,
$ancestors,
[VariableDeclarator
| AssignmentExpression
| LabeledStatement
| Pair
| $field_definition],
[StatementBlock | ReturnStatement | NewExpression | CallExpression | Arguments],
$node.has_sibling($ancestors, PropertyIdentifier as u16),
)
};
}
macro_rules! is_js_func {
($node: ident, $ancestors: ident, $field_definition: ident) => {
match $node.kind_id().into() {
FunctionDeclaration | GeneratorFunctionDeclaration | MethodDefinition => true,
FunctionExpression | GeneratorFunction => {
check_if_func!($node, $ancestors, $field_definition)
}
ArrowFunction => check_if_arrow_func!($node, $ancestors, $field_definition),
_ => false,
}
};
}
macro_rules! is_js_closure {
($node: ident, $ancestors: ident, $field_definition: ident) => {
match $node.kind_id().into() {
FunctionExpression | GeneratorFunction => {
!check_if_func!($node, $ancestors, $field_definition)
}
ArrowFunction => !check_if_arrow_func!($node, $ancestors, $field_definition),
_ => false,
}
};
}
macro_rules! is_js_func_and_closure_checker {
($language: ident, $field_definition: ident) => {
#[inline]
fn is_func<'a>(node: &Node<'a>, ancestors: Ancestors<'a, '_>) -> bool {
use $language::*;
is_js_func!(node, ancestors, $field_definition)
}
#[inline]
fn is_closure<'a>(node: &Node<'a>, ancestors: Ancestors<'a, '_>) -> bool {
use $language::*;
is_js_closure!(node, ancestors, $field_definition)
}
};
}
macro_rules! impl_js_family_is_string {
($lang:ident $(, $extra:ident)* $(,)?) => {
fn is_string(node: &Node) -> bool {
matches!(
node.kind_id().into(),
$lang::String | $lang::String2 | $lang::TemplateString
$(| $lang::$extra)*
)
}
};
}
macro_rules! impl_simple_is_string {
($lang:ident, $first:ident $(, $rest:ident)* $(,)?) => {
fn is_string(node: &Node) -> bool {
matches!(
node.kind_id().into(),
$lang::$first $(| $lang::$rest)*
)
}
};
}
macro_rules! impl_is_else_if_parent_clause {
($lang:ident, $if_kind:ident, $else_clause:ident) => {
#[inline]
fn is_else_if<'a>(node: &Node<'a>, ancestors: Ancestors<'a, '_>) -> bool {
node.kind_id() == $lang::$if_kind
&& ancestors
.parent(node)
.is_some_and(|parent| parent.kind_id() == $lang::$else_clause)
}
};
}
macro_rules! impl_is_else_if_prev_sibling {
($lang:ident, $if_kind:ident, $else_kw:ident) => {
#[inline]
fn is_else_if<'a>(node: &Node<'a>, ancestors: Ancestors<'a, '_>) -> bool {
node.kind_id() == $lang::$if_kind
&& ancestors
.previous_sibling(node)
.is_some_and(|prev| prev.kind_id() == $lang::$else_kw)
}
};
}
macro_rules! impl_is_else_if_clause {
($lang:ident, $first:ident $(, $rest:ident)* $(,)?) => {
#[inline]
fn is_else_if(node: &Node, _ancestors: Ancestors<'_, '_>) -> bool {
matches!(
node.kind_id().into(),
$lang::$first $(| $lang::$rest)*
)
}
};
}
#[inline]
fn get_aho_corasick_match(code: &[u8]) -> bool {
AHO_CORASICK
.get_or_init(|| {
AhoCorasick::new(vec![b"<div rustbindgen"])
.expect("constant single-needle AhoCorasick automaton always compiles")
})
.is_match(code)
}
#[doc(hidden)]
pub(crate) trait Checker {
#[inline]
fn is_comment(_: &Node) -> bool {
false
}
#[inline]
fn is_useful_comment<'a>(_: &Node<'a>, _: &[u8], _: Ancestors<'a, '_>) -> bool {
false
}
#[inline]
fn is_func_space(_: &Node) -> bool {
false
}
#[inline]
fn is_func<'a>(_: &Node<'a>, _ancestors: Ancestors<'a, '_>) -> bool {
false
}
#[inline]
fn is_closure<'a>(_: &Node<'a>, _ancestors: Ancestors<'a, '_>) -> bool {
false
}
#[inline]
fn is_call(_: &Node) -> bool {
false
}
#[inline]
fn is_non_arg(_: &Node) -> bool {
false
}
#[inline]
fn is_empty_param_marker(_param: &Node, _code: &[u8]) -> bool {
false
}
#[inline]
fn is_bare_param(_: &Node) -> bool {
false
}
#[inline]
fn is_string(_: &Node) -> bool {
false
}
#[inline]
fn is_else_if(_: &Node, _: Ancestors<'_, '_>) -> bool {
false
}
#[inline]
fn is_primitive(_node: &Node) -> bool {
false
}
fn is_error(node: &Node) -> bool {
node.has_error()
}
#[inline]
fn should_skip_subtree<'a>(
_node: &Node<'a>,
_code: &[u8],
_ancestors: Ancestors<'a, '_>,
) -> bool {
false
}
#[inline]
fn is_func_space_with_code<'a>(
node: &Node<'a>,
_code: &[u8],
_ancestors: Ancestors<'a, '_>,
) -> bool {
Self::is_func_space(node)
}
#[inline]
fn is_func_with_code<'a>(node: &Node<'a>, _code: &[u8], ancestors: Ancestors<'a, '_>) -> bool {
Self::is_func(node, ancestors)
}
#[inline]
fn promotes_to_func_space_with_code<'a>(
node: &Node<'a>,
code: &[u8],
ancestors: Ancestors<'a, '_>,
) -> bool {
Self::is_func_with_code(node, code, ancestors)
|| Self::is_func_space_with_code(node, code, ancestors)
}
}
mod bash;
mod c;
mod ccomment;
mod cpp;
mod csharp;
mod elixir;
mod go;
mod groovy;
mod irules;
mod java;
mod javascript;
mod kotlin;
mod lua;
mod mozcpp;
mod mozjs;
mod objc;
mod perl;
mod php;
mod preproc;
mod python;
mod ruby;
mod rust;
mod tcl;
mod tsx;
mod typescript;
pub(crate) fn java_anonymous_class_body<'a>(node: &Node<'a>) -> Option<Node<'a>> {
node.first_child(|id| id == Java::ClassBody as u16)
}
pub(crate) fn csharp_accessor_count(node: &Node) -> usize {
node.children()
.filter(|c| c.kind_id() == Csharp::AccessorList as u16)
.flat_map(|list| list.children())
.filter(|c| c.kind_id() == Csharp::AccessorDeclaration as u16)
.count()
}
pub(crate) fn csharp_member_has_accessors(node: &Node) -> bool {
csharp_accessor_count(node) > 0
}
pub(crate) fn c_family_void_parameter(param: &Node, code: &[u8]) -> bool {
param.child_by_field_name("declarator").is_none()
&& param
.child_by_field_name("type")
.and_then(|ty| code.get(ty.start_byte()..ty.end_byte()))
== Some(b"void".as_slice())
}
fn rust_attribute_body<'a>(text: &'a str, marker: &str) -> Option<&'a str> {
text.trim()
.strip_prefix(marker)
.and_then(|t| t.trim_start().strip_prefix('['))
.and_then(|t| t.trim().strip_suffix(']'))
}
fn rust_attribute_node_marks_test(node: &Node, code: &[u8], marker: &str) -> bool {
node.utf8_text(code)
.and_then(|text| rust_attribute_body(text, marker))
.is_some_and(rust_attribute_marks_test)
}
fn rust_item_is_test_only<'a>(node: &Node<'a>, code: &[u8], ancestors: Ancestors<'a, '_>) -> bool {
rust_outer_attr_marks_test(node, code, ancestors) || rust_inner_attr_marks_test(node, code)
}
const MAX_FORWARD_ATTRIBUTE_SCAN_CHILDREN: usize = 6;
const FORWARD_ATTRIBUTE_SCAN_CHILDREN_PER_DEPTH: usize = 3;
fn forward_attribute_scan_budget(ancestors: Ancestors<'_, '_>) -> usize {
ancestors
.depth()
.map_or(0, |depth| {
depth.saturating_mul(FORWARD_ATTRIBUTE_SCAN_CHILDREN_PER_DEPTH)
})
.max(MAX_FORWARD_ATTRIBUTE_SCAN_CHILDREN)
}
fn rust_outer_attr_marks_test<'a>(
node: &Node<'a>,
code: &[u8],
ancestors: Ancestors<'a, '_>,
) -> bool {
match ancestors.parent(node) {
Some(parent) if parent.child_count() <= forward_attribute_scan_budget(ancestors) => {
rust_attribute_run_under(&parent, node, code)
}
_ => rust_attribute_run_before(node, code),
}
}
fn rust_attribute_run_under(parent: &Node, node: &Node, code: &[u8]) -> bool {
let mut run_start = None;
for child in parent.children() {
if child.id() == node.id() {
break;
}
if child.kind_id() == Rust::AttributeItem {
if run_start.is_none() {
run_start = Some(child);
}
} else {
run_start = None;
}
}
let Some(run_start) = run_start else {
return false;
};
parent
.children()
.skip_while(|child| child.id() != run_start.id())
.take_while(|child| child.id() != node.id() && child.kind_id() == Rust::AttributeItem)
.any(|attribute| rust_attribute_node_marks_test(&attribute, code, "#"))
}
fn rust_attribute_run_before(node: &Node, code: &[u8]) -> bool {
let mut sibling = node.previous_sibling();
while let Some(attribute) = sibling {
if attribute.kind_id() != Rust::AttributeItem {
break;
}
if rust_attribute_node_marks_test(&attribute, code, "#") {
return true;
}
sibling = attribute.previous_sibling();
}
false
}
fn rust_inner_attr_marks_test(node: &Node, code: &[u8]) -> bool {
node.kind_id() == Rust::ModItem
&& node.child_by_field_name("body").is_some_and(|body| {
body.children()
.filter(|child| child.kind_id() == Rust::InnerAttributeItem)
.any(|attribute| rust_attribute_node_marks_test(&attribute, code, "#!"))
})
}
#[cfg(test)]
#[allow(
clippy::float_cmp,
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::similar_names,
clippy::doc_markdown,
clippy::needless_raw_string_hashes,
clippy::too_many_lines
)]
mod tests {
use super::*;
use crate::count::count;
use crate::langs::{
BashParser, JavascriptCode, JavascriptParser, MozjsParser, PhpParser, TsxParser,
TypescriptParser,
};
use crate::test_support::for_each_node_with_chain;
use std::fmt::Write as _;
use std::path::PathBuf;
fn parse(source: &str) -> BashParser {
BashParser::new(source.as_bytes().to_vec(), &PathBuf::from("test.sh"), None)
}
fn count_strings(source: &str) -> usize {
count(&parse(source), &["string".to_string()]).0
}
fn has_kind(source: &str, kind_id: u16) -> bool {
count(&parse(source), &[kind_id.to_string()]).0 > 0
}
#[test]
fn bash_is_string_excludes_word_tokens() {
assert_eq!(count_strings("echo hello world\n"), 0);
assert_eq!(
count_strings("if [ -f file.txt ]; then cat file.txt; fi\n"),
0
);
}
#[test]
fn bash_is_string_matches_quoted_literals() {
assert_eq!(count_strings("echo \"double\"\n"), 1);
assert_eq!(count_strings("echo 'single'\n"), 1);
assert_eq!(count_strings("echo $'ansi-c'\n"), 1);
}
#[test]
fn bash_is_string_matches_translated_string() {
let src = "x=$\"translated\"\n";
assert!(
has_kind(src, Bash::TranslatedString as u16),
"expected a translated_string node in {src:?}"
);
assert_eq!(count_strings(src), 2);
}
#[test]
fn bash_is_string_matches_heredoc_bodies() {
assert_eq!(
count_strings("cat <<EOF\nhello world\nEOF\n"),
1,
"heredoc body should be counted as a string literal"
);
assert_eq!(
count_strings("cat <<'EOF'\nliteral $not_expanded\nEOF\n"),
1
);
assert_eq!(count_strings("cat <<EOF\nhi $name\nEOF\n"), 1);
}
fn parse_php(source: &str) -> PhpParser {
PhpParser::new(source.as_bytes().to_vec(), &PathBuf::from("test.php"), None)
}
fn count_php_strings(source: &str) -> usize {
count(&parse_php(source), &["string".to_string()]).0
}
#[test]
fn php_is_string_matches_single_quoted_literal() {
assert_eq!(count_php_strings("<?php $x = 'single';"), 1);
}
#[test]
fn php_is_string_matches_encapsed_heredoc_nowdoc_shell() {
assert_eq!(count_php_strings("<?php $x = \"double\";"), 1);
assert_eq!(
count_php_strings("<?php $x = <<<EOT\nbody\nEOT;\n"),
1,
"heredoc should match is_string"
);
assert_eq!(
count_php_strings("<?php $x = <<<'EOT'\nbody\nEOT;\n"),
1,
"nowdoc should match is_string"
);
assert_eq!(
count_php_strings("<?php $x = `ls`;"),
1,
"shell command (backtick) should match is_string"
);
}
#[test]
fn php_is_string_matches_string_alias_kinds() {
let src = "<?php function f(): string { return 'x'; }";
assert_eq!(count_php_strings(src), 2);
}
fn ast_has_kind_id<P: ParserTrait>(parser: &P, target: u16) -> bool {
let mut stack = vec![parser.root()];
while let Some(node) = stack.pop() {
if node.kind_id() == target {
return true;
}
for i in (0..node.child_count()).rev() {
if let Some(c) = node.child(i) {
stack.push(c);
}
}
}
false
}
fn count_string_matches_for_kind<P: ParserTrait, F: Fn(&Node) -> bool>(
parser: &P,
target: u16,
is_string: F,
) -> usize {
let mut stack = vec![parser.root()];
let mut hits = 0;
while let Some(node) = stack.pop() {
if node.kind_id() == target && is_string(&node) {
hits += 1;
}
for i in (0..node.child_count()).rev() {
if let Some(c) = node.child(i) {
stack.push(c);
}
}
}
hits
}
#[test]
fn javascript_is_string_matches_string2_alias() {
let src = "const a = 'single';\nconst b = \"double\";\nimport \"m\";\n";
let parser = JavascriptParser::new(src.as_bytes().to_vec(), &PathBuf::from("t.js"), None);
assert!(
ast_has_kind_id(&parser, Javascript::String2 as u16),
"expected Javascript::String2 to appear in the parse",
);
assert!(
count_string_matches_for_kind(
&parser,
Javascript::String2 as u16,
JavascriptCode::is_string,
) > 0,
"Javascript::String2 nodes must match is_string",
);
}
#[test]
fn mozjs_is_string_matches_string2_alias() {
let src = "const a = 'single';\nconst b = \"double\";\nimport \"m\";\n";
let parser = MozjsParser::new(src.as_bytes().to_vec(), &PathBuf::from("t.js"), None);
assert!(
ast_has_kind_id(&parser, Mozjs::String2 as u16),
"expected Mozjs::String2 to appear in the parse",
);
assert!(
count_string_matches_for_kind(&parser, Mozjs::String2 as u16, MozjsCode::is_string) > 0,
"Mozjs::String2 nodes must match is_string",
);
}
#[test]
fn typescript_is_string_matches_string2_alias() {
let src = "const a: string = 'x';\nfunction f(): string { return 'y'; }\n";
let parser = TypescriptParser::new(src.as_bytes().to_vec(), &PathBuf::from("t.ts"), None);
assert!(
ast_has_kind_id(&parser, Typescript::String2 as u16),
"expected Typescript::String2 to appear in the parse",
);
assert!(
count_string_matches_for_kind(
&parser,
Typescript::String2 as u16,
TypescriptCode::is_string,
) > 0,
"Typescript::String2 nodes must match is_string",
);
}
#[test]
fn tsx_is_string_matches_string2_and_string3_aliases() {
let src = "const a: string = 'x';\n\
const b = \"y\";\n\
import \"m\";\n\
const el = <div className=\"c\">{\"t\"}</div>;\n";
let parser = TsxParser::new(src.as_bytes().to_vec(), &PathBuf::from("t.tsx"), None);
assert!(
ast_has_kind_id(&parser, Tsx::String3 as u16),
"expected Tsx::String3 (type-keyword `string`) in the parse",
);
assert!(
ast_has_kind_id(&parser, Tsx::String2 as u16),
"expected Tsx::String2 (string-literal alias) in the parse",
);
assert!(
count_string_matches_for_kind(&parser, Tsx::String3 as u16, TsxCode::is_string) > 0,
"Tsx::String3 nodes must match is_string",
);
assert!(
count_string_matches_for_kind(&parser, Tsx::String2 as u16, TsxCode::is_string) > 0,
"Tsx::String2 nodes must match is_string",
);
}
fn find_first_kind<P: ParserTrait>(parser: &P, target: u16) -> Option<Node<'_>> {
let mut stack = vec![parser.root()];
while let Some(node) = stack.pop() {
if node.kind_id() == target {
return Some(node);
}
for i in (0..node.child_count()).rev() {
if let Some(c) = node.child(i) {
stack.push(c);
}
}
}
None
}
#[test]
fn rust_outer_attr_on_mod_is_test_only() {
let src = "#[cfg(test)]\nmod tests {\n fn t() {}\n}\n";
let parser = RustParser::new(src.as_bytes().to_vec(), &PathBuf::from("test.rs"), None);
let code = parser.code();
let node = find_first_kind(&parser, Rust::ModItem as u16).expect("mod_item");
assert!(rust_item_is_test_only(&node, code, Ancestors::unknown()));
assert!(rust_outer_attr_marks_test(
&node,
code,
Ancestors::unknown()
));
assert!(!rust_inner_attr_marks_test(&node, code));
}
#[test]
fn rust_inner_attr_in_mod_is_test_only() {
let src = "mod tests {\n #![cfg(test)]\n fn t() {}\n}\n";
let parser = RustParser::new(src.as_bytes().to_vec(), &PathBuf::from("test.rs"), None);
let code = parser.code();
let node = find_first_kind(&parser, Rust::ModItem as u16).expect("mod_item");
assert!(rust_item_is_test_only(&node, code, Ancestors::unknown()));
assert!(!rust_outer_attr_marks_test(
&node,
code,
Ancestors::unknown()
));
assert!(rust_inner_attr_marks_test(&node, code));
}
#[test]
fn rust_plain_item_is_not_test_only() {
let src = "fn foo() {}\n";
let parser = RustParser::new(src.as_bytes().to_vec(), &PathBuf::from("test.rs"), None);
let code = parser.code();
let node = find_first_kind(&parser, Rust::FunctionItem as u16).expect("function_item");
assert!(!rust_item_is_test_only(&node, code, Ancestors::unknown()));
assert!(!rust_outer_attr_marks_test(
&node,
code,
Ancestors::unknown()
));
assert!(!rust_inner_attr_marks_test(&node, code));
}
#[test]
fn rust_outer_attr_scans_agree() {
let long_run = format!("{}fn f() {{}}\n", "#[allow(dead_code)]\n".repeat(64));
let mut wide = String::new();
for i in 0..200 {
let _ = writeln!(wide, "#[inline] fn f{i}() {{}}");
}
let sources = [
"#[cfg(test)]\nfn a() {}\n",
"#[test]\nfn a() {}\n#[tokio::test]\nfn b() {}\n\
#[cfg(all(test, feature = \"x\"))]\nfn c() {}\n\
#[cfg(not(test))]\nfn d() {}\n",
"#[cfg(test)]\n#[allow(dead_code)]\nfn a() {}\n\
#[allow(dead_code)]\n#[cfg(test)]\nfn b() {}\n\
#[cfg(test)]\n// break\nfn c() {}\n",
"mod outer {\n#![cfg(test)]\nmod inner {\n#[test]\nfn a() {}\n}\n}\n\
fn b() {\n#[cfg(test)]\nfn c() {}\nlet x = 1;\n}\n",
"fn a() {}\n",
"#[cfg(test)]\nfn a() {}\n#[inline]\nfn b() {}\n#[cfg(test)]\nfn c() {}\n",
"#[cfg(test)]\nfn a() {}\n#[inline]\nfn b() {}\n#[cfg(test)]\nfn c() {}\nfn d() {}\n",
"mod m {\n#[cfg(test)]\nfn a() {}\n#[inline]\nfn b() {}\n\
#[cfg(test)]\nfn c() {}\nfn d() {}\n}\n",
&long_run,
&wide,
];
let (mut marked, mut unmarked) = (0_usize, 0_usize);
let (mut read_forward, mut read_backward) = (0_usize, 0_usize);
for source in sources {
let code = source.as_bytes();
let visited = for_each_node_with_chain::<RustCode>(code, |node, chain| {
let Some(parent) = chain.last() else {
return; };
let backward = rust_attribute_run_before(node, code);
assert_eq!(
rust_attribute_run_under(parent, node, code),
backward,
"outer-attribute readings disagree on {} at row {}",
node.kind(),
node.start_row(),
);
assert_eq!(
rust_outer_attr_marks_test(node, code, Ancestors::known(chain)),
backward,
"the width dispatch changed the answer on {} at row {}",
node.kind(),
node.start_row(),
);
if backward {
marked += 1;
} else {
unmarked += 1;
}
if parent.child_count() > forward_attribute_scan_budget(Ancestors::known(chain)) {
read_backward += 1;
} else {
read_forward += 1;
}
});
assert!(visited > 0, "fixture must have nodes to compare");
}
assert!(
marked > 0 && unmarked > 0,
"the comparison saw only one answer ({marked} marked, {unmarked} unmarked), \
so agreeing proves nothing"
);
assert!(
read_forward > 0 && read_backward > 0,
"one dispatch arm went unexercised ({read_forward} forward, \
{read_backward} backward)"
);
}
#[cfg(feature = "rust")]
#[test]
fn the_forward_attribute_scan_budget_grows_with_depth() {
assert_eq!(
forward_attribute_scan_budget(Ancestors::unknown()),
MAX_FORWARD_ATTRIBUTE_SCAN_CHILDREN,
"no chain means no depth to scale by"
);
let mut checked = 0_usize;
for_each_node_with_chain::<RustCode>(b"fn a() {}\n", |node, _| {
checked += 1;
for (depth, want) in [(0, 6), (1, 6), (2, 6), (3, 9), (10, 30), (1_000, 3_000)] {
let chain = vec![*node; depth];
assert_eq!(
forward_attribute_scan_budget(Ancestors::known(&chain)),
want,
"budget at depth {depth}"
);
}
});
assert!(
checked > 0,
"fixture must yield a node to build chains from"
);
}
#[test]
fn rust_should_skip_subtree_matches_the_backward_reading() {
let source = "#[cfg(test)]\nmod tests {\nfn a() {}\n}\n\
#[allow(dead_code)]\nstatic S: i32 = 1;\n\
mod inner {\n#![cfg(test)]\nconst C: i32 = 1;\n}\n\
#[rstest]\nfn b() {}\nfn c() {}\n";
let code = source.as_bytes();
let mut pruned = 0_usize;
let visited = for_each_node_with_chain::<RustCode>(code, |node, chain| {
let reference =
rust_attribute_run_before(node, code) || rust_inner_attr_marks_test(node, code);
let is_item = matches!(
node.kind_id().into(),
Rust::ModItem
| Rust::FunctionItem
| Rust::ImplItem
| Rust::TraitItem
| Rust::ConstItem
| Rust::StaticItem
);
let skipped = RustCode::should_skip_subtree(node, code, Ancestors::known(chain));
assert_eq!(
skipped,
is_item && reference,
"prune decision moved on {} at row {}",
node.kind(),
node.start_row(),
);
pruned += usize::from(skipped);
});
assert!(visited > 0, "fixture must have nodes to compare");
assert_eq!(pruned, 3, "expected exactly the three test-only items");
}
#[test]
fn groovy_is_else_if_recognises_else_followed_by_if() {
let src = "if (x) { } else if (y) { } else { }";
let parser =
GroovyParser::new(src.as_bytes().to_vec(), &PathBuf::from("test.groovy"), None);
let outer =
find_first_kind(&parser, Groovy::IfStatement as u16).expect("outer if_statement");
let mut inner: Option<Node> = None;
for i in 0..outer.child_count() {
if let Some(c) = outer.child(i)
&& c.kind_id() == Groovy::IfStatement as u16
{
inner = Some(c);
break;
}
}
let inner = inner.expect("expected an inner if_statement");
assert!(
GroovyCode::is_else_if(&inner, Ancestors::unknown()),
"inner if_statement after `else` must be recognised as else-if"
);
assert!(
!GroovyCode::is_else_if(&outer, Ancestors::unknown()),
"outer if_statement must not be recognised as else-if"
);
}
#[test]
fn groovy_is_else_if_false_for_standalone_if() {
let src = "if (x) { println(x) }";
let parser =
GroovyParser::new(src.as_bytes().to_vec(), &PathBuf::from("test.groovy"), None);
let node = find_first_kind(&parser, Groovy::IfStatement as u16).expect("if_statement");
assert!(!GroovyCode::is_else_if(&node, Ancestors::unknown()));
}
#[test]
fn groovy_is_call_excludes_constructors() {
let src = "def m() {\n def a = new Foo()\n a.bar()\n println \"hi\"\n}\n";
let parser =
GroovyParser::new(src.as_bytes().to_vec(), &PathBuf::from("test.groovy"), None);
assert_eq!(
count(&parser, &["call".to_string()]).0,
2,
"is_call must count method_invocation + command_chain only, not the constructor"
);
let ctor = find_first_kind(&parser, Groovy::ObjectCreationExpression as u16)
.expect("object_creation_expression");
assert!(
!GroovyCode::is_call(&ctor),
"object_creation_expression must not be a call"
);
let method =
find_first_kind(&parser, Groovy::MethodInvocation as u16).expect("method_invocation");
assert!(
GroovyCode::is_call(&method),
"method_invocation must be a call"
);
let chain = find_first_kind(&parser, Groovy::CommandChain as u16).expect("command_chain");
assert!(GroovyCode::is_call(&chain), "command_chain must be a call");
}
fn parse_python(src: &str) -> PythonParser {
PythonParser::new(src.as_bytes().to_vec(), &PathBuf::from("test.py"), None)
}
fn find_all_kinds<P: ParserTrait>(parser: &P, target: u16) -> Vec<Node<'_>> {
let mut out = Vec::new();
let mut stack = vec![parser.root()];
while let Some(node) = stack.pop() {
if node.kind_id() == target {
out.push(node);
}
for i in (0..node.child_count()).rev() {
if let Some(c) = node.child(i) {
stack.push(c);
}
}
}
out
}
#[test]
fn python_is_else_if_recognises_if_inside_else_clause() {
let src = "if a:\n pass\nelse:\n if b:\n pass\n";
let parser = parse_python(src);
let outer =
find_first_kind(&parser, Python::IfStatement as u16).expect("outer if_statement");
let inner =
find_python_if_inside_else_block(&parser).expect("inner if_statement under else");
assert!(
PythonCode::is_else_if(&inner, Ancestors::unknown()),
"if_statement inside else_clause's block must be recognised as else-if"
);
assert!(
!PythonCode::is_else_if(&outer, Ancestors::unknown()),
"outer if_statement must not be recognised as else-if"
);
}
#[test]
fn python_is_else_if_false_for_standalone_if() {
let src = "if a:\n pass\n";
let parser = parse_python(src);
let node = find_first_kind(&parser, Python::IfStatement as u16).expect("if_statement");
assert!(!PythonCode::is_else_if(&node, Ancestors::unknown()));
}
#[test]
fn python_is_else_if_false_for_outer_if_with_elif_alternative() {
let src = "if a:\n pass\nelif b:\n pass\n";
let parser = parse_python(src);
let outer = find_first_kind(&parser, Python::IfStatement as u16).expect("if_statement");
assert!(!PythonCode::is_else_if(&outer, Ancestors::unknown()));
}
fn find_python_if_inside_else_block(parser: &PythonParser) -> Option<Node<'_>> {
find_all_kinds(parser, Python::IfStatement as u16)
.into_iter()
.find(|n| {
n.parent().is_some_and(|p| {
matches!(p.kind_id().into(), Python::Block | Python::Block2)
&& p.parent()
.is_some_and(|gp| gp.kind_id() == Python::ElseClause)
})
})
}
#[test]
fn python_is_else_if_false_when_else_body_has_siblings() {
let src = "if a:\n pass\nelse:\n if b:\n pass\n pass\n";
let parser = parse_python(src);
let inner =
find_python_if_inside_else_block(&parser).expect("inner if_statement under else");
assert!(
!PythonCode::is_else_if(&inner, Ancestors::unknown()),
"inner if must NOT be recognised as else-if when its block has siblings"
);
}
#[test]
fn python_hidden_block_and_lambda_aliases_stay_unseen() {
let src = "def f(a, b):\n if a:\n return b\n for x in b:\n print(x)\n\nclass C:\n def m(self):\n pass\n\ng = lambda x: x + 1\n";
let parser = parse_python(src);
assert!(
ast_has_kind_id(&parser, Python::Block2 as u16),
"expected Python::Block2 (160, the emitted `block`) in the parse",
);
assert!(
ast_has_kind_id(&parser, Python::Lambda as u16),
"expected Python::Lambda (196, the emitted `lambda`) in the parse",
);
assert!(
!ast_has_kind_id(&parser, Python::Block as u16),
"Python::Block (135) is the hidden `_block` supertype; if it now appears, route it through python_is_block and assert positively (#419)",
);
assert!(
!ast_has_kind_id(&parser, Python::Lambda2 as u16),
"Python::Lambda2 (197) is an unseen lambda alias; if it now appears, cognitive::python_is_lambda (reused by is_closure and the three cognitive lambda-scope sites) must detect it and a positive closure assertion is required (#419/#422)",
);
}
#[test]
fn python_is_lambda_matches_live_lambda_and_agrees_with_is_closure() {
use crate::metrics::cognitive::python_is_lambda;
let parser = parse_python("def f():\n return lambda x: x and x\n");
let lambda = find_first_kind(&parser, Python::Lambda as u16)
.expect("the lambda expression must parse as Python::Lambda (196)");
assert!(
python_is_lambda(&lambda),
"python_is_lambda must accept the emitted Lambda node",
);
assert!(
PythonCode::is_closure(&lambda, Ancestors::unknown()),
"is_closure must agree with python_is_lambda on the same lambda node",
);
let func = find_first_kind(&parser, Python::FunctionDefinition as u16)
.expect("the def must parse as Python::FunctionDefinition");
assert!(
!python_is_lambda(&func),
"python_is_lambda must reject a non-lambda node",
);
}
#[test]
fn languages_without_else_if_chains_answer_false() {
let preproc = PreprocParser::new(b"#define A 1\n".to_vec(), &PathBuf::from("a.h"), None);
let ccomment = CcommentParser::new(b"/* c */\n".to_vec(), &PathBuf::from("a.c"), None);
assert!(
!PreprocCode::is_else_if(&preproc.root(), Ancestors::unknown()),
"the Checker default must answer false"
);
assert!(
!CcommentCode::is_else_if(&ccomment.root(), Ancestors::unknown()),
"the Checker default must answer false"
);
let parser = ElixirParser::new(
b"defmodule M do\n def f(a) do\n if a do\n if a do\n :ok\n end\n end\n end\nend\n".to_vec(),
std::path::Path::new("m.ex"),
None,
);
let ifs: Vec<_> = parser
.root()
.preorder()
.filter(|n| {
n.kind() == "call"
&& n.utf8_text(parser.code())
.is_some_and(|t| t.starts_with("if"))
})
.collect();
assert!(
ifs.len() >= 2,
"fixture must contain a nested if, else the assertion below is vacuous"
);
for node in &ifs {
assert!(
!ElixirCode::is_else_if(node, Ancestors::unknown()),
"Elixir has no else-if chain; every if is a fresh branch"
);
}
}
fn count_with_parser<P: ParserTrait>(parser: &P) -> usize {
count(parser, &["string".to_string()]).0
}
fn assert_variant_is_string<P: ParserTrait, F: Fn(&Node) -> bool>(
parser: &P,
target: u16,
is_string: F,
lang: &str,
variant: &str,
) {
assert!(
ast_has_kind_id(parser, target),
"{lang}::{variant} (kind_id {target}) did not appear in the parse — fixture broken",
);
assert!(
count_string_matches_for_kind(parser, target, is_string) > 0,
"{lang}::{variant} must route through is_string",
);
}
macro_rules! assert_variants_is_string {
($parser:expr, $lang:ident, $code:ident, [$($variant:ident),+ $(,)?]) => {
$(
assert_variant_is_string(
$parser,
$lang::$variant as u16,
$code::is_string,
stringify!($lang),
stringify!($variant),
);
)+
};
}
macro_rules! assert_no_string_matches {
($parser_ty:ident, $path:expr, $src:expr, $lang:literal $(,)?) => {{
let parser = $parser_ty::new($src.to_vec(), $path, None);
assert_eq!(count_with_parser(&parser), 0, $lang);
}};
}
#[test]
fn simple_is_string_macro_recognises_each_language() {
use crate::langs::{
CcommentParser, CppParser, CsharpParser, ElixirParser, GoParser, GroovyParser,
IrulesParser, JavaParser, KotlinParser, LuaParser, PerlParser, PreprocParser,
PythonParser, RubyParser, RustParser, TclParser,
};
let path = PathBuf::from("test");
let src = b"#include \"foo.h\"\nR\"(raw)\"\n".to_vec();
let parser = PreprocParser::new(src, &path, None);
assert_variants_is_string!(
&parser,
Preproc,
PreprocCode,
[StringLiteral, RawStringLiteral]
);
let src = b"\"hello\"\nR\"(raw)\"\n".to_vec();
let parser = CcommentParser::new(src, &path, None);
assert_variants_is_string!(
&parser,
Ccomment,
CcommentCode,
[StringLiteral, RawStringLiteral]
);
let src =
b"const char* a = \"hi\";\nconst char* b = \"a\" \"b\";\nconst char* c = R\"(raw)\";\n"
.to_vec();
let parser = CppParser::new(src, &path, None);
assert_variants_is_string!(
&parser,
Cpp,
CppCode,
[StringLiteral, ConcatenatedString, RawStringLiteral]
);
let src = b"a = \"hi\"\nb = \"a\" \"b\"\n".to_vec();
let parser = PythonParser::new(src, &path, None);
assert_variants_is_string!(&parser, Python, PythonCode, [String, ConcatenatedString]);
let src = b"class C { String a = \"hi\"; String b = \"\"\"\nmulti\n\"\"\"; }\n".to_vec();
let parser = JavaParser::new(src, &path, None);
assert_variants_is_string!(&parser, Java, JavaCode, [StringLiteral]);
assert!(
!ast_has_kind_id(&parser, Java::MultilineStringLiteral as u16),
"Java::MultilineStringLiteral is documented as the hidden _multiline_string_literal supertype; if it now appears in parses, replace this with a positive variant assertion",
);
let src = b"class C { string a = \"hi\"; string b = @\"verb\"; string c = \"\"\"raw\"\"\"; string d = $\"int{1}\"; }\n".to_vec();
let parser = CsharpParser::new(src, &path, None);
assert_variants_is_string!(
&parser,
Csharp,
CsharpCode,
[
StringLiteral,
VerbatimStringLiteral,
RawStringLiteral,
InterpolatedStringExpression,
]
);
let src = b"fn main() { let a = \"hi\"; let b = r\"raw\"; }\n".to_vec();
let parser = RustParser::new(src, &path, None);
assert_variants_is_string!(&parser, Rust, RustCode, [StringLiteral, RawStringLiteral]);
let src = b"package main\nfunc main() { _ = \"hi\"; _ = `raw` }\n".to_vec();
let parser = GoParser::new(src, &path, None);
assert_variants_is_string!(
&parser,
Go,
GoCode,
[InterpretedStringLiteral, RawStringLiteral]
);
let src = b"fun main() { val a = \"hi\"; val b = \"\"\"multi\"\"\" }\n".to_vec();
let parser = KotlinParser::new(src, &path, None);
assert_variants_is_string!(
&parser,
Kotlin,
KotlinCode,
[StringLiteral, MultilineStringLiteral]
);
let src = b"local a = \"hi\"\nlocal b = [[long]]\n".to_vec();
let parser = LuaParser::new(src, &path, None);
assert_variants_is_string!(&parser, Lua, LuaCode, [String]);
let src = b"my $a = 'single';\nmy $b = \"double\";\nmy $c = q(qquoted);\nmy $d = qq(qqquoted);\nmy $e = `cmd`;\nmy $f = qx(qxcmd);\nmy $g = <<EOT;\nbody\nEOT\n".to_vec();
let parser = PerlParser::new(src, &path, None);
assert_variants_is_string!(
&parser,
Perl,
PerlCode,
[
StringSingleQuoted,
StringDoubleQuoted,
StringQQuoted,
StringQqQuoted,
BacktickQuoted,
CommandQxQuoted,
HeredocBodyStatement,
]
);
let src = b"a=\"d\"\nb='r'\nc=$'ansi'\nd=$\"t\"\ncat <<EOF\nbody\nEOF\n".to_vec();
let parser = crate::langs::BashParser::new(src, &path, None);
assert_variants_is_string!(
&parser,
Bash,
BashCode,
[
String,
RawString,
AnsiCString,
TranslatedString,
HeredocBody2
]
);
let src = b"set a \"quoted\"\nset b {braced}\nproc p {x y} { return $x }\n".to_vec();
let parser = TclParser::new(src, &path, None);
assert_variants_is_string!(
&parser,
Tcl,
TclCode,
[QuotedWord, BracedWordSimple, BracedWord]
);
let src = b"set a \"quoted\"\nset b {braced}\nproc p {x y} { return $x }\n".to_vec();
let parser = IrulesParser::new(src, &path, None);
assert_variants_is_string!(
&parser,
Irules,
IrulesCode,
[QuotedWord, BracedWordSimple, BracedWord]
);
let src = b"<?php function f(): string { $a = 'single'; $b = \"double\"; $c = <<<EOT\nbody\nEOT;\n$d = <<<'EOT'\nnow\nEOT;\n$e = `ls`; return $a; }\n".to_vec();
let parser = PhpParser::new(src, &path, None);
assert_variants_is_string!(&parser, Php, PhpCode, [String, String2]);
assert!(
!ast_has_kind_id(&parser, Php::String3 as u16),
"Php::String3 is documented as the hidden _string supertype; if it now appears in parses, add a positive variant assertion",
);
assert_variants_is_string!(
&parser,
Php,
PhpCode,
[EncapsedString, Heredoc, Nowdoc, ShellCommandExpression]
);
let src = b"a = \"hi\"\nb = 'charlist'\nc = ~s(sigil)\n".to_vec();
let parser = ElixirParser::new(src, &path, None);
assert_variants_is_string!(&parser, Elixir, ElixirCode, [String, Charlist, Sigil]);
let src = b"a = \"hi\"\nb = \"x\" \"y\"\nc = `cmd`\nd = /re/\ne = <<EOT\nbody\nEOT\nf = :sym\ng = :\"dsym\"\nh = %w[bare1 bare2]\ni = %i[s1 s2]\nj = ?A\n".to_vec();
let parser = RubyParser::new(src, &path, None);
assert_variants_is_string!(
&parser,
Ruby,
RubyCode,
[
String,
ChainedString,
BareString,
Subshell,
Regex,
HeredocBody,
DelimitedSymbol,
SimpleSymbol,
StringArray,
SymbolArray,
Character,
]
);
let src =
b"def m() { def a = \"hi\"; def b = \"\"\"multi\"\"\"; def c = /pat/ }\n".to_vec();
let parser = GroovyParser::new(src, &path, None);
assert_variants_is_string!(&parser, Groovy, GroovyCode, [StringLiteral]);
}
#[test]
fn simple_is_string_macro_rejects_non_string_nodes() {
use crate::langs::{
BashParser, CcommentParser, CppParser, CsharpParser, ElixirParser, GoParser,
GroovyParser, IrulesParser, JavaParser, KotlinParser, LuaParser, PerlParser,
PreprocParser, PythonParser, RubyParser, RustParser, TclParser,
};
let path = PathBuf::from("test");
assert_no_string_matches!(PreprocParser, &path, b"#define FOO 1\n", "Preproc");
assert_no_string_matches!(CcommentParser, &path, b"// just a comment\n", "Ccomment");
assert_no_string_matches!(CppParser, &path, b"int main() { return x; }\n", "Cpp");
assert_no_string_matches!(PythonParser, &path, b"x = y\n", "Python");
assert_no_string_matches!(JavaParser, &path, b"class C { int x = y; }\n", "Java");
assert_no_string_matches!(CsharpParser, &path, b"class C { int x = y; }\n", "Csharp");
assert_no_string_matches!(RustParser, &path, b"fn main() { let x = y; }\n", "Rust");
assert_no_string_matches!(
GoParser,
&path,
b"package main\nfunc main() { _ = x }\n",
"Go"
);
assert_no_string_matches!(KotlinParser, &path, b"fun main() { val x = y }\n", "Kotlin");
assert_no_string_matches!(PerlParser, &path, b"my $x = $y;\n", "Perl");
assert_no_string_matches!(LuaParser, &path, b"local x = y\n", "Lua");
assert_no_string_matches!(BashParser, &path, b"s=$y\n", "Bash");
assert_no_string_matches!(TclParser, &path, b"set x $y\n", "Tcl");
assert_no_string_matches!(IrulesParser, &path, b"set x $y\n", "Irules");
assert_no_string_matches!(PhpParser, &path, b"<?php $x = $y;\n", "Php");
assert_no_string_matches!(ElixirParser, &path, b"x = 1\n", "Elixir");
assert_no_string_matches!(RubyParser, &path, b"x = y\n", "Ruby");
assert_no_string_matches!(GroovyParser, &path, b"def m() { def x = y }\n", "Groovy");
}
#[test]
fn mozjs_parses_using_declaration() {
let src = "function f() {\n using r = acquire();\n return r;\n}\n";
let parser = MozjsParser::new(src.as_bytes().to_vec(), &PathBuf::from("t.js"), None);
assert!(
ast_has_kind_id(&parser, Mozjs::UsingDeclaration as u16),
"expected Mozjs::UsingDeclaration to appear in the parse",
);
}
#[test]
fn go_rune_literal_is_not_a_string() {
use crate::langs::GoParser;
let path = PathBuf::from("test.go");
let src = b"package main\nfunc main() { r := 'x'; _ = r }\n".to_vec();
let parser = GoParser::new(src, &path, None);
let rune_id = Go::RuneLiteral as u16;
assert!(
ast_has_kind_id(&parser, rune_id),
"fixture should produce a rune_literal node",
);
assert_eq!(
count_string_matches_for_kind(&parser, rune_id, GoCode::is_string),
0,
"Go rune_literal must not match is_string (a rune is a char, not a string)",
);
}
fn assert_func_parity<L: crate::traits::LanguageInfo + Checker>(
label: &str,
code: &[u8],
) -> (usize, usize) {
let mut funcs = 0;
let mut closures = 0;
let visited = for_each_node_with_chain::<L>(code, |node, chain| {
let known = Ancestors::known(chain);
let climbing = Ancestors::unknown();
assert_eq!(
L::is_func(node, known),
L::is_func(node, climbing),
"{label}: is_func disagrees on {} at row {}",
node.kind(),
node.start_row()
);
assert_eq!(
L::is_closure(node, known),
L::is_closure(node, climbing),
"{label}: is_closure disagrees on {} at row {}",
node.kind(),
node.start_row()
);
funcs += usize::from(L::is_func(node, known));
closures += usize::from(L::is_closure(node, known));
});
assert!(visited > 20, "{label}: fixture is too small to prove much");
assert!(
funcs > 0 && closures > 0,
"{label}: fixture must contain both a named function and a closure, \
else parity holds over nodes the walk never classifies: \
{funcs} functions, {closures} closures"
);
(funcs, closures)
}
#[test]
fn js_func_and_closure_agree_between_known_and_climbing() {
let code = concat!(
"const f = a => { g(() => 1); a => 2; };\n",
"const o = { m: function () { return 1; } };\n",
"function h() { return { k: (a) => a + 1 }; }\n",
"function i() { return function () { return 2; }; }\n",
)
.as_bytes();
assert_func_parity::<crate::langs::JavascriptCode>("javascript", code);
assert_func_parity::<crate::langs::MozjsCode>("mozjs", code);
assert_func_parity::<crate::langs::TypescriptCode>("typescript", code);
assert_func_parity::<crate::langs::TsxCode>("tsx", code);
}
#[test]
fn ruby_block_closure_agrees_between_known_and_climbing() {
let code = concat!(
"def m\n",
" f = ->(z) { z + 1 }\n",
" [1].each { |x| x }\n",
" lambda { 2 }\n",
"end\n",
)
.as_bytes();
let (funcs, closures) = assert_func_parity::<crate::langs::RubyCode>("ruby", code);
assert_eq!(funcs, 1, "only `def m` is a named function");
assert_eq!(
closures, 3,
"the stabby lambda, the `each` block and the `lambda` block \
are three closures — the stabby lambda's own body block must \
not add a fourth, which is the answer the parent lookup decides"
);
}
}