use std::collections::{BTreeMap, BTreeSet, HashSet};
use std::path::{Path, PathBuf};
use std::str::{self, FromStr};
use crate::compat::arg_shape::{ArgShape, satisfies_any};
use crate::compat::patch::{EvaluationFiles, Hunk, HunkLine, PatchedFile, SourceKind};
use crate::compat::preimage::{
PreimageMatch, classify_preimage, leading_added_run, postimage_lines, preimage_lines,
preimage_offset, tally_postimage, trailing_added_run,
};
use crate::compat::scope::PinScope;
use crate::config::FamilyDefaults;
use crate::model::names::{
Arity, FieldName, FunctionName, MacroName, Mfa, ModuleName, RecordName, TypeName,
};
use crate::model::snapshot::{
ArityMatch, Deprecation, FunArity, Module, Snapshot, Visibility, state,
};
use crate::model::spec_ast::SpecType;
use crate::model::spec_parser::parse_signature_return;
use crate::model::symbol::{RefOrigin, SymbolKind, SymbolRef};
use crate::model::verdict::{
ArtifactKind, ConflictMarker, HunkTally, Reason, SnapshotSide, SourceDelta, Verdict,
};
#[derive(Debug, Clone, Copy)]
pub(crate) struct AnalyzedInputs<'a> {
pub files: &'a [PatchedFile],
pub referenced: &'a [SymbolRef],
pub defined: &'a [SymbolRef],
pub unsupported_files: &'a [PathBuf],
pub call_args: &'a [(Mfa, Vec<ArgShape>)],
}
#[derive(Debug, Clone, Copy, Default)]
pub(crate) struct PinEvalOptions<'a> {
pub source_snapshot: Option<&'a Snapshot<state::Canonical>>,
pub pin_files: Option<&'a EvaluationFiles>,
pub scope: Option<&'a PinScope>,
pub family_defaults: Option<&'a FamilyDefaults>,
}
pub(crate) fn evaluate_pin(
inputs: AnalyzedInputs<'_>,
snapshot: &Snapshot<state::Canonical>,
options: PinEvalOptions<'_>,
) -> EvaluationResult {
let AnalyzedInputs {
files,
referenced,
defined,
unsupported_files,
call_args,
} = inputs;
let PinEvalOptions {
source_snapshot,
pin_files,
scope,
family_defaults,
} = options;
let mut reasons: Vec<Reason> = Vec::new();
check_syntactic_artifacts(files, &mut reasons);
let mut postimage = None;
if let Some(pf) = pin_files {
let tally = check_files_against_pin(files, pf, snapshot, &mut reasons);
if tally.considered > 0 {
postimage = Some(tally);
}
}
if let Some(fd) = family_defaults {
check_versioned_machine_data(files, snapshot, source_snapshot, fd, &mut reasons);
}
let defined_index: HashSet<&SymbolKind> = defined.iter().map(|d| &d.kind).collect();
let mut tracked_refs: Vec<SymbolRef> = Vec::new();
let mut context_missing: BTreeMap<ModuleName, usize> = BTreeMap::new();
let mut source_deltas: Vec<SourceDelta> = Vec::new();
for r in referenced {
if defined_index.contains(&r.kind) {
continue;
}
if r.origin == RefOrigin::Context {
tally_context_miss(r, snapshot, scope, &mut context_missing);
continue;
}
match &r.kind {
SymbolKind::Function { mfa } => {
if let Some(s) = scope
&& !s.contains_module(&mfa.module)
{
continue;
}
tracked_refs.push(r.clone());
analyze_function_reference(r, mfa, snapshot, source_snapshot, &mut reasons);
if let Some(src) = source_snapshot {
record_source_delta(mfa, snapshot, src, &mut source_deltas);
}
}
SymbolKind::FunctionAnyArity { module, function } => {
if let Some(s) = scope
&& !s.contains_module(module)
{
continue;
}
tracked_refs.push(r.clone());
analyze_any_arity_reference(r, module, function, snapshot, &mut reasons);
}
SymbolKind::Record { name } => {
if let Some(s) = scope
&& !s.contains_record(name)
{
continue;
}
tracked_refs.push(r.clone());
if !record_present(snapshot, name) {
reasons.push(Reason::MissingSymbol {
symbol: r.clone(),
first_seen_at_tag: None,
needs_pin_at_least: None,
suggested_replacement: None,
});
continue;
}
check_record_against_source(name, snapshot, source_snapshot, &mut reasons);
}
SymbolKind::Type {
module,
name,
arity,
} => {
if let Some(s) = scope
&& !s.contains_module(module)
{
continue;
}
tracked_refs.push(r.clone());
if !type_exported(snapshot, module, name, *arity) {
reasons.push(Reason::MissingType {
module: module.clone(),
name: name.clone(),
arity: *arity,
});
}
}
SymbolKind::Behaviour { .. }
| SymbolKind::Callback { .. }
| SymbolKind::Macro { .. } => {}
}
}
check_clause_mismatches(call_args, snapshot, scope, &mut reasons);
if let Some(src) = source_snapshot {
check_behaviour_conformance(snapshot, src, scope, &mut reasons);
check_return_shapes(referenced, snapshot, src, scope, &mut reasons);
}
for path in unsupported_files {
reasons.push(Reason::UnsupportedFileType { path: path.clone() });
}
EvaluationResult {
verdict: Verdict::from_reasons(reasons),
tracked_refs,
context_missing,
source_deltas,
postimage,
}
}
pub(crate) struct EvaluationResult {
pub verdict: Verdict,
pub tracked_refs: Vec<SymbolRef>,
pub context_missing: BTreeMap<ModuleName, usize>,
pub source_deltas: Vec<SourceDelta>,
pub postimage: Option<PostimageTally>,
}
fn tally_context_miss(
r: &SymbolRef,
snapshot: &Snapshot<state::Canonical>,
scope: Option<&PinScope>,
out: &mut BTreeMap<ModuleName, usize>,
) {
let module = match &r.kind {
SymbolKind::Function { mfa } => &mfa.module,
SymbolKind::FunctionAnyArity { module, .. } | SymbolKind::Type { module, .. } => module,
SymbolKind::Behaviour { .. }
| SymbolKind::Callback { .. }
| SymbolKind::Macro { .. }
| SymbolKind::Record { .. } => return,
};
if let Some(s) = scope
&& !s.contains_module(module)
{
return;
}
let resolved = match &r.kind {
SymbolKind::Function { mfa } => {
function_exported(snapshot, &mfa.module, &mfa.function, mfa.arity)
}
SymbolKind::FunctionAnyArity { module, function } => {
function_exported_any_arity(snapshot, module, function)
}
SymbolKind::Type {
module,
name,
arity,
} => type_exported(snapshot, module, name, *arity),
SymbolKind::Behaviour { .. }
| SymbolKind::Callback { .. }
| SymbolKind::Macro { .. }
| SymbolKind::Record { .. } => unreachable!("filtered above"),
};
if !resolved {
*out.entry(module.clone()).or_insert(0) += 1;
}
}
fn function_exported(
snapshot: &Snapshot<state::Canonical>,
module: &ModuleName,
function: &FunctionName,
arity: Arity,
) -> bool {
snapshot.lookup_export(module, function, arity)
}
fn function_exported_any_arity(
snapshot: &Snapshot<state::Canonical>,
module: &ModuleName,
function: &FunctionName,
) -> bool {
snapshot
.module_named(module)
.is_some_and(|m| m.exports.iter().any(|fa| &fa.name == function))
}
fn analyze_any_arity_reference(
r: &SymbolRef,
module: &ModuleName,
function: &FunctionName,
snapshot: &Snapshot<state::Canonical>,
reasons: &mut Vec<Reason>,
) {
let Some(m) = snapshot.module_named(module) else {
reasons.push(Reason::MissingSymbol {
symbol: r.clone(),
first_seen_at_tag: None,
needs_pin_at_least: None,
suggested_replacement: None,
});
return;
};
if m.visibility == Visibility::Hidden {
reasons.push(Reason::NowHidden {
module: module.clone(),
});
return;
}
if m.exports.iter().any(|fa| &fa.name == function) {
return;
}
reasons.push(Reason::MissingSymbol {
symbol: r.clone(),
first_seen_at_tag: None,
needs_pin_at_least: None,
suggested_replacement: None,
});
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub(crate) struct PostimageTally {
pub applied: usize,
pub considered: usize,
pub ambiguous: usize,
pub low_confidence: usize,
pub per_file: BTreeMap<PathBuf, HunkTally>,
}
fn record_source_delta(
mfa: &Mfa,
target: &Snapshot<state::Canonical>,
source: &Snapshot<state::Canonical>,
out: &mut Vec<SourceDelta>,
) {
let already = out
.iter()
.any(|d| d.module == mfa.module && d.function == mfa.function && d.arity == mfa.arity);
if already {
return;
}
let Some(target_spec) = find_spec(target, &mfa.module, &mfa.function, mfa.arity) else {
return;
};
let Some(source_spec) = find_spec(source, &mfa.module, &mfa.function, mfa.arity) else {
return;
};
if target_spec == source_spec {
return;
}
out.push(SourceDelta {
module: mfa.module.clone(),
function: mfa.function.clone(),
arity: mfa.arity,
source_spec: source_spec.to_string(),
target_spec: target_spec.to_string(),
});
}
fn check_clause_mismatches(
call_args: &[(Mfa, Vec<ArgShape>)],
snapshot: &Snapshot<state::Canonical>,
scope: Option<&PinScope>,
reasons: &mut Vec<Reason>,
) {
let signature_already: HashSet<(ModuleName, FunctionName, Arity)> = reasons
.iter()
.filter_map(|r| match r {
Reason::SignatureChanged {
module,
function,
arity,
..
} => Some((module.clone(), function.clone(), *arity)),
_ => None,
})
.collect();
let mut grouped: BTreeMap<(ModuleName, FunctionName, Arity), Vec<&Vec<ArgShape>>> =
BTreeMap::new();
for (mfa, args) in call_args {
if let Some(s) = scope
&& !s.contains_module(&mfa.module)
{
continue;
}
grouped
.entry((mfa.module.clone(), mfa.function.clone(), mfa.arity))
.or_default()
.push(args);
}
for ((module_name, function_name, arity), call_groups) in grouped {
if signature_already.contains(&(module_name.clone(), function_name.clone(), arity)) {
continue;
}
let Some(module) = snapshot.module_named(&module_name) else {
continue;
};
let fun_arity = FunArity {
name: function_name.clone(),
arity,
};
let Some(clauses) = module.clause_heads.get(&fun_arity) else {
continue;
};
let Some(failing) = call_groups
.into_iter()
.find(|args| !satisfies_any(args, clauses))
else {
continue;
};
reasons.push(Reason::ClauseMismatch {
module: module_name,
function: function_name,
arity,
call_args: failing.clone(),
pin_clauses: clauses.clone(),
});
}
}
fn file_to_module_name(path: &Path) -> Option<ModuleName> {
let basename = path.file_name()?.to_str()?;
let stem = basename.strip_suffix(".erl")?;
ModuleName::from_str(stem).ok()
}
fn touched_module_set(files: &[PatchedFile]) -> BTreeSet<ModuleName> {
let mut set = BTreeSet::new();
for f in files {
let Some(path) = f.primary_path() else {
continue;
};
let ext = path.extension().and_then(|s| s.to_str());
if !matches!(ext, Some("erl") | Some("hrl")) {
continue;
}
let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
continue;
};
if let Ok(m) = ModuleName::new(stem) {
set.insert(m);
}
}
set
}
fn recorded_wire_macros<'a>(
snapshot: &'a Snapshot<state::Canonical>,
module: &ModuleName,
) -> BTreeSet<&'a str> {
snapshot
.module_named(module)
.map(|m| {
m.wire_constants
.iter()
.map(|wc| wc.macro_name.as_str())
.collect()
})
.unwrap_or_default()
}
fn check_versioned_machine_data(
files: &[PatchedFile],
target: &Snapshot<state::Canonical>,
source: Option<&Snapshot<state::Canonical>>,
defaults: &FamilyDefaults,
reasons: &mut Vec<Reason>,
) {
let touched = touched_module_set(files);
for impl_decl in &defaults.versioned_machine_impls {
let Ok(module) = ModuleName::new(&impl_decl.module) else {
continue;
};
if !touched.contains(&module) {
continue;
}
let on_target = target
.module_named(&module)
.and_then(|m| m.versioned_machine_version.as_ref())
.is_some();
let on_source = source
.and_then(|s| s.module_named(&module))
.and_then(|m| m.versioned_machine_version.as_ref())
.is_some();
let side = match (source.is_some(), on_source, on_target) {
(true, false, false) => Some(SnapshotSide::Both),
(true, false, true) => Some(SnapshotSide::Source),
(true, true, false) => Some(SnapshotSide::Target),
(true, true, true) => None,
(false, _, false) => Some(SnapshotSide::Target),
(false, _, true) => None,
};
if let Some(side) = side {
reasons.push(Reason::VersionedMachineSnapshotMissing { module, side });
}
}
for wc_decl in &defaults.wire_constants {
let Ok(module) = ModuleName::new(&wc_decl.module) else {
continue;
};
if !touched.contains(&module) {
continue;
}
let target_present = recorded_wire_macros(target, &module);
let missing_target: Vec<MacroName> = wc_decl
.macros
.iter()
.filter(|m| !target_present.contains(m.as_str()))
.filter_map(|m| MacroName::new(m).ok())
.collect();
let Some(src) = source else {
if !missing_target.is_empty() {
let mut macros = missing_target;
macros.sort();
reasons.push(Reason::WireConstantBindingsMissing {
module,
macros,
side: SnapshotSide::Target,
});
}
continue;
};
let source_present = recorded_wire_macros(src, &module);
let missing_source: Vec<MacroName> = wc_decl
.macros
.iter()
.filter(|m| !source_present.contains(m.as_str()))
.filter_map(|m| MacroName::new(m).ok())
.collect();
match (missing_source.is_empty(), missing_target.is_empty()) {
(true, true) => {}
(false, true) => {
let mut macros = missing_source;
macros.sort();
reasons.push(Reason::WireConstantBindingsMissing {
module,
macros,
side: SnapshotSide::Source,
});
}
(true, false) => {
let mut macros = missing_target;
macros.sort();
reasons.push(Reason::WireConstantBindingsMissing {
module,
macros,
side: SnapshotSide::Target,
});
}
(false, false) if missing_source == missing_target => {
let mut macros = missing_target;
macros.sort();
reasons.push(Reason::WireConstantBindingsMissing {
module,
macros,
side: SnapshotSide::Both,
});
}
(false, false) => {
let mut src_macros = missing_source;
src_macros.sort();
let mut tgt_macros = missing_target;
tgt_macros.sort();
reasons.push(Reason::WireConstantBindingsMissing {
module: module.clone(),
macros: src_macros,
side: SnapshotSide::Source,
});
reasons.push(Reason::WireConstantBindingsMissing {
module,
macros: tgt_macros,
side: SnapshotSide::Target,
});
}
}
}
}
fn check_syntactic_artifacts(files: &[PatchedFile], reasons: &mut Vec<Reason>) {
for file in files {
let Some(path) = file.primary_path() else {
continue;
};
for hunk in &file.hunks {
let mut new_line = hunk.new_start;
for line in &hunk.lines {
match line {
HunkLine::Context(_) => {
new_line += 1;
}
HunkLine::Added(text) => {
if let Some(marker) = ConflictMarker::detect(text) {
reasons.push(Reason::SyntacticArtifact {
path: path.to_path_buf(),
line: new_line,
artifact: ArtifactKind::ConflictMarker { marker },
});
}
new_line += 1;
}
HunkLine::Removed(_) => {}
}
}
}
}
}
fn check_files_against_pin(
files: &[PatchedFile],
pin_files: &EvaluationFiles,
snapshot: &Snapshot<state::Canonical>,
reasons: &mut Vec<Reason>,
) -> PostimageTally {
let mut tally = PostimageTally::default();
for file in files {
if file.language == SourceKind::CuttlefishSchema {
continue;
}
let Some(path) = file.primary_path() else {
continue;
};
let Some(slot) = pin_files.get(path) else {
continue;
};
let Some(bytes) = slot else {
if let Some(module) = file_to_module_name(path)
&& snapshot.module_named(&module).is_some()
{
reasons.push(Reason::ModuleRelocated {
module,
patch_path: path.to_path_buf(),
});
} else {
reasons.push(Reason::FileAbsent {
path: path.to_path_buf(),
});
}
continue;
};
let target_lines = match str::from_utf8(bytes) {
Ok(s) => s.lines().collect::<Vec<&str>>(),
Err(_) => continue,
};
for (idx, hunk) in file.hunks.iter().enumerate() {
let pre = classify_preimage(hunk, &target_lines);
tally_postimage(hunk, &pre, &target_lines, path, &mut tally);
match pre {
PreimageMatch::Exact => {}
PreimageMatch::Drifted { line_delta } => {
reasons.push(Reason::PreimageDrifted {
path: path.to_path_buf(),
hunk_index: idx,
line_delta,
});
}
PreimageMatch::Missing { excerpt } => {
reasons.push(Reason::PreimageMissing {
path: path.to_path_buf(),
hunk_index: idx,
preimage_excerpt: excerpt,
});
}
}
}
}
tally
}
pub fn classify_hunks_against_target(
path: &Path,
hunks: &[Hunk],
target_content: &str,
) -> Vec<Reason> {
let target_lines: Vec<&str> = target_content.lines().collect();
let mut reasons = Vec::new();
for (idx, hunk) in hunks.iter().enumerate() {
let collision = || Reason::PostimageCollision {
path: path.to_path_buf(),
hunk_index: idx,
};
match classify_preimage(hunk, &target_lines) {
PreimageMatch::Missing { excerpt } => reasons.push(Reason::PreimageMissing {
path: path.to_path_buf(),
hunk_index: idx,
preimage_excerpt: excerpt,
}),
PreimageMatch::Exact => {
if added_lines_collide(hunk, &target_lines) {
reasons.push(collision());
}
}
PreimageMatch::Drifted { line_delta } => {
if added_lines_collide(hunk, &target_lines) {
reasons.push(collision());
} else {
reasons.push(Reason::PreimageDrifted {
path: path.to_path_buf(),
hunk_index: idx,
line_delta,
});
}
}
}
}
reasons
}
fn added_lines_collide(hunk: &Hunk, target: &[&str]) -> bool {
let preimage: Vec<&str> = preimage_lines(hunk);
let postimage: Vec<&str> = postimage_lines(hunk);
if preimage.is_empty() {
return !target.is_empty() && target != postimage.as_slice();
}
let Some(found) = preimage_offset(&preimage, hunk.old_start, target) else {
return false;
};
let trailing = trailing_added_run(hunk);
if !trailing.is_empty() {
let tail = &target[(found + preimage.len()).min(target.len())..];
if !tail.is_empty() && tail != trailing.as_slice() {
return true;
}
}
let leading = leading_added_run(hunk);
if !leading.is_empty() && hunk.old_start <= 1 {
let head = &target[..found];
if !head.is_empty() && head != leading.as_slice() {
return true;
}
}
false
}
fn analyze_function_reference(
r: &SymbolRef,
mfa: &Mfa,
snapshot: &Snapshot<state::Canonical>,
source_snapshot: Option<&Snapshot<state::Canonical>>,
reasons: &mut Vec<Reason>,
) {
let Some(module) = snapshot.module_named(&mfa.module) else {
reasons.push(Reason::MissingSymbol {
symbol: r.clone(),
first_seen_at_tag: None,
needs_pin_at_least: None,
suggested_replacement: None,
});
return;
};
if module.visibility == Visibility::Hidden {
reasons.push(Reason::NowHidden {
module: mfa.module.clone(),
});
return;
}
if let Some(dep) = deprecation_of(module, &mfa.function, mfa.arity) {
let replacement = dep.replacement.as_ref().map(|rep| {
SymbolRef::function(Mfa::new(
mfa.module.clone(),
rep.function.clone(),
rep.arity,
))
});
reasons.push(Reason::DeprecatedUsage {
symbol: r.clone(),
since: dep.since.clone(),
replacement,
});
}
let target = FunArity {
name: mfa.function.clone(),
arity: mfa.arity,
};
if module.exports.binary_search(&target).is_err() {
let alt_arities = collect_alt_arities_in(module, &mfa.function);
if alt_arities.is_empty() {
reasons.push(Reason::MissingSymbol {
symbol: r.clone(),
first_seen_at_tag: None,
needs_pin_at_least: None,
suggested_replacement: None,
});
} else {
reasons.push(Reason::ArityChanged {
module: mfa.module.clone(),
function: mfa.function.clone(),
expected: mfa.arity,
found: alt_arities,
expected_available_at: None,
needs_pin_at_least: None,
});
}
return;
}
if let Some(src) = source_snapshot {
let target_spec = find_spec(snapshot, &mfa.module, &mfa.function, mfa.arity);
let source_spec = find_spec(src, &mfa.module, &mfa.function, mfa.arity);
if let (Some(t), Some(s)) = (target_spec, source_spec)
&& t != s
{
reasons.push(Reason::SignatureChanged {
module: mfa.module.clone(),
function: mfa.function.clone(),
arity: mfa.arity,
expected_spec: s.to_string(),
found_spec: t.to_string(),
});
}
}
}
fn check_behaviour_conformance(
snapshot: &Snapshot<state::Canonical>,
source_snapshot: &Snapshot<state::Canonical>,
scope: Option<&PinScope>,
reasons: &mut Vec<Reason>,
) {
for source_module in source_snapshot.modules() {
if source_module.callbacks.is_empty() && source_module.optional_callbacks.is_empty() {
continue;
}
let Some(pin_module) = snapshot.module_named(&source_module.name) else {
continue;
};
let implementers = snapshot.implementers_of(&source_module.name);
if implementers.is_empty() {
continue;
}
let optional_at_source: HashSet<FunArity> =
source_module.optional_callbacks.iter().cloned().collect();
let optional_at_pin: HashSet<FunArity> =
pin_module.optional_callbacks.iter().cloned().collect();
let source_by_arity: BTreeMap<FunArity, &str> = source_module
.callbacks
.iter()
.map(|c| {
(
FunArity {
name: c.name.clone(),
arity: c.arity,
},
c.signature.as_str(),
)
})
.collect();
let pin_by_arity: BTreeMap<FunArity, &str> = pin_module
.callbacks
.iter()
.map(|c| {
(
FunArity {
name: c.name.clone(),
arity: c.arity,
},
c.signature.as_str(),
)
})
.collect();
for (key, source_sig) in &source_by_arity {
if let Some(pin_sig) = pin_by_arity.get(key) {
if signatures_differ(source_sig, pin_sig) {
for impl_module in &implementers {
if !implementer_in_scope(impl_module, scope) {
continue;
}
reasons.push(Reason::BehaviourCallbackSignatureChanged {
behaviour: source_module.name.clone(),
callback: key.name.clone(),
arity: key.arity,
expected_after_patch: (*source_sig).to_owned(),
implementer: impl_module.name.clone(),
implementer_signature: (*pin_sig).to_owned(),
});
}
}
} else if !optional_at_source.contains(key) {
for impl_module in &implementers {
if !implementer_in_scope(impl_module, scope) {
continue;
}
if impl_module.exports.binary_search(key).is_err() {
reasons.push(Reason::BehaviourCallbackAdded {
behaviour: source_module.name.clone(),
callback: key.name.clone(),
arity: key.arity,
implementer: impl_module.name.clone(),
});
}
}
}
}
for key in pin_by_arity.keys() {
if !source_by_arity.contains_key(key) && !optional_at_pin.contains(key) {
for impl_module in &implementers {
if !implementer_in_scope(impl_module, scope) {
continue;
}
if impl_module.exports.binary_search(key).is_ok() {
reasons.push(Reason::BehaviourCallbackRemoved {
behaviour: source_module.name.clone(),
callback: key.name.clone(),
arity: key.arity,
implementer: impl_module.name.clone(),
});
}
}
}
}
}
}
fn signatures_differ(a: &str, b: &str) -> bool {
if a == b {
return false;
}
let ast_a = parse_signature_return(a);
let ast_b = parse_signature_return(b);
ast_a != ast_b
}
fn implementer_in_scope(module: &Module, scope: Option<&PinScope>) -> bool {
match scope {
Some(s) => s.contains_module(&module.name),
None => true,
}
}
fn check_return_shapes(
referenced: &[SymbolRef],
snapshot: &Snapshot<state::Canonical>,
source: &Snapshot<state::Canonical>,
scope: Option<&PinScope>,
reasons: &mut Vec<Reason>,
) {
let mut seen: HashSet<(ModuleName, FunctionName, Arity)> = HashSet::new();
for r in referenced {
if r.origin == RefOrigin::Context {
continue;
}
let SymbolKind::Function { mfa } = &r.kind else {
continue;
};
if let Some(s) = scope
&& !s.contains_module(&mfa.module)
{
continue;
}
let key = (mfa.module.clone(), mfa.function.clone(), mfa.arity);
if !seen.insert(key) {
continue;
}
let Some(pin_sig) = find_spec(snapshot, &mfa.module, &mfa.function, mfa.arity) else {
continue;
};
let Some(src_sig) = find_spec(source, &mfa.module, &mfa.function, mfa.arity) else {
continue;
};
if pin_sig == src_sig {
continue;
}
let pin_ast = parse_signature_return(pin_sig);
let src_ast = parse_signature_return(src_sig);
if matches!(pin_ast, SpecType::Unknown) || matches!(src_ast, SpecType::Unknown) {
continue;
}
if pin_ast.matches(&src_ast) {
continue;
}
reasons.push(Reason::ReturnShapeMismatch {
module: mfa.module.clone(),
function: mfa.function.clone(),
arity: mfa.arity,
source_signature: src_sig.to_owned(),
pin_signature: pin_sig.to_owned(),
});
}
}
fn check_record_against_source(
name: &RecordName,
snapshot: &Snapshot<state::Canonical>,
source_snapshot: Option<&Snapshot<state::Canonical>>,
reasons: &mut Vec<Reason>,
) {
let Some(src) = source_snapshot else { return };
let target_fields = find_record_fields(snapshot, name);
let source_fields = find_record_fields(src, name);
if let (Some(t), Some(s)) = (target_fields, source_fields)
&& t != s
{
reasons.push(Reason::RecordFieldsChanged {
record: name.clone(),
expected: s,
found: t,
});
}
}
fn find_spec<'a>(
snapshot: &'a Snapshot<state::Canonical>,
module: &ModuleName,
function: &FunctionName,
arity: Arity,
) -> Option<&'a str> {
let m = snapshot.module_named(module)?;
let idx = m
.specs
.binary_search_by(|s| s.name.cmp(function).then(s.arity.cmp(&arity)))
.ok()?;
Some(m.specs[idx].signature.as_str())
}
fn find_record_fields(
snapshot: &Snapshot<state::Canonical>,
name: &RecordName,
) -> Option<Vec<FieldName>> {
snapshot
.records()
.find(|r| &r.name == name)
.map(|r| r.fields.iter().map(|f| f.name.clone()).collect())
}
fn deprecation_of<'a>(
module: &'a Module,
function: &FunctionName,
arity: Arity,
) -> Option<&'a Deprecation> {
module.deprecations.iter().find(|d| {
if d.module_wide {
return true;
}
if let Some(f) = &d.function
&& f == function
{
return match d.arity_match {
ArityMatch::Any => true,
ArityMatch::Exact { arity: a } => a == arity,
};
}
false
})
}
fn record_present(snapshot: &Snapshot<state::Canonical>, name: &RecordName) -> bool {
snapshot.records().any(|r| &r.name == name)
}
fn type_exported(
snapshot: &Snapshot<state::Canonical>,
module: &ModuleName,
name: &TypeName,
arity: Arity,
) -> bool {
let Some(m) = snapshot.module_named(module) else {
return false;
};
m.export_types
.binary_search_by(|t| t.name.cmp(name).then(t.arity.cmp(&arity)))
.is_ok()
}
fn collect_alt_arities_in(module: &Module, function: &FunctionName) -> Vec<Arity> {
module
.exports
.iter()
.filter(|fa| &fa.name == function)
.map(|fa| fa.arity)
.collect()
}