use std::collections::BTreeSet;
use smol_str::SmolStr;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::{InMemorySchema, ParamDecl, PropertyDecl, PropertyType, in_memory::RelDecl};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[non_exhaustive]
pub enum LintSeverity {
Error,
Warning,
}
impl LintSeverity {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Error => "error",
Self::Warning => "warning",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[non_exhaustive]
pub struct SchemaLint {
pub code: &'static str,
pub severity: LintSeverity,
pub message: String,
}
impl SchemaLint {
fn error(code: &'static str, message: String) -> Self {
Self {
code,
severity: LintSeverity::Error,
message,
}
}
fn warning(code: &'static str, message: String) -> Self {
Self {
code,
severity: LintSeverity::Warning,
message,
}
}
}
impl core::fmt::Display for SchemaLint {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"{sev}[{code}]: {msg}",
sev = self.severity.as_str(),
code = self.code,
msg = self.message,
)
}
}
#[must_use]
pub fn lint(schema: &InMemorySchema) -> Vec<SchemaLint> {
let mut out = Vec::new();
lint_opaque_types(schema, &mut out);
lint_self_referential_rel_types(schema, &mut out);
lint_unreachable_labels(schema, &mut out);
out
}
fn lint_opaque_types(schema: &InMemorySchema, out: &mut Vec<SchemaLint>) {
for (label, props) in schema.labels_iter() {
for p in props {
check_opaque_property(label, p, out);
}
}
for rel in schema.rel_types() {
for p in &rel.properties {
check_opaque_relationship_property(rel, p, out);
}
}
for param in schema.parameters() {
check_opaque_parameter(param, out);
}
}
fn check_opaque_property(label: &SmolStr, p: &PropertyDecl, out: &mut Vec<SchemaLint>) {
if let Some(name) = opaque_type_name(&p.ty) {
out.push(SchemaLint::error(
"E3010",
format!(
"property `{label}.{pname}` has opaque type `{ty}`; \
v0 does not yet resolve `{ty}` structurally (spec 0002 §4, §20)",
pname = p.name,
ty = name,
),
));
}
}
fn check_opaque_relationship_property(rel: &RelDecl, p: &PropertyDecl, out: &mut Vec<SchemaLint>) {
if let Some(name) = opaque_type_name(&p.ty) {
out.push(SchemaLint::error(
"E3010",
format!(
"relationship `{rname}` property `{pname}` has opaque type `{ty}`; \
v0 does not yet resolve `{ty}` structurally (spec 0002 §4, §20)",
rname = rel.name,
pname = p.name,
ty = name,
),
));
}
}
fn check_opaque_parameter(param: &ParamDecl, out: &mut Vec<SchemaLint>) {
if let Some(name) = opaque_type_name(¶m.ty) {
out.push(SchemaLint::error(
"E3010",
format!(
"parameter `${pname}` has opaque type `{ty}`; \
v0 does not yet resolve `{ty}` structurally (spec 0002 §4, §20)",
pname = param.name,
ty = name,
),
));
}
}
fn opaque_type_name(ty: &PropertyType) -> Option<&str> {
match ty {
PropertyType::Opaque(n) => Some(n.as_str()),
PropertyType::List(inner) => opaque_type_name(inner),
_ => None,
}
}
fn lint_self_referential_rel_types(schema: &InMemorySchema, out: &mut Vec<SchemaLint>) {
for rel in schema.rel_types() {
let starts: BTreeSet<&SmolStr> = rel.start_labels.iter().collect();
let ends: BTreeSet<&SmolStr> = rel.end_labels.iter().collect();
let overlap: Vec<&&SmolStr> = starts.intersection(&ends).collect();
for label in overlap {
out.push(SchemaLint::error(
"E3011",
format!(
"relationship `{rname}` declares `{lname}` on both \
`start_labels` and `end_labels`; confirm the self-loop \
is intentional (spec 0002 §6)",
rname = rel.name,
lname = label,
),
));
}
}
}
fn lint_unreachable_labels(schema: &InMemorySchema, out: &mut Vec<SchemaLint>) {
let mut reached: BTreeSet<SmolStr> = BTreeSet::new();
for rel in schema.rel_types() {
for l in rel.start_labels.iter().chain(rel.end_labels.iter()) {
reached.insert(l.clone());
}
}
for label in schema.label_names() {
if !reached.contains(&label) {
out.push(SchemaLint::warning(
"W6010",
format!(
"label `{label}` is declared but not used by any \
relationship type; confirm this is intentional \
(spec 0002 §9)",
),
));
}
}
}
impl InMemorySchema {
pub(crate) fn labels_iter(&self) -> impl Iterator<Item = (&SmolStr, &Vec<PropertyDecl>)> {
self.labels.iter()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{InMemorySchema, ParamDecl, PropertyDecl, PropertyType, in_memory::RelDecl};
#[test]
fn clean_schema_has_no_lints() {
let s = InMemorySchema::builder()
.add_label(
SmolStr::new("Person"),
vec![PropertyDecl::new(
SmolStr::new("name"),
PropertyType::String,
true,
)],
)
.add_label(
SmolStr::new("Movie"),
vec![PropertyDecl::new(
SmolStr::new("title"),
PropertyType::String,
true,
)],
)
.add_rel_type(RelDecl {
name: SmolStr::new("ACTED_IN"),
start_labels: vec![SmolStr::new("Person")],
end_labels: vec![SmolStr::new("Movie")],
properties: vec![],
})
.build()
.expect("builds");
assert!(lint(&s).is_empty());
}
#[test]
fn opaque_property_type_flagged() {
let s = InMemorySchema::builder()
.add_label(
SmolStr::new("Event"),
vec![PropertyDecl::new(
SmolStr::new("at"),
PropertyType::Opaque(SmolStr::new("DURATION")),
false,
)],
)
.add_rel_type(RelDecl {
name: SmolStr::new("R"),
start_labels: vec![SmolStr::new("Event")],
end_labels: vec![SmolStr::new("Event")],
properties: vec![],
})
.build()
.expect("builds");
let issues = lint(&s);
let e3010: Vec<_> = issues.iter().filter(|i| i.code == "E3010").collect();
assert_eq!(e3010.len(), 1);
assert!(
e3010[0].message.contains("DURATION"),
"message: {}",
e3010[0].message
);
}
#[test]
fn opaque_parameter_type_flagged() {
let s = InMemorySchema::builder()
.add_parameter(ParamDecl {
name: SmolStr::new("loc"),
ty: PropertyType::Opaque(SmolStr::new("POINT")),
default: None,
})
.build()
.expect("builds");
let issues = lint(&s);
assert!(issues.iter().any(|i| i.code == "E3010"));
}
#[test]
fn self_loop_rel_type_flagged() {
let s = InMemorySchema::builder()
.add_label(SmolStr::new("Team"), vec![])
.add_rel_type(RelDecl {
name: SmolStr::new("REPORTS_TO"),
start_labels: vec![SmolStr::new("Team")],
end_labels: vec![SmolStr::new("Team")],
properties: vec![],
})
.build()
.expect("builds");
let e3011: Vec<_> = lint(&s).into_iter().filter(|i| i.code == "E3011").collect();
assert_eq!(e3011.len(), 1);
assert!(e3011[0].message.contains("REPORTS_TO"));
}
#[test]
fn unreachable_label_flagged() {
let s = InMemorySchema::builder()
.add_label(SmolStr::new("Connected"), vec![])
.add_label(SmolStr::new("Orphan"), vec![])
.add_rel_type(RelDecl {
name: SmolStr::new("R"),
start_labels: vec![SmolStr::new("Connected")],
end_labels: vec![SmolStr::new("Connected")],
properties: vec![],
})
.build()
.expect("builds");
let issues = lint(&s);
let w6010: Vec<_> = issues.iter().filter(|i| i.code == "W6010").collect();
assert_eq!(w6010.len(), 1);
assert!(w6010[0].message.contains("Orphan"));
}
#[test]
fn severity_classification() {
assert_eq!(LintSeverity::Error.as_str(), "error");
assert_eq!(LintSeverity::Warning.as_str(), "warning");
}
#[test]
fn lint_is_deterministic() {
let s = InMemorySchema::builder()
.add_label(SmolStr::new("A"), vec![])
.add_label(SmolStr::new("B"), vec![])
.add_label(SmolStr::new("C"), vec![])
.build()
.expect("builds");
assert_eq!(lint(&s), lint(&s));
}
}