#![allow(clippy::wildcard_imports, clippy::enum_glob_use)]
use std::borrow::Cow;
use crate::checker::Checker;
use crate::error::MetricsError;
use crate::getter::Getter;
use crate::node::{Ancestors, FoldCache, Node, SiblingCache};
use crate::spaces::{SpaceKind, line_span, push_children};
use crate::halstead::{Halstead, HalsteadMaps};
use crate::MetricSuite;
#[derive(Debug, Clone)]
pub struct Ops {
pub name: Option<String>,
pub name_was_lossy: bool,
pub start_line: usize,
pub end_line: usize,
pub kind: SpaceKind,
pub spaces: Vec<Ops>,
pub operands: Vec<String>,
pub operators: Vec<String>,
}
crate::recursion::impl_iterative_drop!(Ops, spaces);
impl Ops {
#[must_use]
pub fn to_wire(&self) -> crate::wire::Ops {
crate::wire::Ops::from(self)
}
fn new<'a, T: Getter>(
node: &Node<'a>,
code: &[u8],
ancestors: Ancestors<'a, '_>,
kind: SpaceKind,
) -> Self {
let (start_position, end_position) = line_span(node, kind);
let name = (kind != SpaceKind::Unit)
.then(|| T::get_func_space_name(node, code, ancestors).map(str::to_owned))
.flatten();
Self {
name,
name_was_lossy: false,
spaces: Vec::new(),
kind,
start_line: start_position,
end_line: end_position,
operators: Vec::new(),
operands: Vec::new(),
}
}
}
#[derive(Debug, Clone)]
struct State<'a> {
ops: Ops,
halstead_maps: HalsteadMaps<'a>,
}
fn push_synthetic_unit_root<T: MetricSuite>(
state_stack: &mut Vec<State>,
node: &Node,
code: &[u8],
) {
if T::Getter::get_space_kind_with_code(node, code, Ancestors::unknown()) != SpaceKind::Unit {
state_stack.push(State {
ops: Ops::new::<T::Getter>(node, code, Ancestors::unknown(), SpaceKind::Unit),
halstead_maps: HalsteadMaps::new(),
});
}
}
crate::observation::counter!(space_kind_lookups);
fn classify_space_kind<'a, T: MetricSuite>(
node: &Node<'a>,
code: &[u8],
ancestors: Ancestors<'a, '_>,
) -> SpaceKind {
space_kind_lookups::record();
T::Getter::get_space_kind_with_code(node, code, ancestors)
}
fn sorted_vocabulary(mut keys: Vec<&[u8]>) -> Vec<String> {
keys.sort_unstable();
let mut lossy = false;
let mut rendered: Vec<String> = keys
.into_iter()
.map(|key| match String::from_utf8_lossy(key) {
Cow::Borrowed(text) => text.to_owned(),
Cow::Owned(text) => {
lossy = true;
text
}
})
.collect();
if lossy {
rendered.sort_unstable();
}
rendered
}
fn compute_operators_and_operands<T: MetricSuite>(state: &mut State) {
let maps = &state.halstead_maps;
let operators = maps
.operators
.keys()
.map(|k| T::Getter::get_operator_id_as_str(*k).as_bytes())
.chain(maps.primitive_operators.keys().copied())
.collect();
state.ops.operators = sorted_vocabulary(operators);
state.ops.operands = sorted_vocabulary(maps.operands.keys().copied().collect());
}
fn finalize<T: MetricSuite>(state_stack: &mut Vec<State>, diff_level: usize) {
for _ in 0..diff_level {
if state_stack.len() < 2 {
break;
}
let mut state = state_stack
.pop()
.expect("state_stack verified to have len >= 2");
let last_state = state_stack
.last_mut()
.expect("state_stack verified to have len >= 1 after pop");
compute_operators_and_operands::<T>(&mut state);
last_state.halstead_maps.merge(&state.halstead_maps);
last_state.ops.spaces.push(state.ops);
}
}
#[derive(Clone, Copy)]
struct Walk {
level: usize,
depth: usize,
}
pub(crate) fn ops_inner<T: MetricSuite>(
parser: &T,
name: Option<String>,
) -> Result<Ops, MetricsError> {
let code = parser.code();
let node = parser.root();
let mut cursor = node.cursor();
let mut stack = Vec::new();
let mut chain: Vec<Node<'_>> = Vec::new();
let mut siblings = SiblingCache::default();
let folds = FoldCache::default();
let mut state_stack: Vec<State> = Vec::new();
let mut last_level = 0;
push_synthetic_unit_root::<T>(&mut state_stack, &node, code);
stack.push((node, Walk { level: 0, depth: 0 }));
while let Some((node, Walk { level, depth })) = stack.pop() {
chain.truncate(depth);
if level < last_level {
finalize::<T>(&mut state_stack, last_level - level);
last_level = level;
}
let ancestors = Ancestors::checked(&chain, &node)
.with_siblings(&siblings)
.with_folds(&folds);
let func_space = T::Checker::promotes_to_func_space_with_code(&node, code, ancestors);
let new_level = if func_space {
let kind = classify_space_kind::<T>(&node, code, ancestors);
let state = State {
ops: Ops::new::<T::Getter>(&node, code, ancestors, kind),
halstead_maps: HalsteadMaps::new(),
};
state_stack.push(state);
last_level = level + 1;
last_level
} else {
level
};
if let Some(state) = state_stack.last_mut() {
T::Halstead::compute(&node, code, ancestors, &mut state.halstead_maps);
}
chain.push(node);
let pushed = push_children(
&mut cursor,
&node,
Walk {
level: new_level,
depth: depth + 1,
},
&mut stack,
);
siblings.record(&node, depth, pushed.iter().rev().map(|(child, _)| *child));
}
finalize::<T>(&mut state_stack, usize::MAX);
let mut state = state_stack.pop().ok_or(MetricsError::EmptyRoot)?;
compute_operators_and_operands::<T>(&mut state);
state.ops.name = name;
Ok(state.ops)
}
#[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::Ops;
use crate::{Ast, LANG, Source};
#[cfg(any(
feature = "cpp",
feature = "java",
feature = "javascript",
feature = "mozjs",
feature = "perl",
feature = "python",
feature = "rust",
feature = "typescript",
))]
#[inline]
fn check_ops(
lang: LANG,
source: &str,
file: &str,
correct_operators: &mut [&str],
correct_operands: &mut [&str],
) {
let mut trimmed_bytes = source.trim_end().trim_matches('\n').as_bytes().to_vec();
trimmed_bytes.push(b'\n');
let ops = Ast::parse(Source::new(lang, &trimmed_bytes).with_name(Some(file.to_owned())))
.expect("language feature enabled")
.ops()
.expect("ops walk must yield a top-level Ops");
let operators_str: Vec<&str> = ops.operators.iter().map(AsRef::as_ref).collect();
let operands_str: Vec<&str> = ops.operands.iter().map(AsRef::as_ref).collect();
correct_operators.sort_unstable();
assert_eq!(&operators_str[..], correct_operators);
correct_operands.sort_unstable();
assert_eq!(&operands_str[..], correct_operands);
}
#[cfg(feature = "python")]
#[test]
fn python_ops() {
check_ops(
LANG::Python,
"if True:
a = 1 + 2",
"foo.py",
&mut ["if", ":", "=", "+"],
&mut ["True", "a", "1", "2"],
);
}
#[cfg(feature = "perl")]
#[test]
fn perl_pattern_operations_render_as_source_spellings() {
check_ops(
LANG::Perl,
"$s =~ s/a/b/;\n$s =~ tr/c/d/;\n$s =~ y/e/f/;\n$t = /pat/;",
"foo.pl",
&mut ["$", ";", "=", "=~", "s///", "tr///"],
&mut ["$s", "$t", "/pat/"],
);
}
#[cfg(feature = "perl")]
#[test]
fn perl_fat_comma_renders_as_written() {
check_ops(
LANG::Perl,
"my %h = (a => 1);",
"foo.pl",
&mut ["()", ";", "=", "=>", "my"],
&mut ["%h", "1", "a"],
);
}
#[cfg(feature = "perl")]
#[test]
fn perl_bareword_keys_bill_as_written() {
check_ops(
LANG::Perl,
"my %h = (-name => 1, name => 2); my $v = $h{-name};",
"foo.pl",
&mut ["$", "()", ",", ";", "=", "=>", "my", "{}"],
&mut ["$h", "$v", "%h", "-name", "1", "2", "name"],
);
check_ops(
LANG::Perl,
"my @a = (1 - size => 3, -$x);",
"foo.pl",
&mut ["()", ",", "-", ";", "=", "=>", "my", "$"],
&mut ["$x", "1", "3", "@a", "size"],
);
}
#[cfg(feature = "python")]
#[test]
fn python_function_ops() {
check_ops(
LANG::Python,
"def foo():
def bar():
def toto():
a = 1 + 1
b = 2 + a
c = 3 + 3",
"foo.py",
&mut ["def", "()", ":", "=", "+"],
&mut ["foo", "bar", "toto", "a", "b", "c", "1", "2", "3"],
);
}
#[cfg(all(feature = "javascript", feature = "python"))]
#[test]
fn python_call_parens_pair_like_javascript() {
check_ops(LANG::Python, "f(x)", "foo.py", &mut ["()"], &mut ["f", "x"]);
check_ops(
LANG::Javascript,
"f(x)",
"foo.js",
&mut ["()"],
&mut ["f", "x"],
);
}
#[cfg(feature = "cpp")]
#[test]
fn cpp_ops() {
check_ops(
LANG::Cpp,
"int a, b, c;
float avg;
avg = (a + b + c) / 3;",
"foo.c",
&mut ["int", "float", "()", "=", "+", "/", ",", ";"],
&mut ["a", "b", "c", "avg", "3"],
);
}
#[cfg(feature = "cpp")]
#[test]
fn cpp_function_ops() {
check_ops(
LANG::Cpp,
"main()
{
int a, b, c, avg;
scanf(\"%d %d %d\", &a, &b, &c);
avg = (a + b + c) / 3;
printf(\"avg = %d\", avg);
}",
"foo.c",
&mut ["()", "{}", "int", "&", "=", "+", "/", ",", ";"],
&mut [
"main",
"a",
"b",
"c",
"avg",
"scanf",
"\"%d %d %d\"",
"3",
"printf",
"\"avg = %d\"",
],
);
}
#[cfg(feature = "rust")]
#[test]
fn rust_ops() {
check_ops(
LANG::Rust,
"let: usize a = 5; let b: f32 = 7.0; let c: i32 = 3;",
"foo.rs",
&mut ["let", ":", "usize", "=", ";", "f32", "i32"],
&mut ["a", "b", "c", "5", "7.0", "3"],
);
}
#[cfg(feature = "rust")]
#[test]
fn rust_function_ops() {
check_ops(
LANG::Rust,
"fn main() {
let a = 5; let b = 5; let c = 5;
let avg = (a + b + c) / 3;
println!(\"{}\", avg);
}",
"foo.rs",
&mut ["fn", "()", "{}", "let", "=", "+", "/", ";", "!", ","],
&mut ["main", "a", "b", "c", "avg", "5", "3", "println", "\"{}\""],
);
}
#[cfg(feature = "javascript")]
#[test]
fn javascript_ops() {
check_ops(
LANG::Javascript,
"var a, b, c, avg;
let x = 1;
a = 5; b = 5; c = 5;
avg = (a + b + c) / 3;
console.log(\"{}\", avg);",
"foo.js",
&mut ["()", "var", "let", "=", "+", "/", ",", ".", ";"],
&mut [
"a", "b", "c", "avg", "x", "1", "3", "5", "console", "log", "\"{}\"",
],
);
}
#[cfg(feature = "javascript")]
#[test]
fn javascript_function_ops() {
check_ops(
LANG::Javascript,
"function main() {
var a, b, c, avg;
let x = 1;
a = 5; b = 5; c = 5;
avg = (a + b + c) / 3;
console.log(\"{}\", avg);
}",
"foo.js",
&mut [
"function", "()", "{}", "var", "let", "=", "+", "/", ",", ".", ";",
],
&mut [
"main", "a", "b", "c", "avg", "x", "1", "3", "5", "console", "log", "\"{}\"",
],
);
}
#[cfg(feature = "mozjs")]
#[test]
fn mozjs_ops() {
check_ops(
LANG::Mozjs,
"var a, b, c, avg;
let x = 1;
a = 5; b = 5; c = 5;
avg = (a + b + c) / 3;
console.log(\"{}\", avg);",
"foo.js",
&mut ["()", "var", "let", "=", "+", "/", ",", ".", ";"],
&mut [
"a", "b", "c", "avg", "x", "1", "3", "5", "console", "log", "\"{}\"",
],
);
}
#[cfg(feature = "mozjs")]
#[test]
fn mozjs_function_ops() {
check_ops(
LANG::Mozjs,
"function main() {
var a, b, c, avg;
let x = 1;
a = 5; b = 5; c = 5;
avg = (a + b + c) / 3;
console.log(\"{}\", avg);
}",
"foo.js",
&mut [
"function", "()", "{}", "var", "let", "=", "+", "/", ",", ".", ";",
],
&mut [
"main", "a", "b", "c", "avg", "x", "1", "3", "5", "console", "log", "\"{}\"",
],
);
}
#[cfg(feature = "typescript")]
#[test]
fn typescript_ops() {
check_ops(
LANG::Typescript,
"var a, b, c, avg;
let age: number = 32;
let name: string = \"John\"; let isUpdated: boolean = true;
a = 5; b = 5; c = 5;
avg = (a + b + c) / 3;
console.log(\"{}\", avg);",
"foo.ts",
&mut [
"()", "var", "let", "string", "number", "boolean", ":", "=", "+", "/", ",", ".",
";",
],
&mut [
"a",
"b",
"c",
"avg",
"age",
"name",
"isUpdated",
"32",
"\"John\"",
"true",
"3",
"5",
"console",
"log",
"\"{}\"",
],
);
}
#[cfg(feature = "typescript")]
#[test]
fn typescript_function_ops() {
check_ops(
LANG::Typescript,
"function main() {
var a, b, c, avg;
let age: number = 32;
let name: string = \"John\"; let isUpdated: boolean = true;
a = 5; b = 5; c = 5;
avg = (a + b + c) / 3;
console.log(\"{}\", avg);
}",
"foo.ts",
&mut [
"function", "()", "{}", "var", "let", "string", "number", "boolean", ":", "=", "+",
"/", ",", ".", ";",
],
&mut [
"main",
"a",
"b",
"c",
"avg",
"age",
"name",
"isUpdated",
"32",
"\"John\"",
"true",
"3",
"5",
"console",
"log",
"\"{}\"",
],
);
}
#[cfg(feature = "typescript")]
#[test]
fn tsx_ops() {
check_ops(
LANG::Tsx,
"var a, b, c, avg;
let age: number = 32;
let name: string = \"John\"; let isUpdated: boolean = true;
a = 5; b = 5; c = 5;
avg = (a + b + c) / 3;
console.log(\"{}\", avg);",
"foo.ts",
&mut [
"()", "var", "let", "string", "number", "boolean", ":", "=", "+", "/", ",", ".",
";",
],
&mut [
"a",
"b",
"c",
"avg",
"age",
"name",
"isUpdated",
"32",
"\"John\"",
"true",
"3",
"5",
"console",
"log",
"\"{}\"",
],
);
}
#[cfg(feature = "typescript")]
#[test]
fn tsx_function_ops() {
check_ops(
LANG::Tsx,
"function main() {
var a, b, c, avg;
let age: number = 32;
let name: string = \"John\"; let isUpdated: boolean = true;
a = 5; b = 5; c = 5;
avg = (a + b + c) / 3;
console.log(\"{}\", avg);
}",
"foo.ts",
&mut [
"function", "()", "{}", "var", "let", "string", "number", "boolean", ":", "=", "+",
"/", ",", ".", ";",
],
&mut [
"main",
"a",
"b",
"c",
"avg",
"age",
"name",
"isUpdated",
"32",
"\"John\"",
"true",
"3",
"5",
"console",
"log",
"\"{}\"",
],
);
}
#[cfg(feature = "typescript")]
#[test]
fn typescript_void_return_and_expression_single_operator_453() {
check_ops(
LANG::Typescript,
"function f(): void { return void 0; }",
"foo.ts",
&mut ["function", "()", "{}", ":", "void", "return", ";"],
&mut ["f", "0"],
);
}
#[cfg(feature = "typescript")]
#[test]
fn tsx_void_return_and_expression_single_operator_453() {
check_ops(
LANG::Tsx,
"function f(): void { return void 0; }",
"foo.tsx",
&mut ["function", "()", "{}", ":", "void", "return", ";"],
&mut ["f", "0"],
);
}
#[cfg(feature = "java")]
#[test]
fn java_ops() {
check_ops(
LANG::Java,
"public class Main {
public static void main(string args[]) {
int a, b, c, avg;
a = 5; b = 5; c = 5;
avg = (a + b + c) / 3;
MessageFormat.format(\"{0}\", avg);
}
}",
"foo.java",
&mut [
"public", "static", "class", "{}", "void", "()", "[]", ",", ".", ";", "int", "=",
"+", "/",
],
&mut [
"Main",
"main",
"string",
"args",
"a",
"b",
"c",
"avg",
"5",
"3",
"MessageFormat",
"format",
"\"{0}\"",
],
);
}
#[cfg(feature = "java")]
#[test]
fn java_primitive_ops() {
check_ops(
LANG::Java,
"public class Prims {
byte a = 1;
short b = 2;
int c = 3;
long d = 4;
char e = 'x';
float f = 1.0f;
double g = 2.0;
boolean h = true;
boolean i = false;
}",
"foo.java",
&mut [
"public", "class", "{}", ";", "=", "byte", "short", "int", "long", "char", "float",
"double", "boolean",
],
&mut [
"Prims", "a", "b", "c", "d", "e", "f", "g", "h", "i", "1", "2", "3", "4", "'x'",
"1.0f", "2.0", "true", "false",
],
);
}
#[cfg(feature = "rust")]
#[test]
fn unit_space_name_is_none_not_anonymous() {
use crate::getter::Getter;
use crate::node::Ancestors;
use crate::traits::ParserTrait;
use crate::{RustCode, RustParser, SpaceKind};
let code = b"fn f() {}\n";
let parser = RustParser::new(code.to_vec(), std::path::Path::new("foo.rs"), None);
let root = parser.root();
assert_eq!(SpaceKind::Unit, RustCode::get_space_kind(&root));
let ops = super::Ops::new::<RustCode>(&root, code, Ancestors::unknown(), SpaceKind::Unit);
assert_eq!(
ops.name, None,
"Unit space must preserve name = None, not invent <anonymous>"
);
}
#[cfg(feature = "lua")]
#[test]
fn lua_error_root_ops_agrees_with_metrics_789() {
use crate::{MetricsOptions, SpaceKind};
let src = b"function foo(x)\n return x +\n".to_vec();
let name = "partial.lua".to_owned();
let ast = Ast::parse(Source::new(LANG::Lua, &src).with_name(Some(name.clone())))
.expect("lua feature enabled");
let space = ast
.metrics(MetricsOptions::default())
.expect("metrics must yield a top-level space");
assert_eq!(space.kind, SpaceKind::Unit);
let ops = ast
.ops()
.expect("ops must agree with metrics and yield a top-level Ops");
assert_eq!(ops.kind, SpaceKind::Unit);
assert_eq!(
ops.name.as_deref(),
Some(name.as_str()),
"top-level Ops name is the caller-supplied Source::name"
);
}
#[cfg(feature = "rust")]
#[test]
fn ops_vocabularies_are_distinct_790() {
use std::collections::HashSet;
let src = b"fn main() { let a = 1 + 1; let b = a + a; }\n".to_vec();
let ops = Ast::parse(Source::new(LANG::Rust, &src).with_name(Some("foo.rs".to_owned())))
.expect("rust feature enabled")
.ops()
.expect("ops walk must yield a top-level Ops");
let unique_operators: HashSet<&String> = ops.operators.iter().collect();
assert_eq!(
ops.operators.len(),
unique_operators.len(),
"Ops::operators must be the distinct operator vocabulary (n1)"
);
let unique_operands: HashSet<&String> = ops.operands.iter().collect();
assert_eq!(
ops.operands.len(),
unique_operands.len(),
"Ops::operands must be the distinct operand vocabulary (n2)"
);
}
fn assert_sorted_spaces(ops: &Ops, lang: LANG) -> usize {
const MIN_OBSERVABLE: usize = 2;
let mut stack = vec![ops];
let mut visited = 0;
while let Some(space) = stack.pop() {
visited += 1;
for (field, values) in [
("operators", &space.operators),
("operands", &space.operands),
] {
assert!(
values.len() >= MIN_OBSERVABLE && values.is_sorted(),
"{lang:?} {field} of space {:?} (@{}) must hold at least \
{MIN_OBSERVABLE} entries and be sorted: {values:?}",
space.name,
space.start_line
);
}
stack.extend(space.spaces.iter());
}
visited
}
#[test]
fn ops_vocabularies_are_sorted_1091() {
type Case = (
LANG,
&'static str,
&'static str,
(&'static str, &'static str),
);
let cases: &[Case] = &[
#[cfg(feature = "rust")]
(
LANG::Rust,
"rust.rs",
"fn zeta(quux: u32) -> u32 { let mid = quux + 1; \
let alpha = |beta: u32| beta * mid; alpha(mid) - quux }\n",
("u32", "|"),
),
#[cfg(feature = "cpp")]
(
LANG::Cpp,
"cpp.cpp",
"int zeta(int quux) { double mid = quux + 1; \
char alpha = 'z'; return quux - mid + alpha; }\n",
("int", "return"),
),
#[cfg(feature = "java")]
(
LANG::Java,
"Java.java",
"class Zeta { int quux(int mid) { long alpha = mid + 1; \
boolean beta = alpha > 2; return beta ? mid : 0; } }\n",
("long", "return"),
),
#[cfg(feature = "python")]
(
LANG::Python,
"python.py",
"def zeta(quux):\n mid = quux + 1\n \
def alpha(beta):\n return beta * mid\n return alpha(mid) - quux\n",
("def", "return"),
),
#[cfg(feature = "typescript")]
(
LANG::Typescript,
"ts.ts",
"function zeta(quux: number): number { const mid: number = quux + 1; \
const alpha = (beta: number) => beta * mid; return alpha(mid) - quux; }\n",
("number", "return"),
),
];
for (lang, file, source, (from_text_map, sorts_after)) in cases {
let ops = Ast::parse(
Source::new(*lang, source.as_bytes()).with_name(Some((*file).to_owned())),
)
.expect("language feature enabled")
.ops()
.expect("ops walk must yield a top-level Ops");
let position = |needle: &str| {
ops.operators
.iter()
.position(|op| op == needle)
.unwrap_or_else(|| {
panic!(
"{lang:?} operators must contain {needle:?}: {:?}",
ops.operators
)
})
};
assert!(
position(from_text_map) < position(sorts_after),
"{lang:?} must order {from_text_map:?} before {sorts_after:?}: {:?}",
ops.operators
);
assert!(
assert_sorted_spaces(&ops, *lang) > 1,
"{lang:?} sample must nest at least one sub-space"
);
}
}
#[test]
#[cfg(feature = "rust")]
fn ops_are_stable_across_repeated_parses_1091() {
use std::fmt::Write as _;
let mut src = String::new();
for i in 0..40 {
writeln!(src, "fn name{i}(arg{i}: u32) -> u32 {{ arg{i} + {i} }}")
.expect("writing to a String cannot fail");
}
let parse = || {
Ast::parse(Source::new(LANG::Rust, src.as_bytes()).with_name(Some("foo.rs".to_owned())))
.expect("rust feature enabled")
.ops()
.expect("ops walk must yield a top-level Ops")
};
let (first, second) = (parse(), parse());
assert!(
first.operands.len() >= 40 && first.spaces.len() >= 40,
"sample must have a wide vocabulary across many spaces, got {} operands \
in {} spaces",
first.operands.len(),
first.spaces.len()
);
let render =
|ops: &Ops| serde_json::to_string(&ops.to_wire()).expect("wire Ops serializes to JSON");
assert_eq!(render(&first), render(&second));
}
#[test]
#[cfg(feature = "rust")]
fn ops_vocabulary_orders_lossy_entries_by_rendered_text_1110() {
let mut src = b"fn f() { let a = \"".to_vec();
src.push(0xff);
src.extend_from_slice(b"A\"; let b = \"");
src.extend_from_slice(&[0xef, 0xbf, 0xbd]);
src.extend_from_slice(b"B\"; }\n");
let ops = Ast::parse(Source::new(LANG::Rust, &src).with_name(Some("foo.rs".to_owned())))
.expect("rust feature enabled")
.ops()
.expect("ops walk must yield a top-level Ops");
let position = |needle: &str| {
ops.operands
.iter()
.position(|operand| operand == needle)
.unwrap_or_else(|| panic!("operands must contain {needle:?}: {:?}", ops.operands))
};
assert!(
position("\"\u{fffd}A\"") < position("\"\u{fffd}B\""),
"lossy entries must be ordered by rendered text, got {:?}",
ops.operands
);
assert!(
ops.operands.is_sorted(),
"the whole vocabulary must be sorted as rendered, got {:?}",
ops.operands
);
}
#[test]
#[cfg(feature = "rust")]
fn ops_classifies_space_kind_once_per_space_1110() {
let cases: &[(LANG, &str, &str)] = &[
#[cfg(feature = "rust")]
(
LANG::Rust,
"foo.rs",
"fn outer(a: u32) -> u32 { fn inner(b: u32) -> u32 { b + 1 } inner(a) * 2 }\n",
),
#[cfg(feature = "python")]
(
LANG::Python,
"foo.py",
"def outer(a):\n def inner(b):\n return b + 1\n return inner(a) * 2\n",
),
#[cfg(feature = "cpp")]
(
LANG::Cpp,
"foo.cpp",
"struct S { int m(int a) { return a + 1; } }; int f(int b) { return b * 2; }\n",
),
#[cfg(feature = "java")]
(
LANG::Java,
"Foo.java",
"class C { int m(int a) { return a + 1; } int n(int b) { return b * 2; } }\n",
),
#[cfg(feature = "javascript")]
(
LANG::Javascript,
"foo.js",
"function outer(a) { function inner(b) { return b + 1; } return inner(a) * 2; }\n",
),
];
crate::test_support::assert_fixtures_present(cases);
for (lang, file, source) in cases {
let ast = crate::test_support::parse_named(*lang, file, source);
let before = super::space_kind_lookups::observed();
let ops = ast.ops().expect("ops walk must yield a top-level Ops");
let lookups = super::space_kind_lookups::observed() - before;
let mut spaces = 0;
let mut stack = vec![&ops];
while let Some(space) = stack.pop() {
spaces += 1;
stack.extend(space.spaces.iter());
}
let mut nodes = 0;
let mut cursor = vec![ast.as_tree_sitter().root_node()];
while let Some(node) = cursor.pop() {
nodes += 1;
let mut walker = node.walk();
cursor.extend(node.children(&mut walker));
}
assert!(
nodes > spaces * 4,
"{lang:?} fixture must have many more nodes ({nodes}) than spaces ({spaces}) \
for the two counts to be distinguishable"
);
assert_eq!(
lookups, spaces,
"{lang:?} must classify once per space, not once per node ({nodes} nodes)"
);
}
}
#[cfg(feature = "elixir")]
type FlatSpace = (usize, crate::SpaceKind, String, usize, usize);
#[cfg(feature = "elixir")]
fn flatten(ops: &Ops, depth: usize, out: &mut Vec<FlatSpace>) {
out.push((
depth,
ops.kind,
ops.name.clone().unwrap_or_else(|| "<none>".to_owned()),
ops.start_line,
ops.end_line,
));
for child in &ops.spaces {
flatten(child, depth + 1, out);
}
}
#[cfg(feature = "elixir")]
fn elixir_ops_tree(source: &str) -> Vec<FlatSpace> {
let ops = crate::test_support::parse_named(LANG::Elixir, "foo.ex", source)
.ops()
.expect("ops walk must yield a top-level Ops");
let mut flat = Vec::new();
flatten(&ops, 0, &mut flat);
flat
}
#[cfg(feature = "elixir")]
#[test]
fn elixir_ops_opens_module_and_function_spaces_1130() {
use crate::SpaceKind::{Class, Function, Unit};
assert_eq!(
elixir_ops_tree("defmodule Foo do\n def bar(x) do\n x + 1\n end\nend\n"),
vec![
(0, Unit, "foo.ex".to_owned(), 1, 5),
(1, Class, "Foo".to_owned(), 1, 5),
(2, Function, "bar".to_owned(), 2, 4),
],
);
}
#[cfg(feature = "elixir")]
#[test]
fn elixir_ops_opens_anonymous_function_space() {
use crate::SpaceKind::{Class, Function, Unit};
assert_eq!(
elixir_ops_tree(
"defmodule Foo do\n def bar(list) do\n \
Enum.map(list, fn x -> x * 2 end)\n end\nend\n"
),
vec![
(0, Unit, "foo.ex".to_owned(), 1, 5),
(1, Class, "Foo".to_owned(), 1, 5),
(2, Function, "bar".to_owned(), 2, 4),
(3, Function, "<anonymous>".to_owned(), 3, 3),
],
);
}
#[cfg(feature = "elixir")]
#[test]
fn elixir_ops_skips_def_inside_quote_block_310() {
use crate::SpaceKind::{Class, Function, Unit};
assert_eq!(
elixir_ops_tree(
"defmodule Foo do\n defmacro gen(name) do\n quote do\n \
def unquote(name)(x) do\n x + 1\n end\n end\n end\nend\n",
),
vec![
(0, Unit, "foo.ex".to_owned(), 1, 9),
(1, Class, "Foo".to_owned(), 1, 9),
(2, Function, "gen".to_owned(), 2, 8),
],
);
}
#[cfg(any(
feature = "cpp",
feature = "csharp",
feature = "groovy",
feature = "java",
feature = "kotlin",
feature = "mozcpp",
feature = "objc",
feature = "rust",
feature = "typescript",
))]
fn check_generic_pair(lang: LANG, file: &str, source: &str, operators: &[&str], n1: [u64; 2]) {
let ast = crate::test_support::parse_named(lang, file, source);
let ops = ast.ops().expect("ops walk must yield a top-level Ops");
let mut expected = operators.to_vec();
expected.sort_unstable();
assert_eq!(ops.operators, expected, "{lang:?} vocabulary");
let space = ast
.metrics(crate::MetricsOptions::default().with_only(&[crate::Metric::Halstead]))
.expect("metrics walk");
let halstead = &space.metrics.halstead;
assert_eq!(
[halstead.unique_operators(), halstead.total_operators()],
n1,
"{lang:?} [n1, N1]"
);
}
#[cfg(feature = "rust")]
#[test]
fn rust_generic_angles_are_one_pair_1559() {
check_generic_pair(
LANG::Rust,
"foo.rs",
"fn f<T>(v: Vec<Vec<T>>) -> bool { a < b && c > d }\n",
&["fn", "<>", "()", ":", "->", "bool", "{}", "<", "&&", ">"],
[10, 12],
);
check_generic_pair(
LANG::Rust,
"foo.rs",
"fn g(f: for<'a> fn(&'a u8)) { h::<u8>(x >> 1); }\n",
&[
"fn", "()", ":", "for", "<>", "&", "u8", "{}", "::", ">>", ";",
],
[11, 16],
);
check_generic_pair(
LANG::Rust,
"foo.rs",
"fn u<T>(x: T) -> impl Sized + use<T> { x }\n",
&["fn", "<>", "()", ":", "->", "impl", "+", "use", "{}"],
[9, 10],
);
check_generic_pair(
LANG::Rust,
"foo.rs",
"fn q() -> bool { <Vec<u8> as Default>::default(); <T>::g(); a > b }\n",
&[
"fn", "()", "->", "bool", "{}", "<>", "u8", "as", "::", ";", ">",
],
[11, 17],
);
}
#[cfg(feature = "java")]
#[test]
fn java_generic_angles_are_one_pair_1559() {
check_generic_pair(
LANG::Java,
"foo.java",
"class A<T> { List<List<T>> v; boolean m() { return a < b && c > d; } }\n",
&[
"class", "<>", ";", "{}", "boolean", "()", "return", "<", "&&", ">",
],
[10, 14],
);
}
#[cfg(feature = "csharp")]
#[test]
fn csharp_generic_angles_are_one_pair_1559() {
check_generic_pair(
LANG::Csharp,
"foo.cs",
"class A<T> { List<List<T>> v; bool M() { return a < b && c > d; } }\n",
&[
"class", "<>", ";", "{}", "bool", "()", "return", "<", "&&", ">",
],
[10, 14],
);
check_generic_pair(
LANG::Csharp,
"foo.cs",
"unsafe class C { delegate*<int, void> p; delegate* unmanaged[Cdecl]<int, void> q; \
bool M() { return a < b && c > d; } }\n",
&[
"unsafe", "class", "{}", "delegate", "*", "<>", "int", ",", "void", ";", "[]",
"bool", "()", "return", "<", "&&", ">",
],
[17, 26],
);
}
#[cfg(feature = "typescript")]
#[test]
fn typescript_generic_angles_are_one_pair_1559() {
const SOURCE: &str =
"function f<T>(v: Array<Array<T>>): boolean { return a < b && c > d; }\n";
const OPERATORS: &[&str] = &[
"function", "<>", "()", ":", "boolean", "{}", "return", "<", "&&", ">", ";",
];
check_generic_pair(LANG::Typescript, "foo.ts", SOURCE, OPERATORS, [11, 14]);
check_generic_pair(LANG::Tsx, "foo.tsx", SOURCE, OPERATORS, [11, 14]);
}
#[cfg(feature = "cpp")]
#[test]
fn cpp_generic_angles_are_one_pair_1559() {
check_generic_pair(
LANG::Cpp,
"foo.cpp",
"template <class T> bool f(std::vector<std::vector<T>> v) { return a < b && c > d; }\n",
&[
"template", "<>", "class", "bool", "()", "::", "{}", "return", "<", "&&", ">", ";",
],
[12, 15],
);
}
#[cfg(feature = "mozcpp")]
#[test]
fn mozcpp_generic_angles_are_one_pair_1559() {
check_generic_pair(
LANG::Mozcpp,
"foo.cpp",
"template <class T> bool f(std::vector<std::vector<T>> v) { return a < b && c > d; }\n",
&[
"template", "<>", "class", "bool", "()", "::", "{}", "return", "<", "&&", ">", ";",
],
[12, 15],
);
}
#[cfg(feature = "kotlin")]
#[test]
fn kotlin_generic_angles_are_one_pair_1559() {
check_generic_pair(
LANG::Kotlin,
"foo.kt",
"fun <T> f(v: List<List<T>>): Boolean { return a < b && c > d }\n",
&["fun", "<>", "()", ":", "{}", "return", "<", "&&", ">"],
[9, 12],
);
check_generic_pair(
LANG::Kotlin,
"foo.kt",
"class C : A() { fun f(): Boolean { super<A>.f(); return x < y && x > y } }\n",
&[
"class", ":", "()", "{}", "fun", "<>", ".", "return", "<", "&&", ">",
],
[11, 15],
);
}
#[cfg(feature = "groovy")]
#[test]
fn groovy_generic_angles_are_one_pair_1559() {
check_generic_pair(
LANG::Groovy,
"foo.groovy",
"class A<T> { List<List<T>> v; def <U> boolean m() { return a < b && c > d } }\n",
&[
"class", "<>", ";", "{}", "def", "boolean", "()", "return", "<", "&&", ">",
],
[11, 15],
);
}
#[cfg(feature = "objc")]
#[test]
fn objc_misparsed_comparisons_stay_comparisons_1559() {
const COMPARISONS: &[&str] = &["()", ",", ";", "<", ">", "int", "return", "void", "{}"];
check_generic_pair(
LANG::Objc,
"foo.m",
"int f(void) { return g(a < b, c > d); }\n",
COMPARISONS,
[9, 10],
);
check_generic_pair(
LANG::Objc,
"foo.m",
"int f(void) { return g(a<b, c>d); }\n",
COMPARISONS,
[9, 10],
);
check_generic_pair(
LANG::Objc,
"foo.m",
"int f(void) { return g(NSArray<NSString *>); }\n",
&["()", "*", ";", "<>", "int", "return", "void", "{}"],
[8, 9],
);
}
#[cfg(feature = "csharp")]
#[test]
fn csharp_misparsed_comparisons_stay_comparisons_1559() {
check_generic_pair(
LANG::Csharp,
"foo.cs",
"class A { void M() { F(Foo<T>.a < b, c > d); } }\n",
&["()", ",", ".", ";", "<", "<>", ">", "class", "void", "{}"],
[10, 12],
);
check_generic_pair(
LANG::Csharp,
"foo.cs",
"class A { void M() { F(a < b, c > d); } }\n",
&["()", ",", ";", "<", ">", "class", "void", "{}"],
[8, 10],
);
check_generic_pair(
LANG::Csharp,
"foo.cs",
"class A { void M() { F(x.a < b, c > d); } }\n",
&["()", ",", ".", ";", "<", ">", "class", "void", "{}"],
[9, 11],
);
check_generic_pair(
LANG::Csharp,
"foo.cs",
"class A { void M() { F(out List<int> x); } }\n",
&["()", ";", "<>", "class", "int", "out", "void", "{}"],
[8, 10],
);
check_generic_pair(
LANG::Csharp,
"foo.cs",
"class A { void M() { (List<int> p, int q) = (1, 2); } }\n",
&["()", ",", ";", "<>", "=", "class", "int", "void", "{}"],
[9, 14],
);
check_generic_pair(
LANG::Csharp,
"foo.cs",
"using X = List<int>;\n",
&[";", "<>", "=", "int", "using"],
[5, 5],
);
}
#[cfg(feature = "objc")]
#[test]
fn objc_generic_angles_are_one_pair_1559() {
check_generic_pair(
LANG::Objc,
"foo.m",
"@interface A : NSObject <NSCopying>\n@end\n\
id<NSCopying> f(NSArray<NSArray<NSString *> *> *v) \
{ return g(NSArray<NSString *>) || a < b || c > d; }\n",
&[
"@interface",
":",
"<>",
"()",
"*",
"{}",
"return",
"||",
"<",
">",
";",
],
[11, 20],
);
}
}