Skip to main content

cratestack_parser/validate/
field_attributes.rs

1//! The one check every field of every field-bearing declaration goes
2//! through: `model`, `view`, `mixin`, `type` and the `auth` block (ADR 0019
3//! D5). The lists are in `super::field_attribute_tables`.
4//!
5//! In order:
6//!
7//! 1. `super::removed_attributes`: `@allow`, `@deny`, `@pb` and `@custom`
8//!    keep the specific explanation of what replaced them.
9//! 2. `super::rename_attributes`: a field's `@rename` in the one form the
10//!    migrator reads, where it is read, at most once. Its messages say why
11//!    a view, a type or a relation field cannot rename.
12//! 3. [`check_shape_hinted`] on each attribute, the check procedures,
13//!    queries and `@@` attributes go through: the name must be in the
14//!    kind's list, spelled exactly, with an argument list exactly when it
15//!    takes one, nothing after it, and no second attribute run into it.
16//!
17//! The per-kind placement checks that give their own message for one name
18//! (`@server_only` on a `type` or the `auth` block, `@computed` on a mixin,
19//! a view or the `auth` block, `@id` on a mixin) run before this at each
20//! call site, so their message wins; the tests in `tests_field_attribute_lists`
21//! pin that those refusals and the lists agree.
22
23use std::sync::OnceLock;
24
25use cratestack_core::Field;
26
27use super::attribute_shape::{Known, check_shape_hinted};
28use super::field_attribute_tables::{
29    AUTH_FIELD_ATTRIBUTES, MIXIN_FIELD_ATTRIBUTES, MODEL_FIELD_ATTRIBUTES, TYPE_FIELD_ATTRIBUTES,
30    VIEW_FIELD_ATTRIBUTES,
31};
32use super::removed_attributes::{
33    rejected_field_attribute_names, validate_removed_field_attributes,
34};
35use crate::diagnostics::SchemaError;
36
37/// A declaration kind whose fields carry attributes.
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum FieldHost {
40    Model,
41    View,
42    Mixin,
43    Type,
44    Auth,
45}
46
47impl FieldHost {
48    /// Every kind, in the order the language introduces them.
49    pub const ALL: [FieldHost; 5] = [
50        FieldHost::Model,
51        FieldHost::View,
52        FieldHost::Mixin,
53        FieldHost::Type,
54        FieldHost::Auth,
55    ];
56
57    /// How a diagnostic names the declaration (``field `x` on model `Y` ``).
58    pub fn label(self) -> &'static str {
59        match self {
60            FieldHost::Model => "model",
61            FieldHost::View => "view",
62            FieldHost::Mixin => "mixin",
63            FieldHost::Type => "type",
64            FieldHost::Auth => "auth block",
65        }
66    }
67
68    pub(super) fn table(self) -> &'static [Known] {
69        match self {
70            FieldHost::Model => MODEL_FIELD_ATTRIBUTES,
71            FieldHost::View => VIEW_FIELD_ATTRIBUTES,
72            FieldHost::Mixin => MIXIN_FIELD_ATTRIBUTES,
73            FieldHost::Type => TYPE_FIELD_ATTRIBUTES,
74            FieldHost::Auth => AUTH_FIELD_ATTRIBUTES,
75        }
76    }
77}
78
79/// The field attributes `host` accepts, sigil included (`@readonly`), in the
80/// order its list is written. The validator and the editor completions read
81/// the same list, so they cannot disagree.
82pub fn field_attribute_names(host: FieldHost) -> Vec<&'static str> {
83    host.table().iter().map(|(name, _)| *name).collect()
84}
85
86/// Every name a typo on a field might have meant: the names some kind
87/// accepts, and the removed ones whose explanation a typo should reach.
88pub(super) fn suggestion_pool() -> &'static [&'static str] {
89    static POOL: OnceLock<Vec<&'static str>> = OnceLock::new();
90    POOL.get_or_init(|| {
91        let mut pool: Vec<&'static str> = FieldHost::ALL
92            .iter()
93            .flat_map(|host| field_attribute_names(*host))
94            .chain(rejected_field_attribute_names())
95            .collect();
96        pool.sort_unstable();
97        pool.dedup();
98        pool
99    })
100}
101
102/// Checks every attribute of `field`, a field of the `host` declaration
103/// `owner_name`.
104pub(super) fn validate_field_attributes(
105    host: FieldHost,
106    owner_name: &str,
107    field: &Field,
108) -> Result<(), SchemaError> {
109    let kind = host.label();
110    validate_removed_field_attributes(kind, owner_name, field)?;
111    super::rename_attributes::validate_field_rename(kind, owner_name, field)?;
112    let owner = format!("field `{}` on {kind} `{owner_name}`", field.name);
113    let construct = format!("{kind} field");
114    for attribute in &field.attributes {
115        check_shape_hinted(
116            attribute,
117            host.table(),
118            suggestion_pool(),
119            &owner,
120            &construct,
121        )?;
122    }
123    Ok(())
124}