apollo_compiler/
name.rs

1use crate::diagnostic::CliReport;
2use crate::diagnostic::ToCliReport;
3use crate::parser::FileId;
4use crate::parser::LineColumn;
5use crate::parser::SourceMap;
6use crate::parser::SourceSpan;
7use crate::parser::TaggedFileId;
8use crate::schema::ComponentName;
9use crate::schema::ComponentOrigin;
10use rowan::TextRange;
11use std::fmt;
12use std::marker::PhantomData;
13use std::mem::size_of;
14use std::mem::ManuallyDrop;
15use std::ops::Range;
16use std::ptr::NonNull;
17use std::sync::Arc;
18
19/// Create a [`Name`] from a string literal or identifier, checked for validity at compile time.
20///
21/// A `Name` created this way does not own allocated heap memory or a reference counter,
22/// so cloning it is extremely cheap.
23///
24/// # Examples
25///
26/// ```
27/// use apollo_compiler::name;
28///
29/// assert_eq!(name!("Query").as_str(), "Query");
30/// assert_eq!(name!(Query).as_str(), "Query");
31/// ```
32///
33/// ```compile_fail
34/// # use apollo_compiler::name;
35/// // error[E0080]: evaluation of constant value failed
36/// // assertion failed: ::apollo_compiler::ast::Name::valid_syntax(\"è_é\")
37/// let invalid = name!("è_é");
38/// ```
39#[macro_export]
40macro_rules! name {
41    ($value: ident) => {
42        $crate::name!(stringify!($value))
43    };
44    ($value: expr) => {{
45        const _: () = { assert!($crate::Name::is_valid_syntax($value)) };
46        $crate::Name::new_static_unchecked(&$value)
47    }};
48}
49
50/// A GraphQL [_Name_](https://spec.graphql.org/draft/#Name) identifier
51///
52/// Like [`Node`][crate::Node], this string type has cheap `Clone`
53/// and carries an optional source location.
54///
55/// Internally, the string value is either an atomically-reference counted `Arc<str>`
56/// or a `&'static str` borrow that lives until the end of the program.
57//
58// Fields: equivalent to `(UnpackedRepr, Option<SourceSpan>)` but more compact
59pub struct Name {
60    /// Data pointer of either `Arc<str>::into_raw` (if `tagged_file_id.tag() == TAG_ARC`)
61    /// or `&'static str` (if `TAG_STATIC`)
62    ptr: NonNull<u8>,
63    len: u32,
64    start_offset: u32,            // zero if we don’t have a location
65    tagged_file_id: TaggedFileId, // `.file_id() == FileId::NONE` means we don’t have a location
66    phantom: PhantomData<UnpackedRepr>,
67}
68
69#[allow(dead_code)] // only used in PhantomData and static asserts
70enum UnpackedRepr {
71    Heap(Arc<str>),
72    Static(&'static str),
73}
74
75/// Tried to create a [`Name`] from a string that is not in valid
76/// [GraphQL name](https://spec.graphql.org/draft/#sec-Names) syntax.
77#[derive(Clone, Eq, PartialEq, thiserror::Error)]
78#[error("`{name}` is not a valid GraphQL name")]
79pub struct InvalidNameError {
80    pub name: String,
81    pub location: Option<SourceSpan>,
82}
83
84const TAG_ARC: bool = true;
85const TAG_STATIC: bool = false;
86
87const _: () = {
88    // 20 "useful" bytes on 32-bit targets like wasm,
89    // but still padded to 24 for alignment of u64 file ID:
90    assert!(size_of::<Name>() == 24);
91    assert!(size_of::<Name>() == size_of::<Option<Name>>());
92
93    // The `unsafe impl`s below are sound since `(tag, ptr, len)` represents `UnpackedRepr`
94    const fn assert_send_and_sync<T: Send + Sync>() {}
95    assert_send_and_sync::<(UnpackedRepr, u32, TaggedFileId)>();
96};
97
98unsafe impl Send for Name {}
99
100unsafe impl Sync for Name {}
101
102impl Name {
103    /// Create a new `Name`
104    pub fn new(value: &str) -> Result<Self, InvalidNameError> {
105        Self::check_valid_syntax(value)?;
106        Ok(Self::new_unchecked(value))
107    }
108
109    /// Create a new `Name` from a string with static lifetime
110    pub fn new_static(value: &'static str) -> Result<Self, InvalidNameError> {
111        Self::check_valid_syntax(value)?;
112        Ok(Self::new_static_unchecked(value))
113    }
114
115    /// Create a new `Name` without [validity checking][Self::is_valid_syntax].
116    ///
117    /// Constructing an invalid name may cause invalid document serialization
118    /// but not memory-safety issues.
119    pub fn new_unchecked(value: &str) -> Self {
120        Self::from_arc_unchecked(value.into())
121    }
122
123    /// Create a new `Name` from an `Arc`, without [validity checking][Self::is_valid_syntax].
124    ///
125    /// Constructing an invalid name may cause invalid document serialization
126    /// but not memory-safety issues.
127    pub fn from_arc_unchecked(arc: Arc<str>) -> Self {
128        let len = Self::new_len(&arc);
129        let ptr = Arc::into_raw(arc).cast_mut().cast();
130        // SAFETY: Arc always is non-null
131        let ptr = unsafe { NonNull::new_unchecked(ptr) };
132        Self {
133            ptr,
134            len,
135            start_offset: 0,
136            tagged_file_id: TaggedFileId::pack(TAG_ARC, FileId::NONE),
137            phantom: PhantomData,
138        }
139    }
140
141    /// Create a new `Name` from a string with static lifetime,
142    /// without [validity checking][Self::is_valid_syntax].
143    ///
144    /// Constructing an invalid name may cause invalid document serialization
145    /// but not memory-safety issues.
146    pub const fn new_static_unchecked(value: &'static str) -> Self {
147        let ptr = value.as_ptr().cast_mut();
148        // SAFETY: `&'static str` is always non-null
149        let ptr = unsafe { NonNull::new_unchecked(ptr) };
150        Self {
151            ptr,
152            len: Self::new_len(value),
153            start_offset: 0,
154            tagged_file_id: TaggedFileId::pack(TAG_STATIC, FileId::NONE),
155            phantom: PhantomData,
156        }
157    }
158
159    /// Modifies the given name to add its location in a parsed source file
160    pub fn with_location(mut self, location: SourceSpan) -> Self {
161        debug_assert_eq!(location.text_range.len(), self.len.into());
162        self.start_offset = location.text_range.start().into();
163        self.tagged_file_id = TaggedFileId::pack(self.tagged_file_id.tag(), location.file_id);
164        self
165    }
166
167    const fn new_len(value: &str) -> u32 {
168        let len = value.len();
169        if len >= (u32::MAX as usize) {
170            panic!("Name length overflows 4 GiB")
171        }
172        len as _
173    }
174
175    /// If this node was parsed from a source file, returns the file ID and source span
176    /// (start and end byte offsets) within that file.
177    pub fn location(&self) -> Option<SourceSpan> {
178        let file_id = self.tagged_file_id.file_id();
179        if file_id != FileId::NONE {
180            Some(SourceSpan {
181                file_id,
182                text_range: TextRange::at(self.start_offset.into(), self.len.into()),
183            })
184        } else {
185            None
186        }
187    }
188
189    /// If this string contains a location, convert it to line and column numbers
190    pub fn line_column_range(&self, sources: &SourceMap) -> Option<Range<LineColumn>> {
191        self.location()?.line_column_range(sources)
192    }
193
194    #[allow(clippy::len_without_is_empty)] // GraphQL Name is never empty
195    #[inline]
196    pub fn len(&self) -> usize {
197        self.len as _
198    }
199
200    #[inline]
201    pub fn as_str(&self) -> &str {
202        let slice = NonNull::slice_from_raw_parts(self.ptr, self.len());
203        // SAFETY: all constructors set `self.ptr` and `self.len` from valid UTF-8,
204        // and we return a lifetime tied to `self`.
205        unsafe { std::str::from_utf8_unchecked(slice.as_ref()) }
206    }
207
208    /// If this `Name` was created with [`new_static`][Self::new_static]
209    /// or the [`name!`][crate::name!] macro, return the string with `'static` lifetime.
210    ///
211    /// Returns `Some` if and only if [`to_cloned_arc`][Self::to_cloned_arc] returns `None`.
212    pub fn as_static_str(&self) -> Option<&'static str> {
213        if self.tagged_file_id.tag() == TAG_STATIC {
214            let raw_slice = NonNull::slice_from_raw_parts(self.ptr, self.len());
215            // SAFETY: the tag indicates `self.ptr` came from `Self::ptr_and_tag_from_static`,
216            // so it has the static lifetime and points to valid UTF-8 of the correct length.
217            Some(unsafe { std::str::from_utf8_unchecked(raw_slice.as_ref()) })
218        } else {
219            None
220        }
221    }
222
223    fn as_arc(&self) -> Option<ManuallyDrop<Arc<str>>> {
224        if self.tagged_file_id.tag() == TAG_ARC {
225            let raw_slice = NonNull::slice_from_raw_parts(self.ptr, self.len())
226                .as_ptr()
227                .cast_const();
228
229            // SAFETY:
230            //
231            // * The tag indicates `self.ptr` came from `Arc::into_raw` in `ptr_and_tag_with_arc`
232            // * `Arc::from_raw` normally moves ownership away from the raw pointer,
233            //   `ManuallyDrop` counteracts that
234            Some(ManuallyDrop::new(unsafe {
235                Arc::from_raw(raw_slice as *const str)
236            }))
237        } else {
238            None
239        }
240    }
241
242    /// If this `Name` contains an `Arc<str>`, return a clone of it (reference count increment)
243    ///
244    /// Returns `Some` if and only if [`as_static_str`][Self::as_static_str] returns `None`.
245    pub fn to_cloned_arc(&self) -> Option<Arc<str>> {
246        self.as_arc()
247            .map(|manually_drop| Arc::clone(&manually_drop))
248    }
249
250    /// Returns whether the given string is a valid
251    /// GraphQL [_Name_](https://spec.graphql.org/October2021/#Name).
252    pub const fn is_valid_syntax(value: &str) -> bool {
253        let bytes = value.as_bytes();
254        let Some(&first) = bytes.first() else {
255            return false;
256        };
257        if !Self::is_name_start(first) {
258            return false;
259        }
260        // TODO: iterator when available in const
261        let mut i = 1;
262        while i < bytes.len() {
263            if !Self::is_name_continue(bytes[i]) {
264                return false;
265            }
266            i += 1
267        }
268        true
269    }
270
271    fn check_valid_syntax(value: &str) -> Result<(), InvalidNameError> {
272        if Self::is_valid_syntax(value) {
273            Ok(())
274        } else {
275            Err(InvalidNameError {
276                name: value.to_owned(),
277                location: None,
278            })
279        }
280    }
281
282    /// <https://spec.graphql.org/October2021/#NameStart>
283    const fn is_name_start(byte: u8) -> bool {
284        byte.is_ascii_alphabetic() || byte == b'_'
285    }
286
287    /// <https://spec.graphql.org/October2021/#NameContinue>
288    const fn is_name_continue(byte: u8) -> bool {
289        byte.is_ascii_alphanumeric() || byte == b'_'
290    }
291
292    pub fn to_component(&self, origin: ComponentOrigin) -> ComponentName {
293        ComponentName {
294            origin,
295            name: self.clone(),
296        }
297    }
298}
299
300impl Clone for Name {
301    fn clone(&self) -> Self {
302        if let Some(arc) = self.as_arc() {
303            let _ptr = Arc::into_raw(Arc::clone(&arc));
304            // Conceptually move ownership of this "new" pointer into the new clone
305            // However it’s a `*const` and we already have a `NonNull` with the same address in `self`
306        }
307        Self { ..*self }
308    }
309}
310
311impl Drop for Name {
312    fn drop(&mut self) {
313        if let Some(arc) = &mut self.as_arc() {
314            // SAFETY: neither the dropped `ManuallyDrop` nor `self.ptr` is used again
315            unsafe { ManuallyDrop::drop(arc) }
316        }
317    }
318}
319
320impl std::hash::Hash for Name {
321    #[inline]
322    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
323        self.as_str().hash(state) // location not included
324    }
325}
326
327impl std::ops::Deref for Name {
328    type Target = str;
329
330    #[inline]
331    fn deref(&self) -> &Self::Target {
332        self.as_str()
333    }
334}
335
336impl AsRef<str> for Name {
337    #[inline]
338    fn as_ref(&self) -> &str {
339        self.as_str()
340    }
341}
342
343impl std::borrow::Borrow<str> for Name {
344    fn borrow(&self) -> &str {
345        self.as_str()
346    }
347}
348
349impl std::fmt::Debug for Name {
350    #[inline]
351    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
352        self.as_str().fmt(f)
353    }
354}
355
356impl std::fmt::Display for Name {
357    #[inline]
358    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
359        self.as_str().fmt(f)
360    }
361}
362
363impl Eq for Name {}
364
365impl PartialEq for Name {
366    #[inline]
367    fn eq(&self, other: &Self) -> bool {
368        self.as_str() == other.as_str() // don’t compare location
369    }
370}
371
372impl Ord for Name {
373    #[inline]
374    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
375        self.as_str().cmp(other.as_str())
376    }
377}
378
379impl PartialOrd for Name {
380    #[inline]
381    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
382        Some(self.cmp(other))
383    }
384}
385
386impl PartialEq<str> for Name {
387    #[inline]
388    fn eq(&self, other: &str) -> bool {
389        self.as_str() == other
390    }
391}
392
393impl PartialOrd<str> for Name {
394    #[inline]
395    fn partial_cmp(&self, other: &str) -> Option<std::cmp::Ordering> {
396        self.as_str().partial_cmp(other)
397    }
398}
399
400impl PartialEq<&'_ str> for Name {
401    #[inline]
402    fn eq(&self, other: &&'_ str) -> bool {
403        self.as_str() == *other
404    }
405}
406
407impl PartialOrd<&'_ str> for Name {
408    #[inline]
409    fn partial_cmp(&self, other: &&'_ str) -> Option<std::cmp::Ordering> {
410        self.as_str().partial_cmp(*other)
411    }
412}
413
414impl From<&'_ Self> for Name {
415    #[inline]
416    fn from(value: &'_ Self) -> Self {
417        value.clone()
418    }
419}
420
421impl From<Name> for Arc<str> {
422    fn from(value: Name) -> Self {
423        match value.to_cloned_arc() {
424            Some(arc) => arc,
425            None => value.as_str().into(),
426        }
427    }
428}
429
430impl TryFrom<Arc<str>> for Name {
431    type Error = InvalidNameError;
432
433    fn try_from(value: Arc<str>) -> Result<Self, Self::Error> {
434        Self::check_valid_syntax(&value)?;
435        Ok(Self::from_arc_unchecked(value))
436    }
437}
438
439impl serde::Serialize for Name {
440    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
441    where
442        S: serde::Serializer,
443    {
444        serializer.serialize_str(self.as_str())
445    }
446}
447
448impl<'de> serde::Deserialize<'de> for Name {
449    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
450    where
451        D: serde::Deserializer<'de>,
452    {
453        const EXPECTING: &str = "a string in GraphQL Name syntax";
454        struct Visitor;
455        impl serde::de::Visitor<'_> for Visitor {
456            type Value = Name;
457
458            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
459                formatter.write_str(EXPECTING)
460            }
461
462            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
463            where
464                E: serde::de::Error,
465            {
466                Name::new(v)
467                    .map_err(|_| E::invalid_value(serde::de::Unexpected::Str(v), &EXPECTING))
468            }
469        }
470        deserializer.deserialize_str(Visitor)
471    }
472}
473
474impl TryFrom<&str> for Name {
475    type Error = InvalidNameError;
476
477    fn try_from(value: &str) -> Result<Self, Self::Error> {
478        Self::new(value)
479    }
480}
481
482impl TryFrom<String> for Name {
483    type Error = InvalidNameError;
484
485    fn try_from(value: String) -> Result<Self, Self::Error> {
486        Self::new(&value)
487    }
488}
489
490impl TryFrom<&'_ String> for Name {
491    type Error = InvalidNameError;
492
493    fn try_from(value: &'_ String) -> Result<Self, Self::Error> {
494        Self::new(value)
495    }
496}
497
498impl AsRef<Name> for Name {
499    fn as_ref(&self) -> &Name {
500        self
501    }
502}
503
504impl ToCliReport for InvalidNameError {
505    fn location(&self) -> Option<SourceSpan> {
506        self.location
507    }
508    fn report(&self, report: &mut CliReport) {
509        report.with_label_opt(self.location, "cannot be parsed as a GraphQL Name");
510    }
511}
512
513impl fmt::Debug for InvalidNameError {
514    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
515        fmt::Display::fmt(self, f)
516    }
517}