feat: publish Dever core with project scaffolding and versioned AI skills
This commit is contained in:
1 parent
70fa73cdc2
commit
ac458ffa7f
960 files changed
+221319
-1163
No files matched your search
@@ -0,0 +1,188 @@
|
||||
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,
|
||||
))
|
||||
}
|
||||
Reference in new issue
Block a user