Skip to main content

apollo_compiler/
name.rs

1use crate::diagnostic::CliReport;
2use crate::diagnostic::ToCliReport;
3use crate::node::ExtensionId;
4use crate::parser::FileId;
5use crate::parser::LineColumn;
6use crate::parser::SourceMap;
7use crate::parser::SourceSpan;
8use crate::parser::TaggedFileId;
9use crate::Node;
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/September2025/#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/September2025/#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/September2025/#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/September2025/#NameStart>
283    const fn is_name_start(byte: u8) -> bool {
284        byte.is_ascii_alphabetic() || byte == b'_'
285    }
286
287    /// <https://spec.graphql.org/September2025/#NameContinue>
288    const fn is_name_continue(byte: u8) -> bool {
289        byte.is_ascii_alphanumeric() || byte == b'_'
290    }
291
292    /// Converts to a [`Node<Name>`] with the given extension ID,
293    /// keeping the source location of this name.
294    pub fn to_node(&self, extension_id: Option<ExtensionId>) -> Node<Name> {
295        let mut node = Node::new_opt_location(self.clone(), self.location());
296        if let Some(id) = extension_id {
297            node.set_extension_id(id);
298        }
299        node
300    }
301}
302
303impl Clone for Name {
304    fn clone(&self) -> Self {
305        if let Some(arc) = self.as_arc() {
306            let _ptr = Arc::into_raw(Arc::clone(&arc));
307            // Conceptually move ownership of this "new" pointer into the new clone
308            // However it’s a `*const` and we already have a `NonNull` with the same address in `self`
309        }
310        Self { ..*self }
311    }
312}
313
314impl Drop for Name {
315    fn drop(&mut self) {
316        if let Some(arc) = &mut self.as_arc() {
317            // SAFETY: neither the dropped `ManuallyDrop` nor `self.ptr` is used again
318            unsafe { ManuallyDrop::drop(arc) }
319        }
320    }
321}
322
323impl std::hash::Hash for Name {
324    #[inline]
325    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
326        self.as_str().hash(state) // location not included
327    }
328}
329
330impl std::ops::Deref for Name {
331    type Target = str;
332
333    #[inline]
334    fn deref(&self) -> &Self::Target {
335        self.as_str()
336    }
337}
338
339impl AsRef<str> for Name {
340    #[inline]
341    fn as_ref(&self) -> &str {
342        self.as_str()
343    }
344}
345
346impl std::borrow::Borrow<str> for Name {
347    fn borrow(&self) -> &str {
348        self.as_str()
349    }
350}
351
352impl std::fmt::Debug for Name {
353    #[inline]
354    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
355        self.as_str().fmt(f)
356    }
357}
358
359impl std::fmt::Display for Name {
360    #[inline]
361    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
362        self.as_str().fmt(f)
363    }
364}
365
366impl Eq for Name {}
367
368impl PartialEq for Name {
369    #[inline]
370    fn eq(&self, other: &Self) -> bool {
371        self.as_str() == other.as_str() // don’t compare location
372    }
373}
374
375impl Ord for Name {
376    #[inline]
377    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
378        self.as_str().cmp(other.as_str())
379    }
380}
381
382impl PartialOrd for Name {
383    #[inline]
384    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
385        Some(self.cmp(other))
386    }
387}
388
389impl std::borrow::Borrow<str> for Node<Name> {
390    fn borrow(&self) -> &str {
391        self.as_str()
392    }
393}
394
395impl PartialEq<str> for Node<Name> {
396    fn eq(&self, other: &str) -> bool {
397        self.as_str() == other
398    }
399}
400
401impl<T: AsRef<str>> PartialEq<T> for Node<Name> {
402    fn eq(&self, other: &T) -> bool {
403        self.as_str() == other.as_ref()
404    }
405}
406
407impl PartialEq<str> for Name {
408    #[inline]
409    fn eq(&self, other: &str) -> bool {
410        self.as_str() == other
411    }
412}
413
414impl PartialOrd<str> for Name {
415    #[inline]
416    fn partial_cmp(&self, other: &str) -> Option<std::cmp::Ordering> {
417        self.as_str().partial_cmp(other)
418    }
419}
420
421impl PartialEq<&'_ str> for Name {
422    #[inline]
423    fn eq(&self, other: &&'_ str) -> bool {
424        self.as_str() == *other
425    }
426}
427
428impl PartialOrd<&'_ str> for Name {
429    #[inline]
430    fn partial_cmp(&self, other: &&'_ str) -> Option<std::cmp::Ordering> {
431        self.as_str().partial_cmp(*other)
432    }
433}
434
435impl From<&'_ Self> for Name {
436    #[inline]
437    fn from(value: &'_ Self) -> Self {
438        value.clone()
439    }
440}
441
442impl From<Name> for Arc<str> {
443    fn from(value: Name) -> Self {
444        match value.to_cloned_arc() {
445            Some(arc) => arc,
446            None => value.as_str().into(),
447        }
448    }
449}
450
451impl TryFrom<Arc<str>> for Name {
452    type Error = InvalidNameError;
453
454    fn try_from(value: Arc<str>) -> Result<Self, Self::Error> {
455        Self::check_valid_syntax(&value)?;
456        Ok(Self::from_arc_unchecked(value))
457    }
458}
459
460impl serde::Serialize for Name {
461    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
462    where
463        S: serde::Serializer,
464    {
465        serializer.serialize_str(self.as_str())
466    }
467}
468
469impl<'de> serde::Deserialize<'de> for Name {
470    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
471    where
472        D: serde::Deserializer<'de>,
473    {
474        const EXPECTING: &str = "a string in GraphQL Name syntax";
475        struct Visitor;
476        impl serde::de::Visitor<'_> for Visitor {
477            type Value = Name;
478
479            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
480                formatter.write_str(EXPECTING)
481            }
482
483            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
484            where
485                E: serde::de::Error,
486            {
487                Name::new(v)
488                    .map_err(|_| E::invalid_value(serde::de::Unexpected::Str(v), &EXPECTING))
489            }
490        }
491        deserializer.deserialize_str(Visitor)
492    }
493}
494
495impl TryFrom<&str> for Name {
496    type Error = InvalidNameError;
497
498    fn try_from(value: &str) -> Result<Self, Self::Error> {
499        Self::new(value)
500    }
501}
502
503impl TryFrom<String> for Name {
504    type Error = InvalidNameError;
505
506    fn try_from(value: String) -> Result<Self, Self::Error> {
507        Self::new(&value)
508    }
509}
510
511impl TryFrom<&'_ String> for Name {
512    type Error = InvalidNameError;
513
514    fn try_from(value: &'_ String) -> Result<Self, Self::Error> {
515        Self::new(value)
516    }
517}
518
519impl AsRef<Name> for Name {
520    fn as_ref(&self) -> &Name {
521        self
522    }
523}
524
525impl ToCliReport for InvalidNameError {
526    fn location(&self) -> Option<SourceSpan> {
527        self.location
528    }
529    fn report(&self, report: &mut CliReport) {
530        report.with_label_opt(self.location, "cannot be parsed as a GraphQL Name");
531    }
532}
533
534impl fmt::Debug for InvalidNameError {
535    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
536        fmt::Display::fmt(self, f)
537    }
538}