use std::collections::{BTreeMap, BTreeSet};
use pedant_graph::{CodeGraph, GraphCache, GraphEdgeOrigin, GraphNode, GraphNodeId};
use super::cache_counting::{Classification, assert_build_classifies};
use super::cache_fixture::{assert_matches_direct, pair, projection_only, restated};
use super::corpus_revision::{REVISION_CORPUS, REVISION_FRAGMENTS};
use super::fixture::{self, CORPUS_LIBRARY};
use super::inventory::SOURCES;
use super::promotion::Restatement;
use super::scan::{parsed, token_text};
use super::surface::declared_items;
const RETAINED_PROJECTION_ROOTS: &[(&str, &str)] = &[
("src/rust/claim.rs", "SourceKey"),
("src/projection/draft.rs", "SourceFragment"),
];
const REACHED_PROJECTION_TYPES: &[&str] = &[
"SourceKey",
"SourceFragment",
"SourceIdentity",
"DefinitionProjection",
"DefinitionIdentity",
"ReferenceProjection",
"CandidateProjection",
];
const DENSE_IDENTITIES: &[&str] = &["GraphNodeId", "GraphReferenceId", "GraphEdgeId"];
pub struct DeclaredRecord {
path: &'static str,
columns: Vec<(String, String)>,
}
pub fn assert_retained_projections_hold_no_dense_identity() {
let declared = declared_records();
assert_roots_are_declared_where_modelled(&declared);
let reached = reached_from_roots(&declared);
let names: BTreeSet<&str> = reached.iter().map(|(name, _)| *name).collect();
for expected in REACHED_PROJECTION_TYPES {
assert!(
names.contains(expected),
"the projection walk must reach {expected}, reaching {names:?}"
);
}
let offenders: Vec<String> = reached
.iter()
.flat_map(|(name, record)| dense_columns(name, record))
.collect();
assert!(
offenders.is_empty(),
"a retained projection holds a dense identity: {offenders:?}"
);
}
pub fn declared_columns(name: &str) -> Vec<String> {
let declared = declared_records();
let held = declared
.get(name)
.unwrap_or_else(|| panic!("no production source declares {name}"));
held.iter()
.flat_map(|record| &record.columns)
.map(|(column, _)| column.clone())
.collect()
}
fn assert_roots_are_declared_where_modelled(declared: &BTreeMap<String, Vec<DeclaredRecord>>) {
for (path, name) in RETAINED_PROJECTION_ROOTS {
let sites: Vec<&str> = declared
.get(*name)
.map(|held| held.iter().map(|record| record.path).collect())
.unwrap_or_default();
assert_eq!(
sites,
vec![*path],
"{name} is declared by exactly the source the retained model names"
);
}
}
fn declared_records() -> BTreeMap<String, Vec<DeclaredRecord>> {
let mut found: BTreeMap<String, Vec<DeclaredRecord>> = BTreeMap::new();
for entry in SOURCES {
for (name, record) in declared_in(entry.path) {
found.entry(name).or_default().push(record);
}
}
found
}
fn declared_in(path: &'static str) -> Vec<(String, DeclaredRecord)> {
let file = parsed(path);
declared_items(&file.items)
.into_iter()
.filter_map(|item| match item {
syn::Item::Struct(found) => Some((
found.ident.to_string(),
DeclaredRecord {
path,
columns: stated_columns(found.fields.iter()),
},
)),
syn::Item::Enum(found) => Some((
found.ident.to_string(),
DeclaredRecord {
path,
columns: stated_columns(
found.variants.iter().flat_map(|variant| &variant.fields),
),
},
)),
_ => None,
})
.collect()
}
fn stated_columns<'a>(fields: impl Iterator<Item = &'a syn::Field>) -> Vec<(String, String)> {
fields
.map(|field| {
let column = field
.ident
.as_ref()
.map_or_else(|| "a field".to_owned(), ToString::to_string);
(column, token_text(&field.ty))
})
.collect()
}
fn reached_from_roots(
declared: &BTreeMap<String, Vec<DeclaredRecord>>,
) -> Vec<(&str, &DeclaredRecord)> {
let mut visited: BTreeSet<String> = BTreeSet::new();
let mut pending: Vec<String> = RETAINED_PROJECTION_ROOTS
.iter()
.map(|(_, name)| (*name).to_owned())
.collect();
let mut reached: Vec<(&str, &DeclaredRecord)> = Vec::new();
while let Some(name) = pending.pop() {
let Some((declared_name, found)) = declared.get_key_value(&name) else {
continue;
};
if !visited.insert(name) {
continue;
}
reached.extend(found.iter().map(|record| (declared_name.as_str(), record)));
pending.extend(
found
.iter()
.flat_map(|record| &record.columns)
.flat_map(|(_, stated)| named_types(stated)),
);
}
reached
}
fn named_types(stated: &str) -> Vec<String> {
stated
.split(|character: char| !character.is_alphanumeric() && character != '_')
.filter(|token| !token.is_empty())
.map(ToOwned::to_owned)
.collect()
}
fn dense_columns(name: &str, record: &DeclaredRecord) -> Vec<String> {
record
.columns
.iter()
.filter(|(_, stated)| {
DENSE_IDENTITIES
.iter()
.any(|identity| stated.contains(identity))
})
.map(|(column, stated)| format!("{}::{name}::{column} holds {stated}", record.path))
.collect()
}
pub fn assert_tied_definitions_survive_remapping() {
let (_repository, base) = fixture::resolve_target(REVISION_CORPUS, CORPUS_LIBRARY);
let mut cache = GraphCache::new(projection_only(64));
let plain = assert_build_classifies(
&mut cache,
pair(&base),
Classification::retained(0, REVISION_FRAGMENTS),
"the untied report",
);
let tied = restated(&base, Restatement::Tied);
let doubled = assert_build_classifies(
&mut cache,
(&base.snapshot, &tied),
Classification::retained(REVISION_FRAGMENTS - 1, 1),
Restatement::Tied.label(),
);
assert_matches_direct(
doubled.graph(),
(&base.snapshot, &tied),
Restatement::Tied.label(),
);
assert_tied_pair_is_distinct(plain.graph(), doubled.graph(), 1, Restatement::Tied.label());
let probed = restated(&base, Restatement::TiedProbe);
let inserted = assert_build_classifies(
&mut cache,
(&base.snapshot, &probed),
Classification::retained(REVISION_FRAGMENTS - 1, 1),
Restatement::TiedProbe.label(),
);
assert_matches_direct(
inserted.graph(),
(&base.snapshot, &probed),
Restatement::TiedProbe.label(),
);
assert_tied_pair_is_distinct(
plain.graph(),
inserted.graph(),
2,
Restatement::TiedProbe.label(),
);
let subject = "the tied report after the earlier record is withdrawn";
let withdrawn = assert_build_classifies(
&mut cache,
(&base.snapshot, &tied),
Classification::retained(REVISION_FRAGMENTS - 1, 1),
subject,
);
assert_matches_direct(withdrawn.graph(), (&base.snapshot, &tied), subject);
assert_tied_pair_is_distinct(plain.graph(), withdrawn.graph(), 1, subject);
}
fn assert_tied_pair_is_distinct(
plain: &CodeGraph,
restated: &CodeGraph,
added: usize,
subject: &str,
) {
assert_eq!(
restated.nodes().len(),
plain.nodes().len() + added,
"{subject}: the restated report states its further definitions"
);
let held = named(plain, "first");
let twins = named(restated, "first");
assert_eq!(
(held.len(), twins.len()),
(1, 2),
"{subject}: one declaration is stated twice"
);
let (earlier, later) = (twins[0], twins[1]);
let (held, earlier, later) = (
node(plain, held[0], subject),
node(restated, earlier, subject),
node(restated, later, subject),
);
assert_ne!(
earlier.id(),
later.id(),
"{subject}: tied definitions take distinct identities"
);
for twin in [earlier, later] {
assert_eq!(
(twin.kind(), twin.location()),
(held.kind(), held.location()),
"{subject}: both tied nodes state the declaration they were copied from"
);
}
assert_eq!(
parent_of(restated, earlier.id(), subject),
parent_of(restated, later.id(), subject),
"{subject}: both tied nodes are contained by one parent"
);
assert_eq!(
(
candidate_edges_into(restated, earlier.id()),
candidate_edges_into(restated, later.id())
),
(candidate_edges_into(plain, held.id()), 0),
"{subject}: every stated candidate names the occurrence it was stated for"
);
}
fn named(graph: &CodeGraph, name: &str) -> Box<[GraphNodeId]> {
graph
.nodes()
.iter()
.filter(|node| node.name() == name)
.map(GraphNode::id)
.collect()
}
fn node<'graph>(graph: &'graph CodeGraph, id: GraphNodeId, subject: &str) -> &'graph GraphNode {
graph
.nodes()
.iter()
.find(|node| node.id() == id)
.unwrap_or_else(|| panic!("{subject}: the graph holds node {}", id.index()))
}
fn parent_of(graph: &CodeGraph, child: GraphNodeId, subject: &str) -> GraphNodeId {
graph
.containment()
.iter()
.find(|edge| edge.child() == child)
.map(|edge| edge.parent())
.unwrap_or_else(|| panic!("{subject}: node {} states a parent", child.index()))
}
fn candidate_edges_into(graph: &CodeGraph, target: GraphNodeId) -> usize {
graph
.edges()
.iter()
.filter(|edge| edge.target() == target)
.filter(|edge| matches!(edge.origin(), GraphEdgeOrigin::Reference { .. }))
.count()
}