php_ast/ast/names.rs
1use std::borrow::Cow;
2
3#[cfg(feature = "serde")]
4use serde::Serialize;
5
6use crate::Span;
7
8use super::ArenaVec;
9
10/// A bare identifier — the kind that names a function, class, parameter,
11/// enum case, etc. Distinct from [`Name`], which represents possibly-qualified
12/// names.
13///
14/// The "error" state — produced during error recovery when no identifier was
15/// found in the source — is represented by an empty name, which cannot occur
16/// for a real PHP identifier (the lexer rejects empty matches).
17///
18/// Use [`Ident::new`] / [`Ident::error`] to construct, [`Ident::as_str`] to
19/// extract a real name, [`Ident::is_error`] to test for the error state.
20/// Serialises as `{"name": .., "span": ..}` for real names and `null` for the
21/// error state.
22#[derive(Clone, Copy)]
23pub struct Ident<'src> {
24 name: &'src str,
25 /// Source range of the identifier token; includes the `$` of a variable name.
26 pub span: Span,
27}
28
29impl<'src> Ident<'src> {
30 /// Construct an identifier from a non-empty name.
31 /// Empty input is rejected in debug builds — use [`Ident::error`] instead.
32 #[inline]
33 pub fn new(name: &'src str, span: Span) -> Self {
34 debug_assert!(!name.is_empty(), "Ident::new() called with empty name");
35 Self { name, span }
36 }
37
38 /// Placeholder for "no identifier was parsed" at `span`.
39 #[inline]
40 pub const fn error(span: Span) -> Self {
41 Self { name: "", span }
42 }
43
44 /// Returns `Some(s)` for a real identifier, `None` for the error state.
45 #[inline]
46 pub fn as_str(&self) -> Option<&'src str> {
47 if self.name.is_empty() {
48 None
49 } else {
50 Some(self.name)
51 }
52 }
53
54 /// Returns `true` if this identifier was synthesised during error recovery.
55 #[inline]
56 pub fn is_error(&self) -> bool {
57 self.name.is_empty()
58 }
59
60 /// Returns the inner string, or `"<error>"` for the error state.
61 /// Useful when constructing diagnostic messages.
62 #[inline]
63 pub fn or_error(&self) -> &'src str {
64 if self.name.is_empty() {
65 "<error>"
66 } else {
67 self.name
68 }
69 }
70}
71
72impl<'src> std::fmt::Debug for Ident<'src> {
73 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74 if self.name.is_empty() {
75 write!(f, "Ident::error({:?})", self.span)
76 } else {
77 write!(f, "Ident({:?}, {:?})", self.name, self.span)
78 }
79 }
80}
81
82impl<'src> std::fmt::Display for Ident<'src> {
83 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84 f.write_str(self.or_error())
85 }
86}
87
88#[cfg(feature = "serde")]
89impl<'src> serde::Serialize for Ident<'src> {
90 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
91 use serde::ser::SerializeStruct;
92 if self.name.is_empty() {
93 return s.serialize_none();
94 }
95 let mut st = s.serialize_struct("Ident", 2)?;
96 st.serialize_field("name", self.name)?;
97 st.serialize_field("span", &self.span)?;
98 st.end()
99 }
100}
101
102impl<'src> PartialEq<&str> for Ident<'src> {
103 fn eq(&self, other: &&str) -> bool {
104 !self.name.is_empty() && self.name == *other
105 }
106}
107
108/// A PHP name (identifier, qualified name, fully-qualified name, or relative name).
109///
110/// The `Simple` variant is the fast path for the common case (~95%) of single
111/// unqualified identifiers like `strlen`, `Foo`, `MyClass`. It avoids allocating
112/// an `ArenaVec` entirely.
113///
114/// The `Complex` variant handles qualified (`Foo\Bar`), fully-qualified (`\Foo\Bar`),
115/// and relative (`namespace\Foo`) names.
116///
117/// The `Error` variant is synthesised during error recovery when the parser
118/// expected a name but found none. It carries only a span so consumers can
119/// distinguish it from any user-written name.
120#[non_exhaustive]
121pub enum Name<'arena, 'src> {
122 /// Single unqualified identifier — no `ArenaVec` allocation.
123 /// `&'src str` instead of `Cow` since this is always a borrowed slice of the source.
124 Simple {
125 /// The identifier text.
126 value: &'src str,
127 /// Source range of the name.
128 span: Span,
129 },
130 /// Multi-part or prefixed name (`Foo\Bar`, `\Foo`, `namespace\Foo`).
131 Complex {
132 /// Name segments split on `\`.
133 parts: ArenaVec<'arena, &'src str>,
134 /// Qualification form.
135 kind: NameKind,
136 /// Source range of the name.
137 span: Span,
138 },
139 /// Synthesised during error recovery when no real name could be parsed.
140 /// Distinguishable from any user-written name; visitors and tools can
141 /// explicitly skip or flag these.
142 Error {
143 /// Source range where a name was expected.
144 span: Span,
145 },
146}
147
148impl<'arena, 'src> Name<'arena, 'src> {
149 /// Source range of the name.
150 #[inline]
151 pub fn span(&self) -> Span {
152 match self {
153 Self::Simple { span, .. } | Self::Complex { span, .. } | Self::Error { span } => *span,
154 }
155 }
156
157 /// Qualification form of the name.
158 #[inline]
159 pub fn kind(&self) -> NameKind {
160 match self {
161 Self::Simple { .. } => NameKind::Unqualified,
162 Self::Complex { kind, .. } => *kind,
163 Self::Error { .. } => NameKind::Error,
164 }
165 }
166
167 /// Returns the name as a borrowed slice of the source string.
168 ///
169 /// Unlike `to_string_repr`, this never allocates: it uses the stored
170 /// span to slice directly into `src`. The slice includes any leading `\`
171 /// for fully-qualified names, exactly as it appears in the source.
172 ///
173 /// Use this when you need a zero-copy `&'src str` and already have the
174 /// source buffer available (e.g. inside [`crate::visitor::ScopeWalker`]).
175 #[inline]
176 pub fn src_repr(&self, src: &'src str) -> &'src str {
177 match self {
178 Self::Simple { value, .. } => value,
179 Self::Complex { span, .. } => &src[span.start as usize..span.end as usize],
180 Self::Error { .. } => "",
181 }
182 }
183
184 /// Joins all parts with `\` and prepends `\` if fully qualified.
185 /// Returns `Cow::Borrowed` for simple names (zero allocation).
186 /// Returns an empty `Cow::Borrowed("")` for `Name::Error`.
187 #[inline]
188 pub fn to_string_repr(&self) -> Cow<'src, str> {
189 match self {
190 Self::Complex {
191 parts,
192 kind: NameKind::FullyQualified,
193 ..
194 } => Cow::Owned(format!("\\{}", parts.join("\\"))),
195 _ => self.join_parts(),
196 }
197 }
198
199 /// Joins all parts with `\` without any leading backslash.
200 /// Returns `Cow::Borrowed` for single-part names (zero allocation).
201 /// Returns an empty `Cow::Borrowed("")` for `Name::Error`.
202 ///
203 /// Prefer [`Name::join_parts_in`] when the source is at hand: it also
204 /// borrows multi-part names.
205 #[inline]
206 pub fn join_parts(&self) -> Cow<'src, str> {
207 match self {
208 Self::Simple { value, .. } => Cow::Borrowed(value),
209 Self::Complex { parts, .. } if parts.len() == 1 => Cow::Borrowed(parts[0]),
210 Self::Complex { parts, .. } => Cow::Owned(parts.join("\\")),
211 Self::Error { .. } => Cow::Borrowed(""),
212 }
213 }
214
215 /// Same result as [`Name::join_parts`], but borrows from `src` (the source
216 /// the name was parsed from) whenever the name is written there without
217 /// whitespace or comments, which avoids allocating for multi-part names.
218 ///
219 /// Falls back to joining for names that are not contiguous in the source,
220 /// such as `Foo \ Bar` (valid before PHP 8.0) or `A\B` built from the
221 /// group use `use A\{B}`.
222 pub fn join_parts_in(&self, src: &'src str) -> Cow<'src, str> {
223 let Self::Complex { parts, .. } = self else {
224 return self.join_parts();
225 };
226 if parts.len() == 1 {
227 return Cow::Borrowed(parts[0]);
228 }
229 match contiguous_in(parts, src) {
230 Some(joined) => Cow::Borrowed(joined),
231 None => Cow::Owned(parts.join("\\")),
232 }
233 }
234
235 /// Returns the parts as a slice.
236 /// For `Simple`, returns a single-element slice of the value.
237 #[inline]
238 pub fn parts_slice(&self) -> &[&'src str] {
239 match self {
240 Self::Simple { value, .. } => std::slice::from_ref(value),
241 Self::Complex { parts, .. } => parts,
242 Self::Error { .. } => &[],
243 }
244 }
245}
246
247/// The slice of `src` that reads `parts` joined with `\`, if the parts sit there back to back.
248fn contiguous_in<'src>(parts: &[&'src str], src: &'src str) -> Option<&'src str> {
249 let base = src.as_ptr() as usize;
250 let start = (parts.first()?.as_ptr() as usize).checked_sub(base)?;
251 let len = parts.iter().map(|p| p.len()).sum::<usize>() + parts.len() - 1;
252 let joined = src.get(start..start.checked_add(len)?)?;
253 let bytes = joined.as_bytes();
254 let mut offset = 0;
255 for (i, part) in parts.iter().enumerate() {
256 // Pointer identity, not text equality: a part at this exact offset is these bytes.
257 if part.as_ptr() as usize != base + start + offset {
258 return None;
259 }
260 offset += part.len();
261 if i + 1 < parts.len() {
262 if bytes[offset] != b'\\' {
263 return None;
264 }
265 offset += 1;
266 }
267 }
268 Some(joined)
269}
270
271impl<'arena, 'src> std::fmt::Debug for Name<'arena, 'src> {
272 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
273 match self {
274 Self::Simple { value, span } => f
275 .debug_struct("Name")
276 .field("parts", &std::slice::from_ref(value))
277 .field("kind", &NameKind::Unqualified)
278 .field("span", span)
279 .finish(),
280 Self::Complex { parts, kind, span } => f
281 .debug_struct("Name")
282 .field("parts", parts)
283 .field("kind", kind)
284 .field("span", span)
285 .finish(),
286 Self::Error { span } => {
287 let empty: [&str; 0] = [];
288 f.debug_struct("Name")
289 .field("parts", &empty)
290 .field("kind", &NameKind::Error)
291 .field("span", span)
292 .finish()
293 }
294 }
295 }
296}
297
298#[cfg(feature = "serde")]
299impl<'arena, 'src> serde::Serialize for Name<'arena, 'src> {
300 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
301 use serde::ser::SerializeStruct;
302 let mut st = s.serialize_struct("Name", 3)?;
303 match self {
304 Self::Simple { value, span } => {
305 st.serialize_field("parts", std::slice::from_ref(value))?;
306 st.serialize_field("kind", &NameKind::Unqualified)?;
307 st.serialize_field("span", span)?;
308 }
309 Self::Complex { parts, kind, span } => {
310 st.serialize_field("parts", parts)?;
311 st.serialize_field("kind", kind)?;
312 st.serialize_field("span", span)?;
313 }
314 Self::Error { span } => {
315 let empty: [&str; 0] = [];
316 st.serialize_field("parts", &empty[..])?;
317 st.serialize_field("kind", &NameKind::Error)?;
318 st.serialize_field("span", span)?;
319 }
320 }
321 st.end()
322 }
323}
324
325/// How a name is qualified.
326#[derive(Debug, Clone, Copy, PartialEq, Eq)]
327#[cfg_attr(feature = "serde", derive(Serialize))]
328#[non_exhaustive]
329pub enum NameKind {
330 /// A bare identifier with no namespace separator: `Foo`, `strlen`.
331 Unqualified,
332 /// A name with at least one internal `\` but no leading backslash: `Foo\Bar`.
333 Qualified,
334 /// A name with a leading `\`: `\Foo\Bar`.
335 FullyQualified,
336 /// A name starting with the `namespace` keyword: `namespace\Foo`.
337 Relative,
338 /// Synthesised during error recovery — no real name was present in the source.
339 Error,
340}
341
342/// PHP built-in type keyword — compact alternative to `Name::Simple` for the
343/// 17 reserved type names.
344#[repr(u8)]
345#[derive(Debug, Clone, Copy, PartialEq, Eq)]
346#[cfg_attr(feature = "serde", derive(Serialize))]
347#[non_exhaustive]
348pub enum BuiltinType {
349 /// `int` — integer scalar type.
350 Int,
351 /// `float` — floating-point scalar type.
352 Float,
353 /// `string` — string scalar type.
354 String,
355 /// `bool` — boolean scalar type.
356 Bool,
357 /// `void` — return-only type indicating no value is returned.
358 Void,
359 /// `never` — return-only type for functions that never return normally (PHP 8.1+).
360 Never,
361 /// `mixed` — top type; accepts any value.
362 Mixed,
363 /// `object` — any object instance.
364 Object,
365 /// `iterable` — `array` or `Traversable` (deprecated in PHP 8.2; use `array|Traversable`).
366 Iterable,
367 /// `callable` — any callable value.
368 Callable,
369 /// `array` — any PHP array.
370 Array,
371 /// `self` — refers to the class in which the type hint appears.
372 Self_,
373 /// `parent` — refers to the parent class of the class in which the type hint appears.
374 Parent_,
375 /// `static` — late-static-bound type; the class on which the method was called.
376 Static,
377 /// `null` — the null type; only valid in union types.
378 Null,
379 /// `true` — the literal boolean `true` (PHP 8.2+).
380 True,
381 /// `false` — the literal boolean `false`.
382 False,
383}
384
385impl BuiltinType {
386 /// Returns the canonical lowercase spelling used in PHP and in serialized output.
387 #[inline]
388 pub fn as_str(self) -> &'static str {
389 match self {
390 Self::Int => "int",
391 Self::Float => "float",
392 Self::String => "string",
393 Self::Bool => "bool",
394 Self::Void => "void",
395 Self::Never => "never",
396 Self::Mixed => "mixed",
397 Self::Object => "object",
398 Self::Iterable => "iterable",
399 Self::Callable => "callable",
400 Self::Array => "array",
401 Self::Self_ => "self",
402 Self::Parent_ => "parent",
403 Self::Static => "static",
404 Self::Null => "null",
405 Self::True => "true",
406 Self::False => "false",
407 }
408 }
409}
410
411/// A type declaration with its source range.
412#[derive(Debug)]
413#[cfg_attr(feature = "serde", derive(Serialize))]
414pub struct TypeHint<'arena, 'src> {
415 /// Type form.
416 pub kind: TypeHintKind<'arena, 'src>,
417 /// Source range of this node.
418 pub span: Span,
419}
420
421/// A PHP type hint.
422///
423/// `Keyword` is the fast path for the 17 built-in type names (`int`, `string`,
424/// `bool`, `self`, `array`, etc.). It stores only a 1-byte discriminant and a
425/// `Span` instead of a `Name`.
426///
427/// Serialises identically to `Named` so all existing snapshots remain unchanged.
428#[derive(Debug)]
429#[non_exhaustive]
430pub enum TypeHintKind<'arena, 'src> {
431 /// A user-defined or qualified class name: `Foo`, `\Ns\Bar`.
432 Named(Name<'arena, 'src>),
433 /// Built-in type keyword (`int`, `string`, `bool`, `self`, …) — serialises as `Named` for snapshot compatibility.
434 Keyword(BuiltinType, Span),
435 /// Nullable type: `?T` — equivalent to `T|null`.
436 Nullable(&'arena TypeHint<'arena, 'src>),
437 /// Union type: `A|B|C` (PHP 8.0+).
438 Union(ArenaVec<'arena, TypeHint<'arena, 'src>>),
439 /// Intersection type: `A&B` (PHP 8.1+).
440 Intersection(ArenaVec<'arena, TypeHint<'arena, 'src>>),
441}
442
443#[cfg(feature = "serde")]
444impl<'arena, 'src> serde::Serialize for TypeHintKind<'arena, 'src> {
445 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
446 match self {
447 // Standard variants — match what #[derive(Serialize)] would produce.
448 Self::Named(name) => s.serialize_newtype_variant("TypeHintKind", 0, "Named", name),
449 Self::Nullable(inner) => {
450 s.serialize_newtype_variant("TypeHintKind", 2, "Nullable", inner)
451 }
452 Self::Union(types) => s.serialize_newtype_variant("TypeHintKind", 3, "Union", types),
453 Self::Intersection(types) => {
454 s.serialize_newtype_variant("TypeHintKind", 4, "Intersection", types)
455 }
456 // Keyword — serialise as if it were Named(Name::Simple { value: kw.as_str(), span }).
457 // This preserves all existing snapshot output.
458 Self::Keyword(builtin, span) => {
459 struct BuiltinNameRepr<'a>(&'a BuiltinType, &'a Span);
460 impl serde::Serialize for BuiltinNameRepr<'_> {
461 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
462 use serde::ser::SerializeStruct;
463 let mut st = s.serialize_struct("Name", 3)?;
464 st.serialize_field("parts", &[self.0.as_str()])?;
465 st.serialize_field("kind", &NameKind::Unqualified)?;
466 st.serialize_field("span", self.1)?;
467 st.end()
468 }
469 }
470 s.serialize_newtype_variant(
471 "TypeHintKind",
472 0,
473 "Named",
474 &BuiltinNameRepr(builtin, span),
475 )
476 }
477 }
478 }
479}