use std::collections::BTreeSet;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::sync::atomic::{AtomicU64, Ordering};
use tree_sitter::{Node, Parser};
static NEXT: AtomicU64 = AtomicU64::new(0);
fn fixture_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("fixtures/csharp-syntax")
}
fn catalogue_path() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("docs/csharp-coverage.md")
}
fn fixture_files() -> Vec<PathBuf> {
let mut files: Vec<PathBuf> = fs::read_dir(fixture_dir())
.expect("fixtures/csharp-syntax exists")
.map(|entry| entry.unwrap().path())
.filter(|path| path.extension().is_some_and(|ext| ext == "cs"))
.collect();
files.sort();
assert!(
files.len() >= 25,
"expected at least 25 fixture files, found {}",
files.len()
);
files
}
fn file_name(path: &Path) -> String {
path.file_name().unwrap().to_string_lossy().into_owned()
}
const GRAMMAR_GAPS: &[(&str, usize)] = &[("GrammarGaps.cs", 2)];
fn parse_defects(source: &str) -> Vec<String> {
let mut parser = Parser::new();
parser
.set_language(&tree_sitter_c_sharp::LANGUAGE.into())
.expect("C# grammar loads");
let tree = parser.parse(source, None).expect("parse returns a tree");
let mut defects = Vec::new();
collect_defects(tree.root_node(), &mut defects);
defects
}
fn collect_defects(node: Node, defects: &mut Vec<String>) {
if node.is_error() || node.is_missing() {
let label = if node.is_missing() {
"MISSING"
} else {
"ERROR"
};
let point = node.start_position();
defects.push(format!(
"{label} {} at {}:{}",
node.kind(),
point.row + 1,
point.column + 1
));
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
collect_defects(child, defects);
}
}
#[test]
fn every_fixture_file_parses_without_error_or_missing_nodes() {
let mut report = Vec::new();
for path in fixture_files() {
let name = file_name(&path);
let source = fs::read_to_string(&path).unwrap();
let defects = parse_defects(&source);
let allowed = GRAMMAR_GAPS
.iter()
.find(|(file, _)| *file == name)
.map_or(0, |(_, count)| *count);
if defects.len() != allowed {
report.push(format!(
"{name}: {} defect(s), {allowed} documented:\n {}",
defects.len(),
defects.join("\n ")
));
}
}
assert!(
report.is_empty(),
"grammar pin failed:\n{}",
report.join("\n")
);
}
#[test]
fn the_catalogue_names_every_fixture_file_and_every_named_fixture_exists() {
let catalogue = fs::read_to_string(catalogue_path()).expect("docs/csharp-coverage.md exists");
let on_disk: BTreeSet<String> = fixture_files().iter().map(|p| file_name(p)).collect();
let unlisted: Vec<&String> = on_disk
.iter()
.filter(|name| !catalogue.contains(name.as_str()))
.collect();
assert!(
unlisted.is_empty(),
"fixture files missing from docs/csharp-coverage.md: {unlisted:?}"
);
let named: BTreeSet<String> = catalogue
.split(|c: char| c.is_whitespace() || matches!(c, '|' | '`' | ',' | ';' | '(' | ')'))
.filter(|token| token.ends_with(".cs"))
.map(str::to_string)
.collect();
let missing: Vec<&String> = named
.iter()
.filter(|name| !on_disk.contains(*name))
.collect();
assert!(
missing.is_empty(),
"docs/csharp-coverage.md names fixture files that do not exist: {missing:?}"
);
}
struct Fixture {
root: PathBuf,
registry: PathBuf,
}
impl Fixture {
fn new() -> Self {
let root = std::env::temp_dir().join(format!(
"devscout-csharp-syntax-{}-{}",
std::process::id(),
NEXT.fetch_add(1, Ordering::Relaxed)
));
fs::create_dir_all(&root).unwrap();
for path in fixture_files() {
fs::copy(&path, root.join(file_name(&path))).unwrap();
}
let registry = root.join("registry.json");
let fixture = Self { root, registry };
fixture.ok(&["init", "--no-hooks"]);
fixture
}
fn run(&self, args: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_devscout"))
.current_dir(&self.root)
.env("SCOUT_REGISTRY", &self.registry)
.env("HOME", &self.root)
.args(args)
.output()
.unwrap()
}
fn ok(&self, args: &[&str]) -> String {
let output = self.run(args);
assert!(
output.status.success(),
"{args:?}: stdout={} stderr={}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8(output.stdout).unwrap()
}
fn graph(&self) -> serde_json::Value {
let path = self.root.join(".scout/graph/graph.json");
serde_json::from_str(&fs::read_to_string(&path).unwrap()).unwrap()
}
}
impl Drop for Fixture {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.root);
}
}
#[derive(Clone, Copy, PartialEq)]
enum Tier {
Precise,
Ext,
Any,
}
struct Edge(
&'static str,
usize,
&'static str,
&'static str,
&'static str,
Tier,
);
fn str_field<'a>(value: &'a serde_json::Value, key: &str) -> &'a str {
value.get(key).and_then(|v| v.as_str()).unwrap_or("")
}
fn edge_matches(edge: &serde_json::Value, want: &Edge) -> bool {
let Edge(file, line, kind, to, member, tier) = want;
let actual_tier = str_field(edge, "tier");
let tier_ok = match tier {
Tier::Precise => actual_tier.is_empty(),
Tier::Ext => actual_tier == "ext",
Tier::Any => true,
};
let target = if *kind == "imports" { "target" } else { "to" };
str_field(edge, "from_file") == *file
&& (*line == 0 || edge["from_line"].as_u64() == Some(*line as u64))
&& str_field(edge, "kind") == *kind
&& str_field(edge, target) == *to
&& str_field(edge, "member") == *member
&& tier_ok
}
fn describe_edge(want: &Edge) -> String {
let Edge(file, line, kind, to, member, tier) = want;
let tier = match tier {
Tier::Precise => "precise",
Tier::Ext => "ext",
Tier::Any => "any tier",
};
let member = if member.is_empty() {
String::new()
} else {
format!(" member {member}")
};
let line = if *line == 0 {
String::new()
} else {
format!(":{line}")
};
format!("{file}{line} {kind} -> {to}{member} ({tier})")
}
#[rustfmt::skip]
const DEFS: &[(&str, &str, &str)] = &[
("NamespaceBlock.cs", "Syntax.Blocks.Inner.InnerHost", "class"),
("NamespaceBlock.cs", "Syntax.Blocks.BlockHost", "class"),
("NamespaceFileScoped.cs", "Syntax.Scoped.ScopedHost", "class"),
("TypeKinds.cs", "Syntax.Kinds.KindClass", "class"),
("TypeKinds.cs", "Syntax.Kinds.KindAbstract", "class"),
("TypeKinds.cs", "Syntax.Kinds.KindStatic", "class"),
("TypeKinds.cs", "Syntax.Kinds.KindFileLocal", "class"),
("TypeKinds.cs", "Syntax.Kinds.KindSealed", "class"),
("TypeKinds.cs", "Syntax.Kinds.KindStruct", "struct"),
("TypeKinds.cs", "Syntax.Kinds.KindRefStruct", "struct"),
("TypeKinds.cs", "Syntax.Kinds.KindRecord", "record"),
("TypeKinds.cs", "Syntax.Kinds.KindRecordClass", "record"),
("TypeKinds.cs", "Syntax.Kinds.KindRecordStruct", "record"),
("TypeKinds.cs", "Syntax.Kinds.KindReadonlyRecordStruct", "record"),
("TypeKinds.cs", "Syntax.Kinds.IKindInterface", "interface"),
("TypeKinds.cs", "Syntax.Kinds.KindEnum", "enum"),
("TypeKinds.cs", "Syntax.Kinds.KindEnum.Beta", "enum-member"),
("TypeKinds.cs", "Syntax.Kinds.KindFlags", "enum"),
("TypeKinds.cs", "Syntax.Kinds.KindDelegate", "delegate"),
("NestedTypes.cs", "Syntax.Nesting.NestOuter+NestInner", "class"),
("NestedTypes.cs", "Syntax.Nesting.NestOuter+NestMode", "enum"),
("NestedTypes.cs", "Syntax.Nesting.NestOuter+NestPoint", "struct"),
("NestedTypes.cs", "Syntax.Nesting.NestOuter+INestHook", "interface"),
("NestedTypes.cs", "Syntax.Nesting.NestOuter+NestRecord", "record"),
("NestedTypes.cs", "Syntax.Nesting.NestOuter+NestCallback", "delegate"),
("NestedTypes.cs", "Syntax.Nesting.NestOuter+NestInner2+NestDeep", "class"),
("PartialTypesA.cs", "Syntax.Partial.PartialHost", "class"),
("PartialTypesA.cs", "Syntax.Partial.PartialPoint", "struct"),
("PartialTypesA.cs", "Syntax.Partial.IPartialHook", "interface"),
("PartialTypesA.cs", "Syntax.Partial.PartialRecord", "record"),
("Generics.cs", "Syntax.Generics.GenericBox", "class"),
("Generics.cs", "Syntax.Generics.GenericPair", "class"),
("PrimaryConstructors.cs", "Syntax.Primary.PrimaryRecord", "record"),
("PrimaryConstructors.cs", "Syntax.Primary.PrimaryService", "class"),
("StaticAndExtension.cs", "Syntax.Statics.StaticOnly", "class"),
("Program.cs", "Syntax.TopLevel.TopLevelHost", "class"),
("Program.cs", "Syntax.TopLevel.TopLevelHelper", "class"),
("RefAndUnsafe.cs", "Syntax.Memory.RefBuffer", "struct"),
("RefAndUnsafe.cs", "Syntax.Memory.RefView", "struct"),
("StringsAndTrivia.cs", "Syntax.Trivia.@class", "class"),
("StringsAndTrivia.cs", "Syntax.Trivia.Zażółć", "class"),
];
#[rustfmt::skip]
const NAMES: &[(&str, &str, &str, &str)] = &[
("PartialTypesA.cs", "Syntax.Partial.PartialHost", "OnLoaded", "method"),
("PartialTypesB.cs", "Syntax.Partial.PartialHost", "OnLoaded", "method"),
("PartialTypesA.cs", "Syntax.Partial.PartialHost", "Count", "property"),
("PartialTypesB.cs", "Syntax.Partial.PartialHost", "Count", "property"),
("StaticAndExtension.cs", "Syntax.Statics.TextExtensions", "Shout", "method"),
("StaticAndExtension.cs", "Syntax.Statics.TargetExtensions", "Ping", "method"),
("Members.cs", "Syntax.Members.MemberHost", "InstanceField", "field"),
("Members.cs", "Syntax.Members.MemberHost", "StaticField", "field"),
("Members.cs", "Syntax.Members.MemberHost", "Limit", "field"),
("Members.cs", "Syntax.Members.MemberHost", "VolatileField", "field"),
("Members.cs", "Syntax.Members.MemberHost", "Left", "field"),
("Members.cs", "Syntax.Members.MemberHost", "Right", "field"),
("Members.cs", "Syntax.Members.MemberHost", "AutoProperty", "property"),
("Members.cs", "Syntax.Members.MemberHost", "ExpressionProperty", "property"),
("Members.cs", "Syntax.Members.MemberHost", "InitProperty", "property"),
("Members.cs", "Syntax.Members.MemberHost", "RequiredProperty", "property"),
("Members.cs", "Syntax.Members.MemberHost", "Age", "property"),
("Members.cs", "Syntax.Members.MemberHost", "Changed", "event"),
("Members.cs", "Syntax.Members.MemberHost", "Clicked", "event"),
("Members.cs", "Syntax.Members.MemberHost", "AsyncMethod", "method"),
("Members.cs", "Syntax.Members.MemberHost", "StaticMethod", "method"),
("Members.cs", "Syntax.Members.MemberHost", "Describe", "method"),
("Members.cs", "Syntax.Members.MemberBase", "Describe", "method"),
("Members.cs", "Syntax.Members.MemberHost", "ExpressionMethod", "method"),
("Members.cs", "Syntax.Members.MemberHost", "ProtectedInternalMethod", "method"),
("Members.cs", "Syntax.Members.MemberHost", "Hide", "method"),
("Members.cs", "Syntax.Members.MemberHost", "Add", "method"),
("InterfaceMembers.cs", "Syntax.Interfaces.IShapeContract", "Perimeter", "method"),
("InterfaceMembers.cs", "Syntax.Interfaces.IShapeContract", "Create", "method"),
("InterfaceMembers.cs", "Syntax.Interfaces.IShapeContract", "Kind", "property"),
("InterfaceMembers.cs", "Syntax.Interfaces.IShapeContract", "Counter", "field"),
("RefAndUnsafe.cs", "Syntax.Memory.MemUser", "Sum", "method"),
("RefAndUnsafe.cs", "Syntax.Memory.MemUser", "SumSpan", "method"),
];
#[rustfmt::skip]
const EDGES: &[Edge] = &[
Edge("NamespaceBlock.cs", 14, "imports", "Syntax.Blocks", "", Tier::Precise),
Edge("NestedTypes.cs", 43, "uses-type", "Syntax.Nesting.NestOuter+NestInner", "", Tier::Precise),
Edge("NestedTypes.cs", 51, "uses-type", "Syntax.Nesting.NestOuter+NestInner2+NestDeep", "", Tier::Precise),
Edge("NestedTypes.cs", 53, "uses-member", "Syntax.Nesting.NestOuter+NestMode.Fast", "Fast", Tier::Precise),
Edge("Generics.cs", 99, "uses-member", "Syntax.Generics.GenericMethods", "Convert", Tier::Precise),
Edge("Generics.cs", 102, "uses-member", "Syntax.Generics.GenericMethods", "UseSelfReferential", Tier::Precise),
Edge("PrimaryConstructors.cs", 15, "uses-member", "Syntax.Primary.PrimaryDependency", "Describe", Tier::Precise),
Edge("PrimaryConstructors.cs", 6, "inherits", "Syntax.Primary.PrimaryRecord", "", Tier::Precise),
Edge("PrimaryConstructors.cs", 28, "inherits", "Syntax.Primary.PrimaryBase", "", Tier::Precise),
Edge("StaticAndExtension.cs", 46, "uses-member", "Syntax.Statics.StaticOnly", "Reset", Tier::Precise),
Edge("StaticAndExtension.cs", 45, "uses-member", "Syntax.Statics.TargetExtensions", "Ping", Tier::Ext),
Edge("StaticAndExtension.cs", 47, "uses-member", "Syntax.Statics.TextExtensions", "Shout", Tier::Precise),
Edge("Members.cs", 135, "uses-member", "Syntax.Members.MemberHost", "Age", Tier::Precise),
Edge("Members.cs", 74, "ctor-di", "Syntax.Members.MemberDependency", "", Tier::Precise),
Edge("Members.cs", 74, "uses-type", "Syntax.Members.IMemberDependency", "", Tier::Precise),
Edge("Members.cs", 79, "uses-type", "Syntax.Members.IMemberDependency", "", Tier::Precise),
Edge("LocalFunctions.cs", 21, "uses-member", "Syntax.Locals.LocalItem", "Score", Tier::Precise),
Edge("LocalFunctions.cs", 22, "uses-member", "Syntax.Locals.LocalItem", "Value", Tier::Precise),
Edge("Program.cs", 4, "uses-type", "Syntax.TopLevel.TopLevelHost", "", Tier::Precise),
Edge("Program.cs", 6, "uses-member", "Syntax.TopLevel.TopLevelHelper", "Answer", Tier::Precise),
Edge("InterfaceMembers.cs", 66, "uses-member", "Syntax.Interfaces.IShapeContract", "Bump", Tier::Precise),
Edge("Program.cs", 2, "imports", "Syntax.TopLevel", "", Tier::Precise),
Edge("Usings.cs", 5, "imports", "Syntax.Usings.UsingStatics", "", Tier::Precise),
Edge("Usings.cs", 6, "imports", "System.Text.StringBuilder", "", Tier::Precise),
Edge("Usings.cs", 2, "imports", "System.Collections.Concurrent", "", Tier::Precise),
Edge("Usings.cs", 3, "imports", "System.Math", "", Tier::Precise),
Edge("Usings.cs", 4, "imports", "System.Text.StringBuilder", "", Tier::Precise),
Edge("TypeKinds.cs", 4, "inherits", "Syntax.Kinds.IKindInterface", "", Tier::Precise),
Edge("InterfaceMembers.cs", 25, "inherits", "Syntax.Interfaces.IShapeContract", "", Tier::Precise),
Edge("Members.cs", 74, "uses-type", "Syntax.Members.IMemberDependency", "", Tier::Precise),
Edge("Members.cs", 79, "uses-type", "Syntax.Members.IMemberDependency", "", Tier::Precise),
Edge("Generics.cs", 88, "uses-type", "Syntax.Generics.GenericItem", "", Tier::Precise),
Edge("Generics.cs", 94, "uses-type", "Syntax.Generics.GenericItem", "", Tier::Precise),
Edge("MemberAccess.cs", 8, "uses-type", "Syntax.Access.AccessTarget", "", Tier::Precise),
Edge("TuplesAndWith.cs", 24, "uses-type", "Syntax.Tuples.TupleRecord", "", Tier::Precise),
Edge("MemberAccess.cs", 33, "uses-member", "Syntax.Access.AccessTarget", "Count", Tier::Precise),
Edge("MemberAccess.cs", 34, "uses-member", "Syntax.Access.AccessTarget", "Name", Tier::Precise),
Edge("MemberAccess.cs", 35, "uses-member", "Syntax.Access.AccessTarget", "Compute", Tier::Precise),
Edge("MemberAccess.cs", 36, "uses-member", "Syntax.Access.AccessTarget", "Generic", Tier::Precise),
Edge("MemberAccess.cs", 45, "uses-member", "Syntax.Access.AccessTarget", "Shared", Tier::Precise),
Edge("MemberAccess.cs", 41, "uses-member", "Syntax.Access.AccessUser", "Run2", Tier::Precise),
Edge("MemberAccess.cs", 42, "uses-member", "Syntax.Access.AccessBase", "BaseField", Tier::Precise),
Edge("MemberAccess.cs", 43, "uses-member", "Syntax.Access.AccessBase", "Hook", Tier::Precise),
Edge("MemberAccess.cs", 37, "uses-member", "Syntax.Access.AccessTarget", "Compute", Tier::Precise),
Edge("MemberAccess.cs", 38, "uses-member", "Syntax.Access.AccessTarget", "Next", Tier::Precise),
Edge("MemberAccess.cs", 61, "uses-member", "Syntax.Access.AccessTarget", "Next", Tier::Precise),
Edge("MemberAccess.cs", 63, "uses-member", "Syntax.Access.AccessTarget", "Self", Tier::Precise),
Edge("MemberAccess.cs", 63, "uses-member", "Syntax.Access.AccessTarget", "Compute", Tier::Precise),
Edge("MemberAccess.cs", 47, "uses-member", "Syntax.Access.AccessTarget", "Hook2", Tier::Precise),
Edge("MemberAccess.cs", 54, "uses-member", "Syntax.Access.AccessTarget", "Compute", Tier::Precise),
Edge("MemberAccess.cs", 56, "uses-member", "Syntax.Access.AccessTarget", "Compute", Tier::Precise),
Edge("MemberAccess.cs", 59, "uses-member", "Syntax.Access.AccessTarget", "Compute", Tier::Precise),
Edge("MemberAccess.cs", 60, "uses-member", "Syntax.Access.AccessTarget", "Compute", Tier::Precise),
Edge("MemberAccess.cs", 68, "uses-member", "Syntax.Access.AccessTarget", "Compute", Tier::Precise),
Edge("ObjectCreation.cs", 27, "uses-type", "Syntax.Creation.CreatedItem", "", Tier::Precise),
Edge("ObjectCreation.cs", 43, "uses-type", "Syntax.Creation.CreatedItem", "", Tier::Precise),
Edge("ObjectCreation.cs", 28, "uses-type", "Syntax.Creation.CreatedItem", "", Tier::Precise),
Edge("ObjectCreation.cs", 30, "uses-member", "Syntax.Creation.CreatedItem", "Tags", Tier::Precise),
Edge("TypeOperators.cs", 19, "uses-type", "Syntax.TypeOps.TypeOpTarget", "", Tier::Precise),
Edge("TypeOperators.cs", 24, "uses-type", "Syntax.TypeOps.TypeOpTarget", "", Tier::Precise),
Edge("TypeOperators.cs", 21, "uses-type", "Syntax.TypeOps.TypeOpGeneric", "", Tier::Precise),
Edge("TypeOperators.cs", 52, "uses-member", "Syntax.TypeOps.TypeOpTarget", "Value", Tier::Precise),
Edge("TypeOperators.cs", 38, "uses-member", "Syntax.TypeOps.TypeOpTarget", "Value", Tier::Precise),
Edge("Patterns.cs", 68, "uses-member", "Syntax.Patterns.PatternShape", "Area", Tier::Precise),
Edge("Patterns.cs", 79, "uses-member", "Syntax.Patterns.PatternShape", "Area", Tier::Precise),
Edge("Patterns.cs", 80, "uses-member", "Syntax.Patterns.PatternShape", "Area", Tier::Precise),
Edge("Patterns.cs", 52, "uses-member", "Syntax.Patterns.PatternKind.A", "A", Tier::Precise),
Edge("Patterns.cs", 53, "uses-member", "Syntax.Patterns.PatternKind.A", "A", Tier::Precise),
Edge("Lambdas.cs", 27, "uses-member", "Syntax.Lambdas.LambdaItem", "Score", Tier::Precise),
Edge("Lambdas.cs", 28, "uses-member", "Syntax.Lambdas.LambdaItem", "Owner", Tier::Precise),
Edge("Lambdas.cs", 29, "uses-member", "Syntax.Lambdas.LambdaItem", "Score", Tier::Precise),
Edge("Lambdas.cs", 68, "uses-member", "Syntax.Lambdas.LambdaOwner", "Rank", Tier::Precise),
Edge("Lambdas.cs", 30, "uses-member", "Syntax.Lambdas.LambdaItem", "Score", Tier::Precise),
Edge("AsyncAndYield.cs", 44, "uses-member", "Syntax.Async.AsyncItem", "Score", Tier::Precise),
Edge("AsyncAndYield.cs", 50, "uses-member", "Syntax.Async.AsyncRes", "Touch", Tier::Precise),
Edge("AsyncAndYield.cs", 54, "uses-member", "Syntax.Async.AsyncRes", "Touch", Tier::Precise),
Edge("AsyncAndYield.cs", 64, "uses-member", "Syntax.Async.AsyncItem", "Score", Tier::Precise),
Edge("AsyncAndYield.cs", 72, "uses-member", "Syntax.Async.AsyncService", "GetAsync", Tier::Precise),
Edge("AsyncAndYield.cs", 83, "uses-member", "Syntax.Async.AsyncService", "GetAsync", Tier::Precise),
Edge("StatementsAndScopes.cs", 185, "uses-member", "Syntax.Statements.StmtRes", "Touch", Tier::Precise),
Edge("StatementsAndScopes.cs", 190, "uses-member", "Syntax.Statements.StmtRes", "Touch", Tier::Precise),
Edge("StatementsAndScopes.cs", 34, "uses-member", "Syntax.Statements.StmtGate", "Sync", Tier::Precise),
Edge("StatementsAndScopes.cs", 100, "uses-type", "Syntax.Statements.StmtError", "", Tier::Precise),
Edge("StatementsAndScopes.cs", 125, "uses-type", "Syntax.Statements.StmtError", "", Tier::Precise),
Edge("StatementsAndScopes.cs", 126, "uses-member", "Syntax.Statements.StmtGate", "Sync", Tier::Any),
Edge("StatementsAndScopes.cs", 133, "uses-member", "Syntax.Statements.StmtRes", "Touch", Tier::Precise),
Edge("StatementsAndScopes.cs", 137, "uses-member", "Syntax.Statements.StmtRes", "Touch", Tier::Precise),
Edge("RefAndUnsafe.cs", 42, "uses-type", "Syntax.Memory.MemPoint", "", Tier::Precise),
Edge("RefAndUnsafe.cs", 46, "uses-type", "Syntax.Memory.MemPoint", "", Tier::Precise),
Edge("RefAndUnsafe.cs", 70, "uses-member", "Syntax.Memory.MemPoint", "Y", Tier::Precise),
Edge("StringsAndTrivia.cs", 51, "uses-member", "Syntax.Trivia.TriviaHost", "Name", Tier::Precise),
Edge("StringsAndTrivia.cs", 51, "uses-member", "Syntax.Trivia.TriviaHost", "Describe", Tier::Precise),
Edge("StringsAndTrivia.cs", 51, "uses-member", "Syntax.Trivia.TriviaHost", "Value", Tier::Precise),
Edge("StringsAndTrivia.cs", 71, "uses-member", "Syntax.Trivia.@class", "@event", Tier::Precise),
Edge("StringsAndTrivia.cs", 97, "uses-member", "Syntax.Trivia.Zażółć", "Wartość", Tier::Precise),
Edge("Preprocessor.cs", 7, "imports", "System.Text", "", Tier::Precise),
Edge("Preprocessor.cs", 56, "uses-member", "Syntax.Preproc.PreprocMarker", "Compound", Tier::Precise),
Edge("Preprocessor.cs", 62, "uses-member", "Syntax.Preproc.PreprocMarker", "Nested", Tier::Precise),
];
#[test]
fn every_must_row_that_produces_at_base_keeps_producing() {
let fixture = Fixture::new();
let graph = fixture.graph();
let defs = graph["defs"].as_array().unwrap();
let names = graph["names"].as_array().unwrap();
let edges = graph["edges"].as_array().unwrap();
let mut missing = Vec::new();
for (file, id, kind) in DEFS {
let found = defs.iter().any(|def| {
str_field(def, "file") == *file
&& str_field(def, "id") == *id
&& str_field(def, "kind") == *kind
});
if !found {
missing.push(format!("def {file} {id} ({kind})"));
}
}
for (file, owner, name, kind) in NAMES {
let found = names.iter().any(|entry| {
str_field(entry, "file") == *file
&& str_field(entry, "owner") == *owner
&& str_field(entry, "name") == *name
&& str_field(entry, "kind") == *kind
});
if !found {
missing.push(format!("name {file} {owner}::{name} ({kind})"));
}
}
for want in EDGES {
if !edges.iter().any(|edge| edge_matches(edge, want)) {
missing.push(format!("edge {}", describe_edge(want)));
}
}
assert!(
missing.is_empty(),
"{} catalogue row(s) stopped producing their fact:\n {}",
missing.len(),
missing.join("\n ")
);
}
#[test]
fn partial_types_merge_into_one_declaration_with_every_site() {
let fixture = Fixture::new();
let graph = fixture.graph();
let host = graph["defs"]
.as_array()
.unwrap()
.iter()
.find(|def| str_field(def, "id") == "Syntax.Partial.PartialHost")
.expect("PartialHost is declared");
assert_eq!(str_field(host, "file"), "PartialTypesA.cs");
assert_eq!(
host["also_in"][0]["file"].as_str(),
Some("PartialTypesB.cs"),
"{host}"
);
let methods: Vec<&str> = host["methods"]
.as_array()
.unwrap()
.iter()
.filter_map(|m| m.as_str())
.collect();
assert!(
methods.contains(&"Alpha") && methods.contains(&"Beta"),
"{methods:?}"
);
}