Files
core/crates/dever-core/src/check/names.rs
T

189 lines
6.1 KiB
Rust

use crate::diagnostic::Diagnostic;
use crate::syntax::{
self, Declaration, InputKind, Name, Package, Pattern, StatementKind, TypeShape,
};
use super::Checked;
pub(super) fn check(package: &Package) -> Checked<()> {
for name in &package.name {
snake(name)?;
}
for declaration in &package.declarations {
match declaration {
Declaration::Api(binding) => {
snake(&binding.action)?;
for segment in &binding.target {
snake(segment)?;
}
}
Declaration::Rest(rest) => {
for segment in rest.model.iter().flatten() {
snake(segment)?;
}
}
Declaration::External(external) => {
for capability in &external.capabilities {
snake(capability)?;
}
}
Declaration::Type(ty) => {
camel(&ty.name)?;
if matches!(
ty.name.text.as_str(),
"Bool"
| "Int"
| "Decimal"
| "Float"
| "Text"
| "Id"
| "Bytes"
| "Secret"
| "Uuid"
| "DateTime"
| "Date"
| "Time"
| "Duration"
| "Json"
| "List"
| "Map"
| "MapEntry"
| "Stream"
| "AsyncStream"
| "Channel"
| "Task"
| "Group"
) {
return Err(Diagnostic::error(
"C002",
"type name is reserved by the language",
ty.name.span,
));
}
match &ty.shape {
TypeShape::Record(fields) => check_fields(fields)?,
TypeShape::Choice(variants) => {
for variant in variants {
camel(&variant.name)?;
check_fields(&variant.payload)?;
}
}
}
}
Declaration::Function(function) => {
for segment in function.name.text.split('.') {
snake(&Name {
text: segment.into(),
span: function.name.span,
})?;
}
for input in &function.inputs {
snake(&input.name)?;
match &input.kind {
InputKind::Value(Pattern::Variant { bindings, .. }) => {
for binding in bindings {
if binding.text != "_" {
snake(binding)?;
}
}
}
InputKind::Handler(signature) => {
check_fields(&signature.inputs)?;
check_fields(&signature.outputs)?;
}
_ => {}
}
}
check_fields(&function.outputs)?;
for statement in &function.body {
if let StatementKind::Assign { target, .. } = &statement.kind {
for name in target {
snake(name)?;
}
}
}
}
Declaration::Database(binding) => snake(&binding.name)?,
Declaration::Schedule(schedule) => snake(&schedule.target)?,
Declaration::ModelIndex(index) => {
for field in &index.fields {
snake(field)?;
}
}
Declaration::Relation(relation) => {
snake(&relation.name)?;
for segment in &relation.field {
snake(segment)?;
}
}
Declaration::Seed(seed) => {
for row in &seed.rows {
for field in row {
snake(&field.name)?;
}
}
}
Declaration::Migration(migration) => {
snake(&migration.name)?;
for operation in &migration.operations {
match operation {
syntax::MigrationOperation::Drop(field) => snake(field)?,
syntax::MigrationOperation::Sql(_) => {}
}
}
}
Declaration::ModelSql(sql) => {
snake(&sql.name)?;
check_fields(&sql.inputs)?;
check_fields(&sql.outputs)?;
}
}
}
Ok(())
}
fn check_fields(fields: &[syntax::Field]) -> Checked<()> {
for field in fields {
snake(&field.name)?;
}
Ok(())
}
fn snake(name: &Name) -> Checked<()> {
if name
.text
.starts_with(|character: char| character.is_ascii_lowercase())
&& name.text.split('_').all(|word| {
!word.is_empty()
&& word
.chars()
.all(|character| character.is_ascii_lowercase() || character.is_ascii_digit())
})
{
return Ok(());
}
Err(Diagnostic::error(
"C010",
"name must use ASCII lower_snake_case",
name.span,
))
}
fn camel(name: &Name) -> Checked<()> {
if name
.text
.starts_with(|character: char| character.is_ascii_uppercase())
&& name
.text
.chars()
.all(|character| character.is_ascii_alphanumeric())
{
return Ok(());
}
Err(Diagnostic::error(
"C010",
"type and variant names must use ASCII UpperCamelCase",
name.span,
))
}