mod reference;
mod table;
use crate::ast::{Node, SNode};
pub use harn_builtin_meta::{
MethodDescriptor, MethodEffect, MethodMigration, MethodReceiver, RemovedMethod,
};
pub use reference::{
render_reference, splice_reference, REFERENCE_BEGIN_MARKER, REFERENCE_END_MARKER,
};
pub use table::{BUILTIN_METHODS, REMOVED_METHODS};
pub fn method(receiver: MethodReceiver, name: &str) -> Option<&'static MethodDescriptor> {
BUILTIN_METHODS
.iter()
.find(|row| row.receiver == receiver && row.name() == name)
}
pub fn methods_of(receiver: MethodReceiver) -> impl Iterator<Item = &'static MethodDescriptor> {
BUILTIN_METHODS
.iter()
.filter(move |row| row.receiver == receiver)
}
pub fn method_names(receiver: MethodReceiver) -> Vec<&'static str> {
methods_of(receiver).map(MethodDescriptor::name).collect()
}
pub fn is_method_name(name: &str) -> bool {
BUILTIN_METHODS.iter().any(|row| row.name() == name)
}
pub fn removed_method(receiver: MethodReceiver, name: &str) -> Option<&'static RemovedMethod> {
REMOVED_METHODS
.iter()
.find(|row| row.receiver == receiver && row.name == name)
}
const HOST_VERBS: &[&str] = &["delete"];
pub fn removed_method_on_any_receiver(name: &str) -> Vec<&'static RemovedMethod> {
if is_method_name(name) || HOST_VERBS.contains(&name) {
return Vec::new();
}
REMOVED_METHODS
.iter()
.filter(|row| row.name == name)
.collect()
}
pub const COPY_COUNTERPARTS: &[(&str, &str)] = &[
("sort", "sorted"),
("sort_by", "sorted_by"),
("reverse", "reversed"),
];
pub fn copy_counterpart(mutating: &str) -> Option<&'static str> {
COPY_COUNTERPARTS
.iter()
.find(|(name, _)| *name == mutating)
.map(|(_, copy)| *copy)
}
pub fn in_place_counterpart(copy: &str) -> Option<&'static str> {
COPY_COUNTERPARTS
.iter()
.find(|(_, name)| *name == copy)
.map(|(mutating, _)| *mutating)
}
pub fn returns_generic(row: &MethodDescriptor) -> bool {
fn mentions(ty: &harn_builtin_meta::Ty, params: &[&str]) -> bool {
use harn_builtin_meta::Ty;
match ty {
Ty::Generic(name) | Ty::Named(name) => params.contains(name),
Ty::Optional(inner) => mentions(inner, params),
Ty::Apply(_, args) | Ty::Union(args) => args.iter().any(|t| mentions(t, params)),
Ty::Fn(args, ret) => args.iter().any(|t| mentions(t, params)) || mentions(ret, params),
Ty::Shape(fields) => fields.iter().any(|f| mentions(&f.ty, params)),
Ty::OpenShape(fields, rests) => {
fields.iter().any(|f| mentions(&f.ty, params))
|| rests.iter().any(|t| mentions(t, params))
}
Ty::SchemaOf(name) => params.contains(name),
Ty::Any | Ty::Never | Ty::LitInt(_) | Ty::LitString(_) => false,
}
}
mentions(&row.signature.returns, row.signature.type_params)
}
pub fn is_mutating_method_name(name: &str) -> bool {
BUILTIN_METHODS
.iter()
.any(|row| row.is_mutating() && row.name() == name)
}
pub fn is_pure_on_every_receiver(name: &str) -> bool {
is_method_name(name) && !is_mutating_method_name(name)
}
pub fn is_mutable_place(node: &SNode) -> bool {
place_root(node).is_some()
}
pub fn place_root(node: &SNode) -> Option<&str> {
match &node.node {
Node::Identifier(name) => Some(name),
Node::PropertyAccess { object, .. } | Node::SubscriptAccess { object, .. } => {
place_root(object)
}
_ => None,
}
}
pub fn mutating_call_place(node: &SNode) -> Option<&SNode> {
match &node.node {
Node::MethodCall { object, method, .. } if is_mutating_method_name(method) => {
is_mutable_place(object).then_some(&**object)
}
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn method_names_are_unique_per_receiver() {
for receiver in MethodReceiver::ALL {
let mut names = method_names(receiver);
let total = names.len();
names.sort_unstable();
names.dedup();
assert_eq!(names.len(), total, "{receiver:?} lists a method twice");
}
}
#[test]
fn removed_spellings_are_not_live_on_their_receiver() {
for row in REMOVED_METHODS {
assert!(
method(row.receiver, row.name).is_none(),
"`{}` is both removed and live on {:?}",
row.name,
row.receiver
);
if let MethodMigration::Rename(to) = row.migration {
assert!(
method(row.receiver, to).is_some(),
"`{}` on {:?} renames to `{to}`, which does not exist",
row.name,
row.receiver
);
}
}
}
#[test]
fn receiver_generics_are_declared_by_the_signatures_that_use_them() {
for row in BUILTIN_METHODS {
for param in row.receiver.receiver_type_params() {
let rendered = row.signature.to_string();
let used = rendered
.split(|c: char| !c.is_alphanumeric() && c != '_')
.skip(1)
.any(|token| token == *param);
if used {
assert!(
row.signature.is_type_param(param),
"{:?}.{} uses `{param}` without declaring it: {rendered}",
row.receiver,
row.name()
);
}
}
}
}
#[test]
fn copy_counterparts_pair_a_mutating_and_a_pure_list_method() {
for (mutating, copy) in COPY_COUNTERPARTS {
assert!(method(MethodReceiver::List, mutating).is_some_and(|row| row.is_mutating()));
assert!(method(MethodReceiver::List, copy).is_some_and(|row| !row.is_mutating()));
}
}
#[test]
fn retired_spellings_only_flag_unknown_receivers_when_unambiguous() {
assert_eq!(removed_method_on_any_receiver("to_lower").len(), 1);
assert_eq!(removed_method_on_any_receiver("removing").len(), 2);
assert!(removed_method_on_any_receiver("count").is_empty());
assert!(removed_method_on_any_receiver("reverse").is_empty());
assert!(removed_method_on_any_receiver("delete").is_empty());
}
}