#![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, Node};
use crate::spaces::{SpaceKind, line_span, push_children};
use crate::halstead::{Halstead, HalsteadMaps};
use crate::traits::ParserTrait;
#[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: ParserTrait>(
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: ParserTrait>(
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: ParserTrait>(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: ParserTrait>(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: ParserTrait>(
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 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);
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);
push_children(
&mut cursor,
&node,
Walk {
level: new_level,
depth: depth + 1,
},
&mut stack,
);
}
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};
#[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);
}
#[test]
fn python_ops() {
check_ops(
LANG::Python,
"if True:
a = 1 + 2",
"foo.py",
&mut ["if", "=", "+"],
&mut ["True", "a", "1", "2"],
);
}
#[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"],
);
}
#[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"],
);
}
#[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\"",
],
);
}
#[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"],
);
}
#[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", "\"{}\""],
);
}
#[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",
"console",
"log",
"\"{}\"",
],
);
}
#[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",
"console",
"log",
"\"{}\"",
],
);
}
#[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",
"console",
"log",
"\"{}\"",
],
);
}
#[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",
"console",
"log",
"\"{}\"",
],
);
}
#[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",
"console",
"log",
"\"{}\"",
"string",
],
);
}
#[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",
"console",
"log",
"\"{}\"",
"string",
],
);
}
#[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",
"console",
"log",
"\"{}\"",
"string",
],
);
}
#[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",
"console",
"log",
"\"{}\"",
"string",
],
);
}
#[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"],
);
}
#[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"],
);
}
#[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 [
"{}", "void", "()", "[]", ",", ".", ";", "int", "=", "+", "/",
],
&mut [
"Main",
"main",
"args",
"a",
"b",
"c",
"avg",
"5",
"3",
"MessageFormat",
"format",
"\"{0}\"",
],
);
}
#[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 [
"{}",
";",
"=",
"byte",
"short",
"int",
"long",
"char",
"float",
"double",
"boolean_type",
],
&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),
],
);
}
}