use serde_json::Value;
use crate::spaces::SpaceKind;
use crate::test_support::{assert_fixtures_present, space_verbatim};
use crate::{LANG, MetricsOptions};
struct Emitted {
kind: SpaceKind,
name: Option<String>,
has_npm: bool,
has_npa: bool,
has_wmc: bool,
}
fn emitted_spaces(lang: LANG, source: &str) -> Vec<Emitted> {
let space = space_verbatim(lang, source.as_bytes(), MetricsOptions::default());
let value = serde_json::to_value(&space).expect("FuncSpace must serialize");
let mut out = Vec::new();
flatten(&value, &mut out);
out
}
fn flatten(value: &Value, out: &mut Vec<Emitted>) {
let metrics = value["metrics"]
.as_object()
.expect("every space serializes a metrics object");
let kind = value["kind"]
.as_str()
.expect("every space serializes a kind");
out.push(Emitted {
kind: SpaceKind::from_serialized(kind),
name: value["name"].as_str().map(str::to_owned),
has_npm: metrics.contains_key("npm"),
has_npa: metrics.contains_key("npa"),
has_wmc: metrics.contains_key("wmc"),
});
for child in value["spaces"].as_array().into_iter().flatten() {
flatten(child, out);
}
}
struct Fixture {
lang: LANG,
containers: &'static [&'static str],
source: &'static str,
}
fn fixture_source(lang: LANG) -> &'static str {
FIXTURES
.iter()
.find(|f| f.lang == lang)
.unwrap_or_else(|| panic!("no fixture for {lang:?}"))
.source
}
fn summary(spaces: &[Emitted]) -> Vec<(SpaceKind, Option<&str>)> {
spaces.iter().map(|s| (s.kind, s.name.as_deref())).collect()
}
#[track_caller]
fn only_space<'a>(lang: LANG, spaces: &'a [Emitted], name: &str) -> &'a Emitted {
let found: Vec<&Emitted> = spaces
.iter()
.filter(|s| s.name.as_deref() == Some(name))
.collect();
assert_eq!(
found.len(),
1,
"{lang:?}: expected exactly one space named {name:?}, got {:?}",
summary(spaces)
);
found[0]
}
const FIXTURES: &[Fixture] = &[
#[cfg(feature = "kotlin")]
Fixture {
lang: LANG::Kotlin,
containers: &["C", "I"],
source: "\
interface I {
fun q(): Int
}
class C : I {
var p: Int = 0
get() = field
set(v) { field = v }
init { p = 1 }
override fun q(): Int { return p }
}
",
},
#[cfg(feature = "java")]
Fixture {
lang: LANG::Java,
containers: &["C", "I"],
source: "\
interface I {
int q();
}
class C implements I {
public int a = 1;
static { System.out.println(\"x\"); }
public int q() { return a; }
}
",
},
#[cfg(feature = "groovy")]
Fixture {
lang: LANG::Groovy,
containers: &["C", "I"],
source: "\
interface I {
int q()
}
class C implements I {
public int a = 1
static { println 'x' }
int q() { return a }
}
",
},
#[cfg(feature = "javascript")]
Fixture {
lang: LANG::Javascript,
containers: &["C"],
source: "\
class C {
a = 1;
static { this.b = 2; }
q() { return this.a; }
}
function top(x) { return x; }
",
},
#[cfg(feature = "mozjs")]
Fixture {
lang: LANG::Mozjs,
containers: &["C"],
source: "\
class C {
a = 1;
static { this.b = 2; }
q() { return this.a; }
}
function top(x) { return x; }
",
},
#[cfg(feature = "typescript")]
Fixture {
lang: LANG::Typescript,
containers: &["C", "I"],
source: "\
interface I {
q(): number;
}
class C implements I {
public a: number = 1;
static { }
public q(): number { return this.a; }
}
function top(x: number): number { return x; }
",
},
#[cfg(feature = "typescript")]
Fixture {
lang: LANG::Tsx,
containers: &["C", "I"],
source: "\
interface I {
q(): number;
}
class C implements I {
public a: number = 1;
static { }
public q(): number { return this.a; }
}
function top(x: number): number { return x; }
",
},
#[cfg(feature = "csharp")]
Fixture {
lang: LANG::Csharp,
containers: &["C", "I"],
source: "\
interface I {
int Q();
}
class C : I {
public int A = 1;
private int[] _v = new int[4];
// An expression-bodied property and an accessor-less indexer are
// `is_func_space` and `SpaceKind::Function` (#464, #472), so before
// #1197 each carried an all-zero block beside `Q`, which had none.
public int W => A;
public int this[int i] => _v[i];
public int Q() { return A; }
}
",
},
#[cfg(feature = "php")]
Fixture {
lang: LANG::Php,
containers: &["C", "I"],
source: "\
<?php
interface I {
public function q();
}
class C implements I {
public $a = 1;
public function q() { return $this->a; }
}
function top($x) { return $x; }
",
},
#[cfg(feature = "ruby")]
Fixture {
lang: LANG::Ruby,
containers: &["M", "C"],
source: "\
module M
class C
attr_accessor :a
def q
@a
end
end
end
",
},
#[cfg(feature = "rust")]
Fixture {
lang: LANG::Rust,
containers: &["T", "S"],
source: "\
pub struct S {
pub a: u8,
b: u8,
}
pub trait T {
fn q(&self) -> u8;
}
impl S {
pub fn m(&self) -> u8 { self.a }
}
fn top() -> u8 {
struct Inner { pub x: u8 }
Inner { x: 1 }.x
}
",
},
#[cfg(feature = "go")]
Fixture {
lang: LANG::Go,
containers: &[],
source: "\
package main
type S struct {
Pub int
priv int
}
type I interface {
Speak() string
}
func (s S) Method() int { return s.Pub }
func Outer() int {
type inner struct {
X int
}
return inner{X: 1}.X
}
",
},
#[cfg(feature = "python")]
Fixture {
lang: LANG::Python,
containers: &["C", "Inner"],
source: "\
class C:
a = 1
def q(self):
return self.a
def top(x):
class Inner:
b = 2
return Inner
",
},
#[cfg(feature = "cpp")]
Fixture {
lang: LANG::Cpp,
containers: &["N", "C"],
source: "\
namespace N {
class C {
public:
int a;
int q() { return a; }
};
}
int top() { return 0; }
",
},
#[cfg(feature = "mozcpp")]
Fixture {
lang: LANG::Mozcpp,
containers: &["N", "C"],
source: "\
namespace N {
class C {
public:
int a;
int q() { return a; }
};
}
int top() { return 0; }
",
},
#[cfg(feature = "objc")]
Fixture {
lang: LANG::Objc,
containers: &["P", "C"],
source: "\
@protocol P
- (int)r;
@end
@implementation C {
int a;
}
- (int)q { return a; }
@end
",
},
#[cfg(feature = "elixir")]
Fixture {
lang: LANG::Elixir,
containers: &["Outer", "Inner", "Sibling"],
source: "\
defmodule Outer do
defstruct [:a]
def q, do: 1
defp r, do: 2
defmodule Inner do
def s, do: 3
end
end
defmodule Sibling do
def t, do: 4
end
",
},
];
#[test]
fn containers_emit_npm_and_npa() {
assert_fixtures_present(FIXTURES);
for fixture in FIXTURES {
assert_eq!(
fixture.containers.is_empty(),
fixture.lang == LANG::Go,
"{:?}: only Go has no container SpaceKind",
fixture.lang
);
let spaces = emitted_spaces(fixture.lang, fixture.source);
for want in fixture.containers {
let space = only_space(fixture.lang, &spaces, want);
assert!(
matches!(
space.kind,
SpaceKind::Class
| SpaceKind::Interface
| SpaceKind::Namespace
| SpaceKind::Struct
| SpaceKind::Trait
| SpaceKind::Impl
),
"{:?}: {want:?} should be a container kind, is {:?}",
fixture.lang,
space.kind
);
assert!(
space.has_npm && space.has_npa,
"{:?}: container {want:?} must emit npm and npa (npm={}, npa={})",
fixture.lang,
space.has_npm,
space.has_npa
);
}
}
}
#[test]
fn function_spaces_emit_neither() {
assert_fixtures_present(FIXTURES);
for fixture in FIXTURES {
let spaces = emitted_spaces(fixture.lang, fixture.source);
let functions: Vec<&Emitted> = spaces
.iter()
.filter(|s| s.kind == SpaceKind::Function)
.collect();
assert!(
!functions.is_empty(),
"{:?}: expected at least one function space, got {:?}",
fixture.lang,
summary(&spaces)
);
for space in functions {
assert!(
!space.has_npm && !space.has_npa,
"{:?}: {:?} space {:?} must not emit npm/npa (npm={}, npa={})",
fixture.lang,
space.kind,
space.name,
space.has_npm,
space.has_npa
);
}
}
}
#[test]
fn the_file_root_keeps_its_rollup() {
assert_fixtures_present(FIXTURES);
for fixture in FIXTURES {
let spaces = emitted_spaces(fixture.lang, fixture.source);
let root = spaces.first().expect("the root space is always emitted");
assert_eq!(root.kind, SpaceKind::Unit, "{:?}: root kind", fixture.lang);
assert!(
root.has_npm && root.has_npa,
"{:?}: the unit root must keep its npm/npa roll-up (npm={}, npa={})",
fixture.lang,
root.has_npm,
root.has_npa
);
}
}
#[test]
fn no_space_is_emitted_with_an_unknown_kind() {
assert_fixtures_present(FIXTURES);
for fixture in FIXTURES {
let spaces = emitted_spaces(fixture.lang, fixture.source);
assert!(
spaces.len() > 1,
"{:?}: expected at least one space below the root, got {:?}",
fixture.lang,
summary(&spaces)
);
for space in &spaces {
assert!(
space.kind != SpaceKind::Unknown,
"{:?}: space {:?} was promoted to a space but classified \
Unknown, so it serializes no npm/npa block; every space \
was {:?}",
fixture.lang,
space.name,
summary(&spaces)
);
}
}
}
#[test]
#[cfg(any(
feature = "kotlin",
feature = "java",
feature = "groovy",
feature = "javascript",
feature = "mozjs",
feature = "typescript"
))]
fn the_1184_constructs_open_quiet_function_spaces() {
let cases: &[(LANG, &[&str])] = &[
#[cfg(feature = "kotlin")]
(LANG::Kotlin, &["<get>", "<set>", "<init>"]),
#[cfg(feature = "java")]
(LANG::Java, &["<static-init>"]),
#[cfg(feature = "groovy")]
(LANG::Groovy, &["<static-init>"]),
#[cfg(feature = "javascript")]
(LANG::Javascript, &["<static-init>"]),
#[cfg(feature = "mozjs")]
(LANG::Mozjs, &["<static-init>"]),
#[cfg(feature = "typescript")]
(LANG::Typescript, &["<static-init>"]),
#[cfg(feature = "typescript")]
(LANG::Tsx, &["<static-init>"]),
];
assert_fixtures_present(cases);
for (lang, names) in cases {
let spaces = emitted_spaces(*lang, fixture_source(*lang));
for name in *names {
let space = only_space(*lang, &spaces, name);
assert_eq!(space.kind, SpaceKind::Function, "{lang:?}: {name:?}");
assert!(
!space.has_npm && !space.has_npa,
"{lang:?}: {name:?} must not emit npm/npa"
);
}
}
}
#[test]
#[cfg(feature = "java")]
fn container_counts_are_independent_of_the_emission_gate() {
let space = space_verbatim(
LANG::Java,
fixture_source(LANG::Java).as_bytes(),
MetricsOptions::default(),
);
let class = crate::test_support::child_space(&space, "C");
assert_eq!(class.metrics.npm.class_npm_sum(), 1, "public method `q`");
assert_eq!(class.metrics.npa.class_npa_sum(), 1, "public attribute `a`");
let interface = crate::test_support::child_space(&space, "I");
assert_eq!(interface.metrics.npm.interface_npm_sum(), 1, "`I::q`");
assert_eq!(space.metrics.npm.class_npm_sum(), 1);
assert_eq!(space.metrics.npa.class_npa_sum(), 1);
}
#[test]
#[cfg(any(feature = "go", feature = "rust"))]
fn a_type_declared_inside_a_function_reaches_the_root_rollup() {
let cases: &[(LANG, &str, u64, u64)] = &[
#[cfg(feature = "go")]
(LANG::Go, "Outer", 3, 2),
#[cfg(feature = "rust")]
(LANG::Rust, "top", 3, 2),
];
assert_fixtures_present(cases);
for (lang, holder, na_sum, npa_sum) in cases {
let source = fixture_source(*lang);
let spaces = emitted_spaces(*lang, source);
let function = only_space(*lang, &spaces, holder);
assert_eq!(function.kind, SpaceKind::Function, "{lang:?}: {holder:?}");
assert!(
!function.has_npm && !function.has_npa,
"{lang:?}: {holder:?} holds a nested type but must not carry a block \
(npm={}, npa={})",
function.has_npm,
function.has_npa
);
let root = spaces.first().expect("the root space is always emitted");
assert!(
root.has_npm && root.has_npa,
"{lang:?}: the unit root must carry the roll-up (npm={}, npa={})",
root.has_npm,
root.has_npa
);
let space = space_verbatim(*lang, source.as_bytes(), MetricsOptions::default());
assert_eq!(
space.metrics.npa.class_na_sum(),
*na_sum,
"{lang:?}: root attributes, including the type declared in {holder:?}"
);
assert_eq!(
space.metrics.npa.class_npa_sum(),
*npa_sum,
"{lang:?}: root public attributes, including the type declared in {holder:?}"
);
}
}
#[test]
#[cfg(feature = "cpp")]
fn a_cpp_namespace_is_a_member_scope() {
let spaces = emitted_spaces(LANG::Cpp, fixture_source(LANG::Cpp));
let namespace = only_space(LANG::Cpp, &spaces, "N");
assert_eq!(namespace.kind, SpaceKind::Namespace);
assert!(
namespace.has_npm && namespace.has_npa,
"a namespace rolls its classes up and must carry both blocks \
(npm={}, npa={})",
namespace.has_npm,
namespace.has_npa
);
assert!(
!namespace.has_wmc,
"a namespace weights no per-class complexity and must carry no wmc"
);
let class = only_space(LANG::Cpp, &spaces, "C");
assert!(
class.has_wmc && class.has_npm && class.has_npa,
"the class inside the namespace carries all three \
(wmc={}, npm={}, npa={})",
class.has_wmc,
class.has_npm,
class.has_npa
);
}
#[test]
#[cfg(feature = "ruby")]
fn a_ruby_module_is_a_namespace_without_wmc() {
let spaces = emitted_spaces(LANG::Ruby, fixture_source(LANG::Ruby));
let module = only_space(LANG::Ruby, &spaces, "M");
assert_eq!(module.kind, SpaceKind::Namespace);
assert!(
module.has_npm && module.has_npa && !module.has_wmc,
"a Ruby module carries npm/npa and no wmc \
(npm={}, npa={}, wmc={})",
module.has_npm,
module.has_npa,
module.has_wmc
);
let class = only_space(LANG::Ruby, &spaces, "C");
assert!(
class.has_wmc && class.has_npm && class.has_npa,
"the class inside the module carries all three \
(wmc={}, npm={}, npa={})",
class.has_wmc,
class.has_npm,
class.has_npa
);
}
#[test]
#[cfg(feature = "go")]
fn go_emits_npa_and_npm_but_never_wmc() {
let spaces = emitted_spaces(LANG::Go, fixture_source(LANG::Go));
let root = &spaces[0];
assert_eq!(root.kind, SpaceKind::Unit);
assert!(
root.has_npm && root.has_npa,
"Go's root carries npa/npm (npm={}, npa={})",
root.has_npm,
root.has_npa
);
assert!(
!spaces.iter().any(|space| space.has_wmc),
"no Go space may carry a wmc block, including the unit root"
);
}
#[test]
#[cfg(any(feature = "bash", feature = "lua", feature = "c"))]
fn a_language_with_no_member_construct_emits_neither_block() {
let cases: &[(LANG, &str)] = &[
#[cfg(feature = "bash")]
(LANG::Bash, "foo() { echo hi; }\nfoo\n"),
#[cfg(feature = "lua")]
(LANG::Lua, "function f(a) return a end\n"),
#[cfg(feature = "c")]
(LANG::C, "int add(int a, int b) { return a + b; }\n"),
];
assert_fixtures_present(cases);
for (lang, source) in cases {
let spaces = emitted_spaces(*lang, source);
let root = spaces.first().expect("the root space is always emitted");
assert_eq!(root.kind, SpaceKind::Unit, "{lang:?}: root kind");
assert!(
!root.has_npm && !root.has_npa,
"{lang:?}: a grammar with no member construct must emit neither \
block (npm={}, npa={})",
root.has_npm,
root.has_npa
);
}
}
#[test]
#[cfg(feature = "elixir")]
fn nested_elixir_modules_are_not_double_counted() {
use crate::test_support::child_space;
let root = space_verbatim(
LANG::Elixir,
fixture_source(LANG::Elixir).as_bytes(),
MetricsOptions::default(),
);
let outer = child_space(&root, "Outer");
assert_eq!(outer.metrics.npm.class_nm_sum(), 3, "Outer: q, r, Inner::s");
assert_eq!(outer.metrics.npm.class_npm_sum(), 2, "Outer: q, Inner::s");
assert_eq!(outer.metrics.npa.class_na_sum(), 1, "Outer: defstruct :a");
let inner = child_space(outer, "Inner");
assert_eq!(inner.metrics.npm.class_nm_sum(), 1, "Inner: s");
assert_eq!(
inner.metrics.npa.class_na_sum(),
0,
"Inner has no defstruct"
);
let sibling = child_space(&root, "Sibling");
assert_eq!(sibling.metrics.npm.class_nm_sum(), 1, "Sibling: t");
assert_eq!(root.metrics.npm.class_nm_sum(), 4, "q, r, Inner::s, t");
assert_eq!(root.metrics.npm.class_npm_sum(), 3, "q, Inner::s, t");
}