diff --git a/crates/fingerprint/src/attribute.rs b/crates/fingerprint/src/attribute.rs index 37393f69b..b92b236f5 100644 --- a/crates/fingerprint/src/attribute.rs +++ b/crates/fingerprint/src/attribute.rs @@ -29,6 +29,7 @@ impl Fingerprintable for Attribute<'_> { ) { "attr".hash(hasher); self.name.fingerprint_with_hasher(hasher, resolved_names, options); + self.turbofish.fingerprint_with_hasher(hasher, resolved_names, options); self.argument_list.fingerprint_with_hasher(hasher, resolved_names, options); } } diff --git a/crates/fingerprint/src/call.rs b/crates/fingerprint/src/call.rs index d64f6c15a..61be7584e 100644 --- a/crates/fingerprint/src/call.rs +++ b/crates/fingerprint/src/call.rs @@ -37,6 +37,7 @@ impl Fingerprintable for FunctionCall<'_> { ) { "fn_call".hash(hasher); self.function.fingerprint_with_hasher(hasher, resolved_names, options); + self.turbofish.fingerprint_with_hasher(hasher, resolved_names, options); self.argument_list.fingerprint_with_hasher(hasher, resolved_names, options); } } @@ -52,6 +53,7 @@ impl Fingerprintable for MethodCall<'_> { "method_call".hash(hasher); self.object.fingerprint_with_hasher(hasher, resolved_names, options); self.method.fingerprint_with_hasher(hasher, resolved_names, options); + self.turbofish.fingerprint_with_hasher(hasher, resolved_names, options); self.argument_list.fingerprint_with_hasher(hasher, resolved_names, options); } } @@ -68,6 +70,7 @@ impl Fingerprintable for NullSafeMethodCall<'_> { self.object.fingerprint_with_hasher(hasher, resolved_names, options); self.method.fingerprint_with_hasher(hasher, resolved_names, options); + self.turbofish.fingerprint_with_hasher(hasher, resolved_names, options); self.argument_list.fingerprint_with_hasher(hasher, resolved_names, options); } } @@ -83,6 +86,7 @@ impl Fingerprintable for StaticMethodCall<'_> { "static_method_call".hash(hasher); self.class.fingerprint_with_hasher(hasher, resolved_names, options); self.method.fingerprint_with_hasher(hasher, resolved_names, options); + self.turbofish.fingerprint_with_hasher(hasher, resolved_names, options); self.argument_list.fingerprint_with_hasher(hasher, resolved_names, options); } } diff --git a/crates/fingerprint/src/class_like/mod.rs b/crates/fingerprint/src/class_like/mod.rs index f726dfb2f..6c9ba156e 100644 --- a/crates/fingerprint/src/class_like/mod.rs +++ b/crates/fingerprint/src/class_like/mod.rs @@ -7,6 +7,7 @@ use mago_syntax::ast::Class; use mago_syntax::ast::ClassLikeConstant; use mago_syntax::ast::ClassLikeConstantItem; use mago_syntax::ast::ClassLikeMember; +use mago_syntax::ast::ClassLikeReference; use mago_syntax::ast::Enum; use mago_syntax::ast::EnumBackingTypeHint; use mago_syntax::ast::EnumCase; @@ -57,6 +58,24 @@ impl Fingerprintable for AnonymousClass<'_> { } } +impl Fingerprintable for ClassLikeReference<'_> { + #[inline] + fn fingerprint_with_hasher( + &self, + hasher: &mut H, + resolved_names: &ResolvedNames, + options: &FingerprintOptions<'_>, + ) { + self.name.fingerprint_with_hasher(hasher, resolved_names, options); + if let Some(arguments) = &self.generic_arguments { + "generic_args".hash(hasher); + for argument in arguments.arguments.iter() { + argument.fingerprint_with_hasher(hasher, resolved_names, options); + } + } + } +} + impl Fingerprintable for Extends<'_> { #[inline] fn fingerprint_with_hasher( @@ -475,6 +494,7 @@ impl Fingerprintable for Method<'_> { "by_ref".hash(hasher); } self.name.fingerprint_with_hasher(hasher, resolved_names, options); + self.generic_parameters.fingerprint_with_hasher(hasher, resolved_names, options); self.parameter_list.fingerprint_with_hasher(hasher, resolved_names, options); self.return_type_hint.fingerprint_with_hasher(hasher, resolved_names, options); @@ -525,6 +545,7 @@ impl Fingerprintable for Class<'_> { } crate::modifier::fingerprint_modifiers(self.modifiers.iter(), hasher, resolved_names, options); self.name.fingerprint_with_hasher(hasher, resolved_names, options); + self.generic_parameters.fingerprint_with_hasher(hasher, resolved_names, options); self.extends.fingerprint_with_hasher(hasher, resolved_names, options); self.implements.fingerprint_with_hasher(hasher, resolved_names, options); for member in &self.members { @@ -553,6 +574,7 @@ impl Fingerprintable for Interface<'_> { attribute_list.fingerprint_with_hasher(hasher, resolved_names, options); } self.name.fingerprint_with_hasher(hasher, resolved_names, options); + self.generic_parameters.fingerprint_with_hasher(hasher, resolved_names, options); self.extends.fingerprint_with_hasher(hasher, resolved_names, options); for member in &self.members { member.fingerprint_with_hasher(hasher, resolved_names, options); @@ -580,6 +602,7 @@ impl Fingerprintable for Trait<'_> { attribute_list.fingerprint_with_hasher(hasher, resolved_names, options); } self.name.fingerprint_with_hasher(hasher, resolved_names, options); + self.generic_parameters.fingerprint_with_hasher(hasher, resolved_names, options); for member in &self.members { member.fingerprint_with_hasher(hasher, resolved_names, options); } diff --git a/crates/fingerprint/src/function_like/mod.rs b/crates/fingerprint/src/function_like/mod.rs index f37d1fe73..eac63dae5 100644 --- a/crates/fingerprint/src/function_like/mod.rs +++ b/crates/fingerprint/src/function_like/mod.rs @@ -30,6 +30,7 @@ impl Fingerprintable for Closure<'_> { } self.r#static.is_some().hash(hasher); self.ampersand.is_some().hash(hasher); + self.generic_parameters.fingerprint_with_hasher(hasher, resolved_names, options); self.parameter_list.fingerprint_with_hasher(hasher, resolved_names, options); self.use_clause.fingerprint_with_hasher(hasher, resolved_names, options); self.return_type_hint.fingerprint_with_hasher(hasher, resolved_names, options); @@ -88,6 +89,7 @@ impl Fingerprintable for Function<'_> { } self.ampersand.is_some().hash(hasher); self.name.fingerprint_with_hasher(hasher, resolved_names, options); + self.generic_parameters.fingerprint_with_hasher(hasher, resolved_names, options); self.parameter_list.fingerprint_with_hasher(hasher, resolved_names, options); self.return_type_hint.fingerprint_with_hasher(hasher, resolved_names, options); @@ -111,6 +113,7 @@ impl Fingerprintable for ArrowFunction<'_> { } self.r#static.is_some().hash(hasher); self.ampersand.is_some().hash(hasher); + self.generic_parameters.fingerprint_with_hasher(hasher, resolved_names, options); self.parameter_list.fingerprint_with_hasher(hasher, resolved_names, options); self.return_type_hint.fingerprint_with_hasher(hasher, resolved_names, options); self.expression.fingerprint_with_hasher(hasher, resolved_names, options); diff --git a/crates/fingerprint/src/generic.rs b/crates/fingerprint/src/generic.rs new file mode 100644 index 000000000..512d79de2 --- /dev/null +++ b/crates/fingerprint/src/generic.rs @@ -0,0 +1,84 @@ +use std::hash::Hash; + +use mago_names::ResolvedNames; +use mago_syntax::ast::GenericArgumentList; +use mago_syntax::ast::GenericParameter; +use mago_syntax::ast::GenericParameterList; +use mago_syntax::ast::GenericVariance; +use mago_syntax::ast::Turbofish; + +use crate::Fingerprintable; +use crate::FingerprintOptions; + +impl Fingerprintable for GenericParameterList<'_> { + #[inline] + fn fingerprint_with_hasher( + &self, + hasher: &mut H, + resolved_names: &ResolvedNames, + options: &FingerprintOptions<'_>, + ) { + "generic_param_list".hash(hasher); + (self.parameters.as_slice().len() as u32).hash(hasher); + for parameter in self.parameters.iter() { + parameter.fingerprint_with_hasher(hasher, resolved_names, options); + } + } +} + +impl Fingerprintable for GenericParameter<'_> { + #[inline] + fn fingerprint_with_hasher( + &self, + hasher: &mut H, + resolved_names: &ResolvedNames, + options: &FingerprintOptions<'_>, + ) { + match self.variance { + Some(GenericVariance::Covariant(_)) => "covariant".hash(hasher), + Some(GenericVariance::Contravariant(_)) => "contravariant".hash(hasher), + None => "invariant".hash(hasher), + } + self.name.value.hash(hasher); + if let Some(bound) = &self.bound { + "bound".hash(hasher); + bound.hint.fingerprint_with_hasher(hasher, resolved_names, options); + } + if let Some(default) = &self.default { + "default".hash(hasher); + default.hint.fingerprint_with_hasher(hasher, resolved_names, options); + } + } +} + +impl Fingerprintable for GenericArgumentList<'_> { + #[inline] + fn fingerprint_with_hasher( + &self, + hasher: &mut H, + resolved_names: &ResolvedNames, + options: &FingerprintOptions<'_>, + ) { + "generic_arg_list".hash(hasher); + (self.arguments.as_slice().len() as u32).hash(hasher); + for argument in self.arguments.iter() { + argument.fingerprint_with_hasher(hasher, resolved_names, options); + } + } +} + +impl Fingerprintable for Turbofish<'_> { + #[inline] + fn fingerprint_with_hasher( + &self, + hasher: &mut H, + resolved_names: &ResolvedNames, + options: &FingerprintOptions<'_>, + ) { + "turbofish".hash(hasher); + (self.arguments.as_slice().len() as u32).hash(hasher); + for argument in self.arguments.iter() { + argument.fingerprint_with_hasher(hasher, resolved_names, options); + } + } +} diff --git a/crates/fingerprint/src/instantiation.rs b/crates/fingerprint/src/instantiation.rs index 8babb2600..8e5df3672 100644 --- a/crates/fingerprint/src/instantiation.rs +++ b/crates/fingerprint/src/instantiation.rs @@ -15,6 +15,7 @@ impl Fingerprintable for Instantiation<'_> { ) { "new".hash(hasher); self.class.fingerprint_with_hasher(hasher, resolved_names, options); + self.turbofish.fingerprint_with_hasher(hasher, resolved_names, options); self.argument_list.fingerprint_with_hasher(hasher, resolved_names, options); } } diff --git a/crates/fingerprint/src/lib.rs b/crates/fingerprint/src/lib.rs index 1428bb655..d6ca5a298 100644 --- a/crates/fingerprint/src/lib.rs +++ b/crates/fingerprint/src/lib.rs @@ -24,6 +24,7 @@ pub mod declare; pub mod echo; pub mod expression; pub mod function_like; +pub mod generic; pub mod global; pub mod goto; pub mod halt_compiler; diff --git a/crates/fingerprint/src/partial_application.rs b/crates/fingerprint/src/partial_application.rs index fa07f4b6b..2353e147a 100644 --- a/crates/fingerprint/src/partial_application.rs +++ b/crates/fingerprint/src/partial_application.rs @@ -36,6 +36,7 @@ impl Fingerprintable for FunctionPartialApplication<'_> { ) { "fn_partial".hash(hasher); self.function.fingerprint_with_hasher(hasher, resolved_names, options); + self.turbofish.fingerprint_with_hasher(hasher, resolved_names, options); self.argument_list.fingerprint_with_hasher(hasher, resolved_names, options); } } @@ -51,6 +52,7 @@ impl Fingerprintable for MethodPartialApplication<'_> { "method_partial".hash(hasher); self.object.fingerprint_with_hasher(hasher, resolved_names, options); self.method.fingerprint_with_hasher(hasher, resolved_names, options); + self.turbofish.fingerprint_with_hasher(hasher, resolved_names, options); self.argument_list.fingerprint_with_hasher(hasher, resolved_names, options); } } @@ -66,6 +68,7 @@ impl Fingerprintable for StaticMethodPartialApplication<'_> { "static_method_partial".hash(hasher); self.class.fingerprint_with_hasher(hasher, resolved_names, options); self.method.fingerprint_with_hasher(hasher, resolved_names, options); + self.turbofish.fingerprint_with_hasher(hasher, resolved_names, options); self.argument_list.fingerprint_with_hasher(hasher, resolved_names, options); } } diff --git a/crates/fingerprint/src/type_hint.rs b/crates/fingerprint/src/type_hint.rs index 38857e09b..1db822333 100644 --- a/crates/fingerprint/src/type_hint.rs +++ b/crates/fingerprint/src/type_hint.rs @@ -26,6 +26,13 @@ impl Fingerprintable for Hint<'_> { Hint::Nullable(n) => n.fingerprint_with_hasher(hasher, resolved_names, options), Hint::Union(u) => u.fingerprint_with_hasher(hasher, resolved_names, options), Hint::Intersection(i) => i.fingerprint_with_hasher(hasher, resolved_names, options), + Hint::Generic(g) => { + "generic".hash(hasher); + g.base.fingerprint_with_hasher(hasher, resolved_names, options); + for argument in g.arguments.arguments.iter() { + argument.fingerprint_with_hasher(hasher, resolved_names, options); + } + } Hint::Null(_) => "null".hash(hasher), Hint::True(_) => "true".hash(hasher), Hint::False(_) => "false".hash(hasher), diff --git a/crates/formatter/src/internal/format/call_node.rs b/crates/formatter/src/internal/format/call_node.rs index 3079fa7cd..4e410d2e7 100644 --- a/crates/formatter/src/internal/format/call_node.rs +++ b/crates/formatter/src/internal/format/call_node.rs @@ -112,14 +112,39 @@ pub(super) fn print_call_like_node<'arena>( // format the callee-like expression let mut parts = match node { CallLikeNode::Call(c) => match c { - Call::Function(c) => vec![in f.arena; c.function.format(f)], - Call::StaticMethod(c) => vec![in f.arena; c.class.format(f), Document::String("::"), c.method.format(f)], + Call::Function(c) => { + let mut parts = vec![in f.arena; c.function.format(f)]; + if let Some(turbofish) = &c.turbofish { + parts.push(turbofish.format(f)); + } + parts + } + Call::StaticMethod(c) => { + let mut parts = + vec![in f.arena; c.class.format(f), Document::String("::"), c.method.format(f)]; + if let Some(turbofish) = &c.turbofish { + parts.push(turbofish.format(f)); + } + parts + } _ => { return print_access_call_node(f, c); } }, - CallLikeNode::Instantiation(i) => vec![in f.arena; i.new.format(f), Document::space(), i.class.format(f)], - CallLikeNode::Attribute(a) => vec![in f.arena; a.name.format(f)], + CallLikeNode::Instantiation(i) => { + let mut parts = vec![in f.arena; i.new.format(f), Document::space(), i.class.format(f)]; + if let Some(turbofish) = &i.turbofish { + parts.push(turbofish.format(f)); + } + parts + } + CallLikeNode::Attribute(a) => { + let mut parts = vec![in f.arena; a.name.format(f)]; + if let Some(turbofish) = &a.turbofish { + parts.push(turbofish.format(f)); + } + parts + } CallLikeNode::DieConstruct(d) => vec![in f.arena; d.die.format(f)], CallLikeNode::ExitConstruct(e) => vec![in f.arena; e.exit.format(f)], }; @@ -130,13 +155,20 @@ pub(super) fn print_call_like_node<'arena>( } fn print_access_call_node<'arena>(f: &mut FormatterState<'_, 'arena>, node: &'arena Call<'arena>) -> Document<'arena> { - let (base, operator, operator_str, selector) = match node { - Call::Method(method_call) => (&method_call.object, method_call.arrow, "->", &method_call.method), + let (base, operator, operator_str, selector, turbofish) = match node { + Call::Method(method_call) => ( + &method_call.object, + method_call.arrow, + "->", + &method_call.method, + method_call.turbofish.as_ref(), + ), Call::NullSafeMethod(null_safe_method_call) => ( &null_safe_method_call.object, null_safe_method_call.question_mark_arrow, "?->", &null_safe_method_call.method, + null_safe_method_call.turbofish.as_ref(), ), #[allow(clippy::unreachable)] _ => unreachable!(), @@ -147,6 +179,8 @@ fn print_access_call_node<'arena>(f: &mut FormatterState<'_, 'arena>, node: &'ar || (f.settings.preserve_breaking_member_access_chain && misc::has_new_line_in_range(f.source_text, base_span.end.offset, operator.start.offset)); + let turbofish_doc = turbofish.map(|t| t.format(f)).unwrap_or_else(Document::empty); + if should_break { Document::Group(Group::new(vec![ in f.arena; @@ -156,6 +190,7 @@ fn print_access_call_node<'arena>(f: &mut FormatterState<'_, 'arena>, node: &'ar Document::Line(Line::hard()), format_access_operator(f, operator, operator_str), selector.format(f), + turbofish_doc, print_call_arguments(f, CallLikeNode::Call(node)), ]), ])) @@ -165,6 +200,7 @@ fn print_access_call_node<'arena>(f: &mut FormatterState<'_, 'arena>, node: &'ar base.format(f), format_access_operator(f, operator, operator_str), selector.format(f), + turbofish_doc, print_call_arguments(f, CallLikeNode::Call(node)), ])) } diff --git a/crates/formatter/src/internal/format/expression.rs b/crates/formatter/src/internal/format/expression.rs index f2414c2e2..5af033995 100644 --- a/crates/formatter/src/internal/format/expression.rs +++ b/crates/formatter/src/internal/format/expression.rs @@ -753,6 +753,9 @@ impl<'arena> Format<'arena> for ArrowFunction<'arena> { } contents.push(self.r#fn.format(f)); + if let Some(generic_parameters) = &self.generic_parameters { + contents.push(generic_parameters.format(f)); + } if f.settings.space_before_arrow_function_parameter_list_parenthesis { contents.push(Document::space()); } @@ -1748,7 +1751,12 @@ impl<'arena> Format<'arena> for PartialApplication<'arena> { impl<'arena> Format<'arena> for FunctionPartialApplication<'arena> { fn format(&'arena self, f: &mut FormatterState<'_, 'arena>) -> Document<'arena> { wrap!(f, self, FunctionPartialApplication, { - Document::Group(Group::new(vec![in f.arena; self.function.format(f), self.argument_list.format(f)])) + let mut parts = vec![in f.arena; self.function.format(f)]; + if let Some(turbofish) = &self.turbofish { + parts.push(turbofish.format(f)); + } + parts.push(self.argument_list.format(f)); + Document::Group(Group::new(parts)) }) } } @@ -1756,13 +1764,17 @@ impl<'arena> Format<'arena> for FunctionPartialApplication<'arena> { impl<'arena> Format<'arena> for MethodPartialApplication<'arena> { fn format(&'arena self, f: &mut FormatterState<'_, 'arena>) -> Document<'arena> { wrap!(f, self, MethodPartialApplication, { - Document::Group(Group::new(vec![ + let mut parts = vec![ in f.arena; self.object.format(f), Document::String("->"), self.method.format(f), - self.argument_list.format(f), - ])) + ]; + if let Some(turbofish) = &self.turbofish { + parts.push(turbofish.format(f)); + } + parts.push(self.argument_list.format(f)); + Document::Group(Group::new(parts)) }) } } @@ -1770,13 +1782,17 @@ impl<'arena> Format<'arena> for MethodPartialApplication<'arena> { impl<'arena> Format<'arena> for StaticMethodPartialApplication<'arena> { fn format(&'arena self, f: &mut FormatterState<'_, 'arena>) -> Document<'arena> { wrap!(f, self, StaticMethodPartialApplication, { - Document::Group(Group::new(vec![ + let mut parts = vec![ in f.arena; self.class.format(f), Document::String("::"), self.method.format(f), - self.argument_list.format(f), - ])) + ]; + if let Some(turbofish) = &self.turbofish { + parts.push(turbofish.format(f)); + } + parts.push(self.argument_list.format(f)); + Document::Group(Group::new(parts)) }) } } diff --git a/crates/formatter/src/internal/format/function_like.rs b/crates/formatter/src/internal/format/function_like.rs index a8dacfa4b..95cca0d1d 100644 --- a/crates/formatter/src/internal/format/function_like.rs +++ b/crates/formatter/src/internal/format/function_like.rs @@ -10,6 +10,7 @@ use mago_syntax::ast::ClosureUseClauseVariable; use mago_syntax::ast::Function; use mago_syntax::ast::FunctionLikeParameterList; use mago_syntax::ast::FunctionLikeReturnTypeHint; +use mago_syntax::ast::GenericParameterList; use mago_syntax::ast::Keyword; use mago_syntax::ast::LocalIdentifier; use mago_syntax::ast::Method; @@ -56,6 +57,7 @@ struct FunctionLikeParts<'arena> { pub fn_or_function: &'arena Keyword<'arena>, pub ampersand: Option, pub name: Option<&'arena LocalIdentifier<'arena>>, + pub generic_parameters: Option<&'arena GenericParameterList<'arena>>, pub parameter_list: &'arena FunctionLikeParameterList<'arena>, pub use_clause: Option<&'arena ClosureUseClause<'arena>>, pub return_type_hint: Option<&'arena FunctionLikeReturnTypeHint<'arena>>, @@ -71,6 +73,7 @@ impl<'arena> FunctionLikeParts<'arena> { fn_or_function: &closure.function, ampersand: closure.ampersand, name: None, + generic_parameters: closure.generic_parameters.as_ref(), parameter_list: &closure.parameter_list, use_clause: closure.use_clause.as_ref(), return_type_hint: closure.return_type_hint.as_ref(), @@ -86,6 +89,7 @@ impl<'arena> FunctionLikeParts<'arena> { fn_or_function: &function.function, ampersand: function.ampersand, name: Some(&function.name), + generic_parameters: function.generic_parameters.as_ref(), parameter_list: &function.parameter_list, use_clause: None, return_type_hint: function.return_type_hint.as_ref(), @@ -101,6 +105,7 @@ impl<'arena> FunctionLikeParts<'arena> { fn_or_function: &method.function, ampersand: method.ampersand, name: Some(&method.name), + generic_parameters: method.generic_parameters.as_ref(), parameter_list: &method.parameter_list, use_clause: None, return_type_hint: method.return_type_hint.as_ref(), @@ -198,6 +203,11 @@ impl<'arena> FunctionLikeParts<'arena> { signature.push(name.format(f)); } + // Add generic type parameters: ``. + if let Some(generic_parameters) = self.generic_parameters { + signature.push(generic_parameters.format(f)); + } + let signature_id = f.next_id(); // Add parameter list directly - don't wrap in another group signature.push(Document::Group( diff --git a/crates/formatter/src/internal/format/generic.rs b/crates/formatter/src/internal/format/generic.rs new file mode 100644 index 000000000..c9a5148bd --- /dev/null +++ b/crates/formatter/src/internal/format/generic.rs @@ -0,0 +1,107 @@ +use bumpalo::vec; + +use mago_span::HasSpan; +use mago_syntax::ast::GenericArgumentList; +use mago_syntax::ast::GenericParameter; +use mago_syntax::ast::GenericParameterBound; +use mago_syntax::ast::GenericParameterDefault; +use mago_syntax::ast::GenericParameterList; +use mago_syntax::ast::GenericVariance; +use mago_syntax::ast::Turbofish; + +use crate::document::Document; +use crate::internal::FormatterState; +use crate::internal::format::Format; +use crate::wrap; + +impl<'arena> Format<'arena> for GenericParameterList<'arena> { + fn format(&'arena self, f: &mut FormatterState<'_, 'arena>) -> Document<'arena> { + wrap!(f, self, GenericParameterList, { + let mut parts = vec![in f.arena; Document::String("<")]; + let mut first = true; + for parameter in self.parameters.iter() { + if !first { + parts.push(Document::String(", ")); + } + first = false; + parts.push(parameter.format(f)); + } + parts.push(Document::String(">")); + Document::Array(parts) + }) + } +} + +impl<'arena> Format<'arena> for GenericParameter<'arena> { + fn format(&'arena self, f: &mut FormatterState<'_, 'arena>) -> Document<'arena> { + wrap!(f, self, GenericParameter, { + let mut parts = vec![in f.arena]; + if let Some(variance) = &self.variance { + parts.push(Document::String(match variance { + GenericVariance::Covariant(_) => "+", + GenericVariance::Contravariant(_) => "-", + })); + } + parts.push(self.name.format(f)); + if let Some(bound) = &self.bound { + parts.push(bound.format(f)); + } + if let Some(default) = &self.default { + parts.push(default.format(f)); + } + Document::Array(parts) + }) + } +} + +impl<'arena> Format<'arena> for GenericParameterBound<'arena> { + fn format(&'arena self, f: &mut FormatterState<'_, 'arena>) -> Document<'arena> { + wrap!(f, self, GenericParameterBound, { + Document::Array(vec![in f.arena; Document::String(": "), self.hint.format(f)]) + }) + } +} + +impl<'arena> Format<'arena> for GenericParameterDefault<'arena> { + fn format(&'arena self, f: &mut FormatterState<'_, 'arena>) -> Document<'arena> { + wrap!(f, self, GenericParameterDefault, { + Document::Array(vec![in f.arena; Document::String(" = "), self.hint.format(f)]) + }) + } +} + +impl<'arena> Format<'arena> for GenericArgumentList<'arena> { + fn format(&'arena self, f: &mut FormatterState<'_, 'arena>) -> Document<'arena> { + wrap!(f, self, GenericArgumentList, { + let mut parts = vec![in f.arena; Document::String("<")]; + let mut first = true; + for argument in self.arguments.iter() { + if !first { + parts.push(Document::String(", ")); + } + first = false; + parts.push(argument.format(f)); + } + parts.push(Document::String(">")); + Document::Array(parts) + }) + } +} + +impl<'arena> Format<'arena> for Turbofish<'arena> { + fn format(&'arena self, f: &mut FormatterState<'_, 'arena>) -> Document<'arena> { + wrap!(f, self, Turbofish, { + let mut parts = vec![in f.arena; Document::String("::<")]; + let mut first = true; + for argument in self.arguments.iter() { + if !first { + parts.push(Document::String(", ")); + } + first = false; + parts.push(argument.format(f)); + } + parts.push(Document::String(">")); + Document::Array(parts) + }) + } +} diff --git a/crates/formatter/src/internal/format/mod.rs b/crates/formatter/src/internal/format/mod.rs index bc2fd5b44..f46167e2d 100644 --- a/crates/formatter/src/internal/format/mod.rs +++ b/crates/formatter/src/internal/format/mod.rs @@ -10,6 +10,7 @@ use mago_syntax::ast::Class; use mago_syntax::ast::ClassLikeConstant; use mago_syntax::ast::ClassLikeConstantItem; use mago_syntax::ast::ClassLikeMember; +use mago_syntax::ast::ClassLikeReference; use mago_syntax::ast::ClosingTag; use mago_syntax::ast::Constant; use mago_syntax::ast::ConstantItem; @@ -134,6 +135,7 @@ pub mod class_like; pub mod control_structure; pub mod expression; pub mod function_like; +pub mod generic; pub mod member_access; pub mod misc; pub mod parameters; @@ -432,6 +434,18 @@ impl<'arena> Format<'arena> for FullyQualifiedIdentifier<'arena> { } } +impl<'arena> Format<'arena> for ClassLikeReference<'arena> { + fn format(&'arena self, f: &mut FormatterState<'_, 'arena>) -> Document<'arena> { + wrap!(f, self, ClassLikeReference, { + let mut parts = vec![in f.arena; self.name.format(f)]; + if let Some(arguments) = &self.generic_arguments { + parts.push(arguments.format(f)); + } + Document::Array(parts) + }) + } +} + impl<'arena> Format<'arena> for Use<'arena> { fn format(&'arena self, f: &mut FormatterState<'_, 'arena>) -> Document<'arena> { wrap!(f, self, Use, { @@ -1141,6 +1155,11 @@ impl<'arena> Format<'arena> for Interface<'arena> { self.interface.format(f), Document::space(), self.name.format(f), + if let Some(generic_parameters) = &self.generic_parameters { + generic_parameters.format(f) + } else { + Document::empty() + }, if let Some(e) = &self.extends { Document::Array(vec![in f.arena; Document::space(), e.format(f)]) } else { @@ -1184,6 +1203,10 @@ impl<'arena> Format<'arena> for Class<'arena> { signature.push(Document::space()); signature.push(self.name.format(f)); + if let Some(generic_parameters) = &self.generic_parameters { + signature.push(generic_parameters.format(f)); + } + if let Some(e) = &self.extends { signature.push(Document::space()); signature.push(e.format(f)); @@ -1218,10 +1241,16 @@ impl<'arena> Format<'arena> for Trait<'arena> { attributes.push(Document::Line(Line::hard())); } + let mut signature = + vec![in f.arena; self.r#trait.format(f), Document::space(), self.name.format(f)]; + if let Some(generic_parameters) = &self.generic_parameters { + signature.push(generic_parameters.format(f)); + } + Document::Group(Group::new(vec![ in f.arena; Document::Group(Group::new(attributes)), - Document::Group(Group::new(vec![in f.arena; self.r#trait.format(f), Document::space(), self.name.format(f)])), + Document::Group(Group::new(signature)), print_class_like_body(f, &self.left_brace, &self.members, &self.right_brace, None), ])) }) @@ -1511,6 +1540,9 @@ impl<'arena> Format<'arena> for Hint<'arena> { format_token(f, intersection_hint.ampersand, "&"), intersection_hint.right.format(f), ]), + Hint::Generic(generic_hint) => { + Document::Array(vec![in f.arena; generic_hint.base.format(f), generic_hint.arguments.format(f)]) + } Hint::Null(_) => Document::String("null"), Hint::True(_) => Document::String("true"), Hint::False(_) => Document::String("false"), diff --git a/crates/formatter/src/internal/utils.rs b/crates/formatter/src/internal/utils.rs index 66bdb966d..1a33f50e1 100644 --- a/crates/formatter/src/internal/utils.rs +++ b/crates/formatter/src/internal/utils.rs @@ -355,7 +355,7 @@ pub fn get_expression_width(element: &Expression<'_>) -> Option { Expression::ConstantAccess(ConstantAccess { name: Identifier::Local(local) }) | Expression::Identifier(Identifier::Local(local)) => string_width(local.value), Expression::Variable(Variable::Direct(variable)) => string_width(variable.name), - Expression::Call(Call::Function(FunctionCall { function, argument_list })) => { + Expression::Call(Call::Function(FunctionCall { function, argument_list, .. })) => { let function_width = get_expression_width(function)?; let args_width = get_argument_list_width(argument_list)?; diff --git a/crates/linter/src/rule/consistency/no_fully_qualified_global_class_like.rs b/crates/linter/src/rule/consistency/no_fully_qualified_global_class_like.rs index d84d69aa9..7e117cb8c 100644 --- a/crates/linter/src/rule/consistency/no_fully_qualified_global_class_like.rs +++ b/crates/linter/src/rule/consistency/no_fully_qualified_global_class_like.rs @@ -183,13 +183,13 @@ impl LintRule for NoFullyQualifiedGlobalClassLikeRule { self.report_if_fq(ctx, attribute.name); } Node::Extends(extends) => { - for identifier in extends.types.iter() { - self.report_if_fq(ctx, *identifier); + for reference in extends.types.iter() { + self.report_if_fq(ctx, reference.name); } } Node::Implements(implements) => { - for identifier in implements.types.iter() { - self.report_if_fq(ctx, *identifier); + for reference in implements.types.iter() { + self.report_if_fq(ctx, reference.name); } } Node::Instantiation(instantiation) => { @@ -235,8 +235,8 @@ impl LintRule for NoFullyQualifiedGlobalClassLikeRule { self.report_if_fq(ctx, *identifier); } Node::TraitUse(trait_use) => { - for identifier in trait_use.trait_names.iter() { - self.report_if_fq(ctx, *identifier); + for reference in trait_use.trait_names.iter() { + self.report_if_fq(ctx, reference.name); } } _ => {} diff --git a/crates/names/src/internal/walker.rs b/crates/names/src/internal/walker.rs index 2fd1eff60..c25243d71 100644 --- a/crates/names/src/internal/walker.rs +++ b/crates/names/src/internal/walker.rs @@ -125,17 +125,17 @@ impl<'ast, 'arena> MutWalker<'ast, 'arena, NameResolutionContext<'arena>> for Na fn walk_in_trait_use(&mut self, trait_use: &'ast TraitUse<'arena>, context: &mut NameResolutionContext<'arena>) { for trait_name in &trait_use.trait_names { - let (trait_classlike, imported) = context.resolve(NameKind::Default, trait_name.value()); + let (trait_classlike, imported) = context.resolve(NameKind::Default, trait_name.name.value()); - self.resolved_names.insert_at(trait_name.span(), trait_classlike, imported); + self.resolved_names.insert_at(trait_name.name.span(), trait_classlike, imported); } } fn walk_in_extends(&mut self, extends: &'ast Extends<'arena>, context: &mut NameResolutionContext<'arena>) { for parent in &extends.types { - let (parent_classlike, imported) = context.resolve(NameKind::Default, parent.value()); + let (parent_classlike, imported) = context.resolve(NameKind::Default, parent.name.value()); - self.resolved_names.insert_at(parent.span(), parent_classlike, imported); + self.resolved_names.insert_at(parent.name.span(), parent_classlike, imported); } } @@ -145,9 +145,9 @@ impl<'ast, 'arena> MutWalker<'ast, 'arena, NameResolutionContext<'arena>> for Na context: &mut NameResolutionContext<'arena>, ) { for parent in &implements.types { - let (parent_classlike, imported) = context.resolve(NameKind::Default, parent.value()); + let (parent_classlike, imported) = context.resolve(NameKind::Default, parent.name.value()); - self.resolved_names.insert_at(parent.span(), parent_classlike, imported); + self.resolved_names.insert_at(parent.name.span(), parent_classlike, imported); } } diff --git a/crates/php-version/src/feature.rs b/crates/php-version/src/feature.rs index b597092fe..e415953e0 100644 --- a/crates/php-version/src/feature.rs +++ b/crates/php-version/src/feature.rs @@ -80,6 +80,7 @@ pub enum Feature { NeverTypeHint, StaticReturnTypeHint, NewWithoutParentheses, + BoundErasedGenericTypes, ClassLikeConstantVisibilityModifiers, ListReferenceAssignment, NumericLiteralSeparator, diff --git a/crates/php-version/src/lib.rs b/crates/php-version/src/lib.rs index 6424d7f93..08c94b9c1 100644 --- a/crates/php-version/src/lib.rs +++ b/crates/php-version/src/lib.rs @@ -333,7 +333,7 @@ impl PHPVersion { Feature::InterfaceConstantImplicitlyFinal => self.0 < 0x08_01_00, Feature::PassNoneEncodings => self.0 < 0x07_03_00, Feature::ImplicitlyNullableParameterTypes => self.0 < 0x09_00_00, - Feature::PartialFunctionApplication => self.0 >= 0x08_06_00, + Feature::PartialFunctionApplication | Feature::BoundErasedGenericTypes => self.0 >= 0x08_06_00, _ => true, } } diff --git a/crates/semantics/src/internal/checker/class_like/inheritance.rs b/crates/semantics/src/internal/checker/class_like/inheritance.rs index 9d4ef927f..1a6d0ced2 100644 --- a/crates/semantics/src/internal/checker/class_like/inheritance.rs +++ b/crates/semantics/src/internal/checker/class_like/inheritance.rs @@ -56,7 +56,7 @@ pub fn check_extends( } for extended_type in &extends.types { - let extended_name = extended_type.value(); + let extended_name = extended_type.name.value(); if RESERVED_KEYWORDS.iter().any(|keyword| keyword.eq_ignore_ascii_case(extended_name)) || SOFT_RESERVED_KEYWORDS_MINUS_SYMBOL_ALLOWED @@ -114,7 +114,7 @@ pub fn check_implements( } for implemented_type in &implements.types { - let implemented_name = implemented_type.value(); + let implemented_name = implemented_type.name.value(); if RESERVED_KEYWORDS.iter().any(|keyword| keyword.eq_ignore_ascii_case(implemented_name)) || SOFT_RESERVED_KEYWORDS_MINUS_SYMBOL_ALLOWED diff --git a/crates/semantics/src/internal/checker/expression.rs b/crates/semantics/src/internal/checker/expression.rs index 588f9319c..4d1f48b75 100644 --- a/crates/semantics/src/internal/checker/expression.rs +++ b/crates/semantics/src/internal/checker/expression.rs @@ -36,7 +36,7 @@ pub fn check_for_clone_with(expr: &Expression, context: &mut Context<'_, '_, '_> return; } - let Expression::Call(Call::Function(FunctionCall { function, argument_list })) = expr else { + let Expression::Call(Call::Function(FunctionCall { function, argument_list, .. })) = expr else { return; }; diff --git a/crates/semantics/src/internal/checker/generic.rs b/crates/semantics/src/internal/checker/generic.rs new file mode 100644 index 000000000..4d270eabb --- /dev/null +++ b/crates/semantics/src/internal/checker/generic.rs @@ -0,0 +1,219 @@ +use mago_php_version::feature::Feature; +use mago_reporting::Annotation; +use mago_reporting::Issue; +use mago_span::HasSpan; +use mago_syntax::ast::GenericArgumentList; +use mago_syntax::ast::GenericParameter; +use mago_syntax::ast::GenericParameterList; +use mago_syntax::ast::GenericVariance; +use mago_syntax::ast::Hint; +use mago_syntax::ast::Turbofish; + +use crate::internal::context::Context; + +/// Maximum entries in any generic parameter or argument list. +const MAX_GENERIC_LIST_LEN: usize = 127; + +#[inline] +pub fn check_generic_parameter_list(list: &GenericParameterList, context: &mut Context<'_, '_, '_>) { + if !context.version.is_supported(Feature::BoundErasedGenericTypes) { + context.report( + Issue::error("Generic type parameters are only available in PHP 8.6 and above.") + .with_annotation(Annotation::primary(list.span()).with_message("Generic parameter list used here.")) + .with_note("PHP recognises generic type parameters on classes, interfaces, traits, functions, methods, closures, and arrow functions starting in PHP 8.6.") + .with_help("Upgrade to PHP 8.6 or above to use generic type parameters."), + ); + return; + } + + let nodes = list.parameters.as_slice(); + if nodes.len() > MAX_GENERIC_LIST_LEN { + context.report( + Issue::error(format!( + "Generic parameter list has {} entries; the maximum is {MAX_GENERIC_LIST_LEN}.", + nodes.len() + )) + .with_annotation(Annotation::primary(list.span()).with_message("Too many generic parameters.")) + .with_note("PHP caps every generic parameter list at 127 entries.") + .with_help("Reduce the number of type parameters on this declaration to 127 or fewer."), + ); + } + + let mut seen_default = false; + for parameter in nodes { + if parameter.default.is_some() { + seen_default = true; + } else if seen_default { + context.report( + Issue::error(format!( + "Required type parameter `{}` cannot follow an optional one.", + parameter.name.value + )) + .with_annotation(Annotation::primary(parameter.span()).with_message("Required parameter here.")) + .with_note("Type-parameter defaults follow the same rule as value-parameter defaults: once a default is given, every later entry must also have a default.") + .with_help("Move parameters with defaults to the end of the list, or give this parameter a default."), + ); + } + + check_top_level_self_reference(parameter, context); + } +} + +fn check_top_level_self_reference(parameter: &GenericParameter, context: &mut Context<'_, '_, '_>) { + let name = parameter.name.value; + + if let Some(bound) = ¶meter.bound + && hint_is_bare_local(&bound.hint, name) + { + context.report( + Issue::error(format!("Type parameter `{name}` cannot reference itself at the top level of its bound.")) + .with_annotation(Annotation::primary(bound.hint.span()).with_message("Top-level self-reference here.")) + .with_note("A bare recursive reference would have no fixed point and would loop the resolver forever.") + .with_help("Nest the recursive reference inside another type's arguments, e.g. `T : Comparable`."), + ); + } + + if let Some(default) = ¶meter.default + && hint_is_bare_local(&default.hint, name) + { + context.report( + Issue::error(format!("Type parameter `{name}` cannot reference itself at the top level of its default.")) + .with_annotation(Annotation::primary(default.hint.span()).with_message("Top-level self-reference here.")) + .with_note("Defaults must resolve in a single pass at instantiation time; a bare self-reference cannot terminate.") + .with_help("Nest the recursive reference inside another type's arguments, or remove the default."), + ); + } + + if let Some(variance) = ¶meter.variance { + let (label, span) = match variance { + GenericVariance::Covariant(span) => ("Covariant", *span), + GenericVariance::Contravariant(span) => ("Contravariant", *span), + }; + + if let Some(bound) = ¶meter.bound + && hint_mentions_local(&bound.hint, parameter.name.value) + { + context.report( + Issue::error(format!("{label} type parameter `{}` cannot appear in its own bound.", parameter.name.value)) + .with_annotation(Annotation::primary(span).with_message("Variance marker here.")) + .with_annotation( + Annotation::secondary(bound.hint.span()).with_message("Self-reference inside its own bound."), + ) + .with_note("Bounds are invariant positions: a non-invariant type parameter cannot appear inside its own bound expression.") + .with_help("Drop the variance marker, or remove the self-reference from the bound."), + ); + } + + if let Some(default) = ¶meter.default + && hint_mentions_local(&default.hint, parameter.name.value) + { + context.report( + Issue::error(format!( + "{label} type parameter `{}` cannot appear in its own default.", + parameter.name.value + )) + .with_annotation(Annotation::primary(span).with_message("Variance marker here.")) + .with_annotation( + Annotation::secondary(default.hint.span()).with_message("Self-reference inside its own default."), + ) + .with_note("Defaults are invariant positions: a non-invariant type parameter cannot appear inside its own default expression.") + .with_help("Drop the variance marker, or remove the self-reference from the default."), + ); + } + } +} + +fn hint_is_bare_local(hint: &Hint, name: &str) -> bool { + match hint { + Hint::Identifier(identifier) => identifier.value().eq_ignore_ascii_case(name), + _ => false, + } +} + +fn hint_mentions_local(hint: &Hint, name: &str) -> bool { + match hint { + Hint::Identifier(id) => id.value().eq_ignore_ascii_case(name), + Hint::Parenthesized(p) => hint_mentions_local(p.hint, name), + Hint::Nullable(n) => hint_mentions_local(n.hint, name), + Hint::Union(u) => hint_mentions_local(u.left, name) || hint_mentions_local(u.right, name), + Hint::Intersection(i) => hint_mentions_local(i.left, name) || hint_mentions_local(i.right, name), + Hint::Generic(g) => { + hint_mentions_local(g.base, name) + || g.arguments.arguments.iter().any(|argument| hint_mentions_local(argument, name)) + } + _ => false, + } +} + +#[inline] +pub fn check_generic_argument_list(list: &GenericArgumentList, context: &mut Context<'_, '_, '_>) { + if !context.version.is_supported(Feature::BoundErasedGenericTypes) { + context.report( + Issue::error("Generic type arguments are only available in PHP 8.6 and above.") + .with_annotation(Annotation::primary(list.span()).with_message("Generic argument list used here.")) + .with_note("PHP recognises type arguments on named types (e.g. `Box`, `Map`) starting in PHP 8.6.") + .with_help("Upgrade to PHP 8.6 or above to use generic type arguments."), + ); + return; + } + + if list.arguments.as_slice().len() > MAX_GENERIC_LIST_LEN { + context.report( + Issue::error(format!( + "Generic argument list has {} entries; the maximum is {MAX_GENERIC_LIST_LEN}.", + list.arguments.as_slice().len() + )) + .with_annotation(Annotation::primary(list.span()).with_message("Too many type arguments.")) + .with_note("PHP caps every generic argument list at 127 entries.") + .with_help("Reduce the number of type arguments on this use site to 127 or fewer."), + ); + } +} + +#[inline] +pub fn check_turbofish(turbofish: &Turbofish, context: &mut Context<'_, '_, '_>) { + if !context.version.is_supported(Feature::BoundErasedGenericTypes) { + context.report( + Issue::error("Turbofish (`::<...>`) is only available in PHP 8.6 and above.") + .with_annotation(Annotation::primary(turbofish.span()).with_message("Turbofish used here.")) + .with_note("PHP recognises the turbofish call-site type-argument syntax starting in PHP 8.6.") + .with_help("Upgrade to PHP 8.6 or above to use turbofish call-site type arguments."), + ); + return; + } + + if turbofish.arguments.as_slice().len() > MAX_GENERIC_LIST_LEN { + context.report( + Issue::error(format!( + "Turbofish argument list has {} entries; the maximum is {MAX_GENERIC_LIST_LEN}.", + turbofish.arguments.as_slice().len() + )) + .with_annotation(Annotation::primary(turbofish.span()).with_message("Too many type arguments.")) + .with_note("PHP caps every generic argument list at 127 entries.") + .with_help("Reduce the number of type arguments at this call site to 127 or fewer."), + ); + } +} + +#[inline] +pub fn check_generic_hint_base(hint: &Hint, args_span: mago_span::Span, context: &mut Context<'_, '_, '_>) { + match hint { + Hint::Array(_) => { + context.report( + Issue::error("`array<...>` is not supported; PHP does not accept type arguments on `array`.") + .with_annotation(Annotation::primary(args_span).with_message("Type arguments are not allowed here.")) + .with_note("PHP's `array` is both a hash map and a vector at runtime, with no parametric shape that maps cleanly onto ``, so the engine does not recognise type arguments on it.") + .with_help("Use a parametric collection class instead, or describe the array shape with a docblock."), + ); + } + Hint::Iterable(_) => { + context.report( + Issue::error("`iterable<...>` is not supported; PHP does not accept type arguments on `iterable`.") + .with_annotation(Annotation::primary(args_span).with_message("Type arguments are not allowed here.")) + .with_note("`iterable` is the union `array | Traversable`, and the two branches have different key-type constraints, so the engine does not recognise type arguments on it.") + .with_help("Use a more specific traversable type instead, or describe the iterable shape with a docblock."), + ); + } + _ => {} + } +} diff --git a/crates/semantics/src/internal/checker/mod.rs b/crates/semantics/src/internal/checker/mod.rs index 02fb14e89..630a75588 100644 --- a/crates/semantics/src/internal/checker/mod.rs +++ b/crates/semantics/src/internal/checker/mod.rs @@ -32,6 +32,7 @@ pub mod constant; pub mod control_flow; pub mod expression; pub mod function_like; +pub mod generic; pub mod hint; pub mod literal; pub mod partial_application; diff --git a/crates/semantics/src/internal/mod.rs b/crates/semantics/src/internal/mod.rs index c9ad74e9d..db91d3889 100644 --- a/crates/semantics/src/internal/mod.rs +++ b/crates/semantics/src/internal/mod.rs @@ -22,7 +22,11 @@ use mago_syntax::ast::List; use mago_syntax::ast::Literal; use mago_syntax::ast::Match; use mago_syntax::ast::Namespace; +use mago_syntax::ast::GenericArgumentList; +use mago_syntax::ast::GenericHint; +use mago_syntax::ast::GenericParameterList; use mago_syntax::ast::PartialApplication; +use mago_syntax::ast::Turbofish; use mago_syntax::ast::Pipe; use mago_syntax::ast::Program; use mago_syntax::ast::Statement; @@ -208,6 +212,34 @@ impl<'ast, 'arena> Walker<'ast, 'arena, Context<'_, 'ast, 'arena>> for CheckingW checker::partial_application::check_partial_application(partial_application, context); } + #[inline] + fn walk_in_generic_parameter_list( + &self, + list: &'ast GenericParameterList<'arena>, + context: &mut Context<'_, 'ast, 'arena>, + ) { + checker::generic::check_generic_parameter_list(list, context); + } + + #[inline] + fn walk_in_generic_argument_list( + &self, + list: &'ast GenericArgumentList<'arena>, + context: &mut Context<'_, 'ast, 'arena>, + ) { + checker::generic::check_generic_argument_list(list, context); + } + + #[inline] + fn walk_in_turbofish(&self, turbofish: &'ast Turbofish<'arena>, context: &mut Context<'_, 'ast, 'arena>) { + checker::generic::check_turbofish(turbofish, context); + } + + #[inline] + fn walk_in_generic_hint(&self, generic_hint: &'ast GenericHint<'arena>, context: &mut Context<'_, 'ast, 'arena>) { + checker::generic::check_generic_hint_base(generic_hint.base, generic_hint.arguments.span(), context); + } + #[inline] fn walk_in_list(&self, list: &'ast List<'arena>, context: &mut Context<'_, 'ast, 'arena>) { checker::array::check_list(list, context); diff --git a/crates/syntax/src/ast/ast/attribute.rs b/crates/syntax/src/ast/ast/attribute.rs index 3d9a542c7..f2a09abf9 100644 --- a/crates/syntax/src/ast/ast/attribute.rs +++ b/crates/syntax/src/ast/ast/attribute.rs @@ -4,6 +4,7 @@ use mago_span::HasSpan; use mago_span::Span; use crate::ast::ast::argument::ArgumentList; +use crate::ast::ast::generic::Turbofish; use crate::ast::ast::identifier::Identifier; use crate::ast::sequence::TokenSeparatedSequence; @@ -23,6 +24,7 @@ pub struct AttributeList<'arena> { #[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, PartialOrd, Ord)] pub struct Attribute<'arena> { pub name: Identifier<'arena>, + pub turbofish: Option>, pub argument_list: Option>, } diff --git a/crates/syntax/src/ast/ast/call.rs b/crates/syntax/src/ast/ast/call.rs index 5a536f048..96d6d20a7 100644 --- a/crates/syntax/src/ast/ast/call.rs +++ b/crates/syntax/src/ast/ast/call.rs @@ -7,6 +7,7 @@ use mago_span::Span; use crate::ast::ast::argument::ArgumentList; use crate::ast::ast::class_like::member::ClassLikeMemberSelector; use crate::ast::ast::expression::Expression; +use crate::ast::ast::generic::Turbofish; #[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, PartialOrd, Ord, Display)] #[serde(tag = "type", content = "value")] @@ -20,6 +21,7 @@ pub enum Call<'arena> { #[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, PartialOrd, Ord)] pub struct FunctionCall<'arena> { pub function: &'arena Expression<'arena>, + pub turbofish: Option>, pub argument_list: ArgumentList<'arena>, } @@ -28,6 +30,7 @@ pub struct MethodCall<'arena> { pub object: &'arena Expression<'arena>, pub arrow: Span, pub method: ClassLikeMemberSelector<'arena>, + pub turbofish: Option>, pub argument_list: ArgumentList<'arena>, } @@ -36,6 +39,7 @@ pub struct NullSafeMethodCall<'arena> { pub object: &'arena Expression<'arena>, pub question_mark_arrow: Span, pub method: ClassLikeMemberSelector<'arena>, + pub turbofish: Option>, pub argument_list: ArgumentList<'arena>, } @@ -44,6 +48,7 @@ pub struct StaticMethodCall<'arena> { pub class: &'arena Expression<'arena>, pub double_colon: Span, pub method: ClassLikeMemberSelector<'arena>, + pub turbofish: Option>, pub argument_list: ArgumentList<'arena>, } diff --git a/crates/syntax/src/ast/ast/class_like/inheritance.rs b/crates/syntax/src/ast/ast/class_like/inheritance.rs index 4aa57c833..30eaee655 100644 --- a/crates/syntax/src/ast/ast/class_like/inheritance.rs +++ b/crates/syntax/src/ast/ast/class_like/inheritance.rs @@ -3,7 +3,7 @@ use serde::Serialize; use mago_span::HasSpan; use mago_span::Span; -use crate::ast::ast::identifier::Identifier; +use crate::ast::ast::generic::ClassLikeReference; use crate::ast::ast::keyword::Keyword; use crate::ast::sequence::TokenSeparatedSequence; use crate::ast::sequence::TokenSeparatedSequenceExt; @@ -20,7 +20,7 @@ use crate::ast::sequence::TokenSeparatedSequenceExt; #[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, PartialOrd, Ord)] pub struct Implements<'arena> { pub implements: Keyword<'arena>, - pub types: TokenSeparatedSequence<'arena, Identifier<'arena>>, + pub types: TokenSeparatedSequence<'arena, ClassLikeReference<'arena>>, } /// Represents `extends` keyword with one or more types. @@ -41,7 +41,7 @@ pub struct Implements<'arena> { #[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, PartialOrd, Ord)] pub struct Extends<'arena> { pub extends: Keyword<'arena>, - pub types: TokenSeparatedSequence<'arena, Identifier<'arena>>, + pub types: TokenSeparatedSequence<'arena, ClassLikeReference<'arena>>, } impl HasSpan for Implements<'_> { diff --git a/crates/syntax/src/ast/ast/class_like/method.rs b/crates/syntax/src/ast/ast/class_like/method.rs index 048de4a94..65806ce64 100644 --- a/crates/syntax/src/ast/ast/class_like/method.rs +++ b/crates/syntax/src/ast/ast/class_like/method.rs @@ -8,6 +8,7 @@ use crate::ast::ast::attribute::AttributeList; use crate::ast::ast::block::Block; use crate::ast::ast::function_like::parameter::FunctionLikeParameterList; use crate::ast::ast::function_like::r#return::FunctionLikeReturnTypeHint; +use crate::ast::ast::generic::GenericParameterList; use crate::ast::ast::identifier::LocalIdentifier; use crate::ast::ast::keyword::Keyword; use crate::ast::ast::modifier::Modifier; @@ -31,6 +32,7 @@ pub struct Method<'arena> { pub function: Keyword<'arena>, pub ampersand: Option, pub name: LocalIdentifier<'arena>, + pub generic_parameters: Option>, pub parameter_list: FunctionLikeParameterList<'arena>, pub return_type_hint: Option>, pub body: MethodBody<'arena>, diff --git a/crates/syntax/src/ast/ast/class_like/mod.rs b/crates/syntax/src/ast/ast/class_like/mod.rs index 75c923e24..5bf95a6a5 100644 --- a/crates/syntax/src/ast/ast/class_like/mod.rs +++ b/crates/syntax/src/ast/ast/class_like/mod.rs @@ -8,6 +8,7 @@ use crate::ast::ast::attribute::AttributeList; use crate::ast::ast::class_like::inheritance::Extends; use crate::ast::ast::class_like::inheritance::Implements; use crate::ast::ast::class_like::member::ClassLikeMember; +use crate::ast::ast::generic::GenericParameterList; use crate::ast::ast::identifier::LocalIdentifier; use crate::ast::ast::keyword::Keyword; use crate::ast::ast::modifier::Modifier; @@ -36,6 +37,7 @@ pub struct Interface<'arena> { pub attribute_lists: Sequence<'arena, AttributeList<'arena>>, pub interface: Keyword<'arena>, pub name: LocalIdentifier<'arena>, + pub generic_parameters: Option>, pub extends: Option>, pub left_brace: Span, pub members: Sequence<'arena, ClassLikeMember<'arena>>, @@ -62,6 +64,7 @@ pub struct Class<'arena> { pub modifiers: Sequence<'arena, Modifier<'arena>>, pub class: Keyword<'arena>, pub name: LocalIdentifier<'arena>, + pub generic_parameters: Option>, pub extends: Option>, pub implements: Option>, pub left_brace: Span, @@ -115,6 +118,7 @@ pub struct Trait<'arena> { pub attribute_lists: Sequence<'arena, AttributeList<'arena>>, pub r#trait: Keyword<'arena>, pub name: LocalIdentifier<'arena>, + pub generic_parameters: Option>, pub left_brace: Span, pub members: Sequence<'arena, ClassLikeMember<'arena>>, pub right_brace: Span, diff --git a/crates/syntax/src/ast/ast/class_like/trait_use.rs b/crates/syntax/src/ast/ast/class_like/trait_use.rs index 370d1831a..19e601815 100644 --- a/crates/syntax/src/ast/ast/class_like/trait_use.rs +++ b/crates/syntax/src/ast/ast/class_like/trait_use.rs @@ -4,6 +4,7 @@ use strum::Display; use mago_span::HasSpan; use mago_span::Span; +use crate::ast::ast::generic::ClassLikeReference; use crate::ast::ast::identifier::Identifier; use crate::ast::ast::identifier::LocalIdentifier; use crate::ast::ast::keyword::Keyword; @@ -15,7 +16,7 @@ use crate::ast::sequence::TokenSeparatedSequence; #[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, PartialOrd, Ord)] pub struct TraitUse<'arena> { pub r#use: Keyword<'arena>, - pub trait_names: TokenSeparatedSequence<'arena, Identifier<'arena>>, + pub trait_names: TokenSeparatedSequence<'arena, ClassLikeReference<'arena>>, pub specification: TraitUseSpecification<'arena>, } diff --git a/crates/syntax/src/ast/ast/function_like/arrow_function.rs b/crates/syntax/src/ast/ast/function_like/arrow_function.rs index 3cd87f344..cef601665 100644 --- a/crates/syntax/src/ast/ast/function_like/arrow_function.rs +++ b/crates/syntax/src/ast/ast/function_like/arrow_function.rs @@ -7,6 +7,7 @@ use crate::ast::ast::attribute::AttributeList; use crate::ast::ast::expression::Expression; use crate::ast::ast::function_like::parameter::FunctionLikeParameterList; use crate::ast::ast::function_like::r#return::FunctionLikeReturnTypeHint; +use crate::ast::ast::generic::GenericParameterList; use crate::ast::ast::keyword::Keyword; use crate::ast::sequence::Sequence; @@ -25,6 +26,7 @@ pub struct ArrowFunction<'arena> { pub r#static: Option>, pub r#fn: Keyword<'arena>, pub ampersand: Option, + pub generic_parameters: Option>, pub parameter_list: FunctionLikeParameterList<'arena>, pub return_type_hint: Option>, pub arrow: Span, diff --git a/crates/syntax/src/ast/ast/function_like/closure.rs b/crates/syntax/src/ast/ast/function_like/closure.rs index aab92ce18..ff6a7999d 100644 --- a/crates/syntax/src/ast/ast/function_like/closure.rs +++ b/crates/syntax/src/ast/ast/function_like/closure.rs @@ -7,6 +7,7 @@ use crate::ast::ast::attribute::AttributeList; use crate::ast::ast::block::Block; use crate::ast::ast::function_like::parameter::FunctionLikeParameterList; use crate::ast::ast::function_like::r#return::FunctionLikeReturnTypeHint; +use crate::ast::ast::generic::GenericParameterList; use crate::ast::ast::keyword::Keyword; use crate::ast::ast::variable::DirectVariable; use crate::ast::sequence::Sequence; @@ -18,6 +19,7 @@ pub struct Closure<'arena> { pub r#static: Option>, pub function: Keyword<'arena>, pub ampersand: Option, + pub generic_parameters: Option>, pub parameter_list: FunctionLikeParameterList<'arena>, pub use_clause: Option>, pub return_type_hint: Option>, diff --git a/crates/syntax/src/ast/ast/function_like/function.rs b/crates/syntax/src/ast/ast/function_like/function.rs index 7ed08b3d5..2edc17dbe 100644 --- a/crates/syntax/src/ast/ast/function_like/function.rs +++ b/crates/syntax/src/ast/ast/function_like/function.rs @@ -7,6 +7,7 @@ use crate::ast::ast::attribute::AttributeList; use crate::ast::ast::block::Block; use crate::ast::ast::function_like::parameter::FunctionLikeParameterList; use crate::ast::ast::function_like::r#return::FunctionLikeReturnTypeHint; +use crate::ast::ast::generic::GenericParameterList; use crate::ast::ast::identifier::LocalIdentifier; use crate::ast::ast::keyword::Keyword; use crate::ast::sequence::Sequence; @@ -28,6 +29,7 @@ pub struct Function<'arena> { pub function: Keyword<'arena>, pub ampersand: Option, pub name: LocalIdentifier<'arena>, + pub generic_parameters: Option>, pub parameter_list: FunctionLikeParameterList<'arena>, pub return_type_hint: Option>, pub body: Block<'arena>, diff --git a/crates/syntax/src/ast/ast/generic.rs b/crates/syntax/src/ast/ast/generic.rs new file mode 100644 index 000000000..e22e688e2 --- /dev/null +++ b/crates/syntax/src/ast/ast/generic.rs @@ -0,0 +1,159 @@ +use serde::Serialize; +use strum::Display; + +use mago_span::HasSpan; +use mago_span::Span; + +use crate::ast::Identifier; +use crate::ast::ast::identifier::LocalIdentifier; +use crate::ast::ast::type_hint::Hint; +use crate::ast::sequence::TokenSeparatedSequence; + +/// Variance marker on a generic type parameter. +/// +/// `+T` is covariant, `-T` is contravariant. An absent marker means invariant. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, PartialOrd, Ord, Display)] +#[serde(tag = "type", content = "value")] +pub enum GenericVariance { + Covariant(Span), + Contravariant(Span), +} + +impl HasSpan for GenericVariance { + fn span(&self) -> Span { + match self { + GenericVariance::Covariant(span) | GenericVariance::Contravariant(span) => *span, + } + } +} + +/// The upper-bound clause of a generic parameter: `: `. +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, PartialOrd, Ord)] +pub struct GenericParameterBound<'arena> { + pub colon: Span, + pub hint: Hint<'arena>, +} + +impl HasSpan for GenericParameterBound<'_> { + fn span(&self) -> Span { + self.colon.join(self.hint.span()) + } +} + +/// The default-argument clause of a generic parameter: `= `. +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, PartialOrd, Ord)] +pub struct GenericParameterDefault<'arena> { + pub equals: Span, + pub hint: Hint<'arena>, +} + +impl HasSpan for GenericParameterDefault<'_> { + fn span(&self) -> Span { + self.equals.join(self.hint.span()) + } +} + +/// A single generic type parameter on a declaration. +/// +/// Example: `+T : Stringable = string` +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, PartialOrd, Ord)] +pub struct GenericParameter<'arena> { + pub variance: Option, + pub name: LocalIdentifier<'arena>, + pub bound: Option>, + pub default: Option>, +} + +impl HasSpan for GenericParameter<'_> { + fn span(&self) -> Span { + let start = self.variance.as_ref().map(HasSpan::span).unwrap_or_else(|| self.name.span()); + let end = self + .default + .as_ref() + .map(HasSpan::span) + .or_else(|| self.bound.as_ref().map(HasSpan::span)) + .unwrap_or_else(|| self.name.span()); + + start.join(end) + } +} + +/// The comma-separated generic parameter list on a declaration. +/// +/// Example: `` +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, PartialOrd, Ord)] +pub struct GenericParameterList<'arena> { + pub less_than: Span, + pub parameters: TokenSeparatedSequence<'arena, GenericParameter<'arena>>, + pub greater_than: Span, +} + +impl HasSpan for GenericParameterList<'_> { + fn span(&self) -> Span { + self.less_than.join(self.greater_than) + } +} + +/// The comma-separated generic argument list at a type-use site. +/// +/// Example: `` in `Map`. +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, PartialOrd, Ord)] +pub struct GenericArgumentList<'arena> { + pub less_than: Span, + pub arguments: TokenSeparatedSequence<'arena, Hint<'arena>>, + pub greater_than: Span, +} + +impl HasSpan for GenericArgumentList<'_> { + fn span(&self) -> Span { + self.less_than.join(self.greater_than) + } +} + +/// A named type with type arguments: `Box`, `self`, `static`, `parent`. +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, PartialOrd, Ord)] +pub struct GenericHint<'arena> { + pub base: &'arena Hint<'arena>, + pub arguments: GenericArgumentList<'arena>, +} + +impl HasSpan for GenericHint<'_> { + fn span(&self) -> Span { + self.base.span().join(self.arguments.span()) + } +} + +/// A reference to a named class-like (class, interface, trait), optionally +/// carrying a generic argument list. +/// +/// Used in `extends`, `implements`, and `use Trait;` clauses. +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, PartialOrd, Ord)] +pub struct ClassLikeReference<'arena> { + pub name: Identifier<'arena>, + pub generic_arguments: Option>, +} + +impl HasSpan for ClassLikeReference<'_> { + fn span(&self) -> Span { + match &self.generic_arguments { + Some(args) => self.name.span().join(args.span()), + None => self.name.span(), + } + } +} + +/// Turbofish: a call-site type-argument list, introduced by the `::<` token. +/// +/// Example: `Box::` in `new Box::($v)`. +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, PartialOrd, Ord)] +pub struct Turbofish<'arena> { + pub turbofish: Span, + pub arguments: TokenSeparatedSequence<'arena, Hint<'arena>>, + pub greater_than: Span, +} + +impl HasSpan for Turbofish<'_> { + fn span(&self) -> Span { + self.turbofish.join(self.greater_than) + } +} diff --git a/crates/syntax/src/ast/ast/instantiation.rs b/crates/syntax/src/ast/ast/instantiation.rs index 9f924d905..daa20e27c 100644 --- a/crates/syntax/src/ast/ast/instantiation.rs +++ b/crates/syntax/src/ast/ast/instantiation.rs @@ -5,12 +5,14 @@ use mago_span::Span; use crate::ast::ast::argument::ArgumentList; use crate::ast::ast::expression::Expression; +use crate::ast::ast::generic::Turbofish; use crate::ast::ast::keyword::Keyword; #[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, PartialOrd, Ord)] pub struct Instantiation<'arena> { pub new: Keyword<'arena>, pub class: &'arena Expression<'arena>, + pub turbofish: Option>, pub argument_list: Option>, } diff --git a/crates/syntax/src/ast/ast/mod.rs b/crates/syntax/src/ast/ast/mod.rs index 2110646c2..0a4511be2 100644 --- a/crates/syntax/src/ast/ast/mod.rs +++ b/crates/syntax/src/ast/ast/mod.rs @@ -130,6 +130,15 @@ pub use crate::ast::ast::function_like::parameter::FunctionLikeParameter; pub use crate::ast::ast::function_like::parameter::FunctionLikeParameterDefaultValue; pub use crate::ast::ast::function_like::parameter::FunctionLikeParameterList; pub use crate::ast::ast::function_like::r#return::FunctionLikeReturnTypeHint; +pub use crate::ast::ast::generic::ClassLikeReference; +pub use crate::ast::ast::generic::GenericArgumentList; +pub use crate::ast::ast::generic::GenericHint; +pub use crate::ast::ast::generic::GenericParameter; +pub use crate::ast::ast::generic::GenericParameterBound; +pub use crate::ast::ast::generic::GenericParameterDefault; +pub use crate::ast::ast::generic::GenericParameterList; +pub use crate::ast::ast::generic::GenericVariance; +pub use crate::ast::ast::generic::Turbofish; pub use crate::ast::ast::global::Global; pub use crate::ast::ast::goto::Goto; pub use crate::ast::ast::goto::Label; @@ -245,6 +254,7 @@ pub mod declare; pub mod echo; pub mod expression; pub mod function_like; +pub mod generic; pub mod global; pub mod goto; pub mod halt_compiler; diff --git a/crates/syntax/src/ast/ast/partial_application.rs b/crates/syntax/src/ast/ast/partial_application.rs index 65ead971f..952dbc205 100644 --- a/crates/syntax/src/ast/ast/partial_application.rs +++ b/crates/syntax/src/ast/ast/partial_application.rs @@ -7,6 +7,7 @@ use mago_span::Span; use crate::ast::ast::argument::PartialArgumentList; use crate::ast::ast::class_like::member::ClassLikeMemberSelector; use crate::ast::ast::expression::Expression; +use crate::ast::ast::generic::Turbofish; #[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, PartialOrd, Ord, Display)] #[serde(tag = "type", content = "value")] @@ -19,6 +20,7 @@ pub enum PartialApplication<'arena> { #[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, PartialOrd, Ord)] pub struct FunctionPartialApplication<'arena> { pub function: &'arena Expression<'arena>, + pub turbofish: Option>, pub argument_list: PartialArgumentList<'arena>, } @@ -27,6 +29,7 @@ pub struct MethodPartialApplication<'arena> { pub object: &'arena Expression<'arena>, pub arrow: Span, pub method: ClassLikeMemberSelector<'arena>, + pub turbofish: Option>, pub argument_list: PartialArgumentList<'arena>, } @@ -35,6 +38,7 @@ pub struct StaticMethodPartialApplication<'arena> { pub class: &'arena Expression<'arena>, pub double_colon: Span, pub method: ClassLikeMemberSelector<'arena>, + pub turbofish: Option>, pub argument_list: PartialArgumentList<'arena>, } diff --git a/crates/syntax/src/ast/ast/type_hint.rs b/crates/syntax/src/ast/ast/type_hint.rs index d8333d811..55c59fa1b 100644 --- a/crates/syntax/src/ast/ast/type_hint.rs +++ b/crates/syntax/src/ast/ast/type_hint.rs @@ -4,6 +4,7 @@ use strum::Display; use mago_span::HasSpan; use mago_span::Span; +use crate::ast::ast::generic::GenericHint; use crate::ast::ast::identifier::Identifier; use crate::ast::ast::identifier::LocalIdentifier; use crate::ast::ast::keyword::Keyword; @@ -25,6 +26,7 @@ pub enum Hint<'arena> { Nullable(NullableHint<'arena>), Union(UnionHint<'arena>), Intersection(IntersectionHint<'arena>), + Generic(GenericHint<'arena>), Null(Keyword<'arena>), True(Keyword<'arena>), False(Keyword<'arena>), @@ -249,6 +251,7 @@ impl HasSpan for Hint<'_> { Hint::Nullable(nullable) => nullable.span(), Hint::Union(union) => union.span(), Hint::Intersection(intersection) => intersection.span(), + Hint::Generic(generic) => generic.span(), Hint::Null(keyword) | Hint::True(keyword) | Hint::Static(keyword) diff --git a/crates/syntax/src/ast/node.rs b/crates/syntax/src/ast/node.rs index be6c5cf2b..80920dd8a 100644 --- a/crates/syntax/src/ast/node.rs +++ b/crates/syntax/src/ast/node.rs @@ -34,6 +34,7 @@ use crate::ast::ast::ClassLikeConstantSelector; use crate::ast::ast::ClassLikeMember; use crate::ast::ast::ClassLikeMemberExpressionSelector; use crate::ast::ast::ClassLikeMemberSelector; +use crate::ast::ast::ClassLikeReference; use crate::ast::ast::Clone; use crate::ast::ast::ClosingTag; use crate::ast::ast::Closure; @@ -86,6 +87,12 @@ use crate::ast::ast::FunctionLikeParameterDefaultValue; use crate::ast::ast::FunctionLikeParameterList; use crate::ast::ast::FunctionLikeReturnTypeHint; use crate::ast::ast::FunctionPartialApplication; +use crate::ast::ast::GenericArgumentList; +use crate::ast::ast::GenericHint; +use crate::ast::ast::GenericParameter; +use crate::ast::ast::GenericParameterBound; +use crate::ast::ast::GenericParameterDefault; +use crate::ast::ast::GenericParameterList; use crate::ast::ast::Global; use crate::ast::ast::Goto; use crate::ast::ast::HaltCompiler; @@ -204,6 +211,7 @@ use crate::ast::ast::TraitUseSpecification; use crate::ast::ast::Try; use crate::ast::ast::TryCatchClause; use crate::ast::ast::TryFinallyClause; +use crate::ast::ast::Turbofish; use crate::ast::ast::TypedUseItemList; use crate::ast::ast::TypedUseItemSequence; use crate::ast::ast::UnaryPostfix; @@ -372,6 +380,14 @@ pub enum NodeKind { FunctionLikeParameterDefaultValue, FunctionLikeParameterList, FunctionLikeReturnTypeHint, + ClassLikeReference, + GenericArgumentList, + GenericHint, + GenericParameter, + GenericParameterBound, + GenericParameterDefault, + GenericParameterList, + Turbofish, Global, Goto, Label, @@ -606,6 +622,14 @@ pub enum Node<'ast, 'arena> { FunctionLikeParameterDefaultValue(&'ast FunctionLikeParameterDefaultValue<'arena>), FunctionLikeParameterList(&'ast FunctionLikeParameterList<'arena>), FunctionLikeReturnTypeHint(&'ast FunctionLikeReturnTypeHint<'arena>), + ClassLikeReference(&'ast ClassLikeReference<'arena>), + GenericArgumentList(&'ast GenericArgumentList<'arena>), + GenericHint(&'ast GenericHint<'arena>), + GenericParameter(&'ast GenericParameter<'arena>), + GenericParameterBound(&'ast GenericParameterBound<'arena>), + GenericParameterDefault(&'ast GenericParameterDefault<'arena>), + GenericParameterList(&'ast GenericParameterList<'arena>), + Turbofish(&'ast Turbofish<'arena>), Global(&'ast Global<'arena>), Goto(&'ast Goto<'arena>), Label(&'ast Label<'arena>), @@ -913,6 +937,14 @@ impl<'ast, 'arena> Node<'ast, 'arena> { Self::FunctionLikeParameterDefaultValue(_) => NodeKind::FunctionLikeParameterDefaultValue, Self::FunctionLikeParameterList(_) => NodeKind::FunctionLikeParameterList, Self::FunctionLikeReturnTypeHint(_) => NodeKind::FunctionLikeReturnTypeHint, + Self::ClassLikeReference(_) => NodeKind::ClassLikeReference, + Self::GenericArgumentList(_) => NodeKind::GenericArgumentList, + Self::GenericHint(_) => NodeKind::GenericHint, + Self::GenericParameter(_) => NodeKind::GenericParameter, + Self::GenericParameterBound(_) => NodeKind::GenericParameterBound, + Self::GenericParameterDefault(_) => NodeKind::GenericParameterDefault, + Self::GenericParameterList(_) => NodeKind::GenericParameterList, + Self::Turbofish(_) => NodeKind::Turbofish, Self::Global(_) => NodeKind::Global, Self::Goto(_) => NodeKind::Goto, Self::Label(_) => NodeKind::Label, @@ -1107,6 +1139,9 @@ impl<'ast, 'arena> Node<'ast, 'arena> { Node::VariadicArrayElement(node) => f(Node::Expression(node.value)), Node::Attribute(node) => { f(Node::Identifier(&node.name)); + if let Some(turbofish) = &node.turbofish { + f(Node::Turbofish(turbofish)); + } if let Some(arguments) = &node.argument_list { f(Node::ArgumentList(arguments)); } @@ -1129,21 +1164,33 @@ impl<'ast, 'arena> Node<'ast, 'arena> { }, Node::FunctionCall(node) => { f(Node::Expression(node.function)); + if let Some(turbofish) = &node.turbofish { + f(Node::Turbofish(turbofish)); + } f(Node::ArgumentList(&node.argument_list)); } Node::MethodCall(node) => { f(Node::Expression(node.object)); f(Node::ClassLikeMemberSelector(&node.method)); + if let Some(turbofish) = &node.turbofish { + f(Node::Turbofish(turbofish)); + } f(Node::ArgumentList(&node.argument_list)); } Node::NullSafeMethodCall(node) => { f(Node::Expression(node.object)); f(Node::ClassLikeMemberSelector(&node.method)); + if let Some(turbofish) = &node.turbofish { + f(Node::Turbofish(turbofish)); + } f(Node::ArgumentList(&node.argument_list)); } Node::StaticMethodCall(node) => { f(Node::Expression(node.class)); f(Node::ClassLikeMemberSelector(&node.method)); + if let Some(turbofish) = &node.turbofish { + f(Node::Turbofish(turbofish)); + } f(Node::ArgumentList(&node.argument_list)); } Node::PartialApplication(node) => match node { @@ -1153,16 +1200,25 @@ impl<'ast, 'arena> Node<'ast, 'arena> { }, Node::FunctionPartialApplication(node) => { f(Node::Expression(node.function)); + if let Some(turbofish) = &node.turbofish { + f(Node::Turbofish(turbofish)); + } f(Node::PartialArgumentList(&node.argument_list)); } Node::MethodPartialApplication(node) => { f(Node::Expression(node.object)); f(Node::ClassLikeMemberSelector(&node.method)); + if let Some(turbofish) = &node.turbofish { + f(Node::Turbofish(turbofish)); + } f(Node::PartialArgumentList(&node.argument_list)); } Node::StaticMethodPartialApplication(node) => { f(Node::Expression(node.class)); f(Node::ClassLikeMemberSelector(&node.method)); + if let Some(turbofish) = &node.turbofish { + f(Node::Turbofish(turbofish)); + } f(Node::PartialArgumentList(&node.argument_list)); } Node::ClassLikeConstant(node) => { @@ -1208,13 +1264,19 @@ impl<'ast, 'arena> Node<'ast, 'arena> { Node::Extends(node) => { f(Node::Keyword(&node.extends)); for item in node.types.iter() { - f(Node::Identifier(item)); + f(Node::ClassLikeReference(item)); } } Node::Implements(node) => { f(Node::Keyword(&node.implements)); for item in node.types.iter() { - f(Node::Identifier(item)); + f(Node::ClassLikeReference(item)); + } + } + Node::ClassLikeReference(node) => { + f(Node::Identifier(&node.name)); + if let Some(generic_arguments) = &node.generic_arguments { + f(Node::GenericArgumentList(generic_arguments)); } } Node::ClassLikeConstantSelector(node) => match node { @@ -1244,15 +1306,22 @@ impl<'ast, 'arena> Node<'ast, 'arena> { for item in node.attribute_lists.iter() { f(Node::AttributeList(item)); } + for item in node.modifiers.iter() { f(Node::Modifier(item)); } + f(Node::Keyword(&node.function)); f(Node::LocalIdentifier(&node.name)); + if let Some(generic_parameters) = &node.generic_parameters { + f(Node::GenericParameterList(generic_parameters)); + } + f(Node::FunctionLikeParameterList(&node.parameter_list)); for item in node.return_type_hint.iter() { f(Node::FunctionLikeReturnTypeHint(item)); } + f(Node::MethodBody(&node.body)); } Node::MethodAbstractBody(_) => {} @@ -1264,15 +1333,19 @@ impl<'ast, 'arena> Node<'ast, 'arena> { for item in node.attribute_lists.iter() { f(Node::AttributeList(item)); } + for item in node.var.iter() { f(Node::Keyword(item)); } + for item in node.modifiers.iter() { f(Node::Modifier(item)); } + for item in node.hint.iter() { f(Node::Hint(item)); } + f(Node::PropertyItem(&node.item)); f(Node::PropertyHookList(&node.hook_list)); } @@ -1280,15 +1353,19 @@ impl<'ast, 'arena> Node<'ast, 'arena> { for item in node.attribute_lists.iter() { f(Node::AttributeList(item)); } + for item in node.var.iter() { f(Node::Keyword(item)); } + for item in node.modifiers.iter() { f(Node::Modifier(item)); } + for item in node.hint.iter() { f(Node::Hint(item)); } + for item in node.items.iter() { f(Node::PropertyItem(item)); } @@ -1308,13 +1385,16 @@ impl<'ast, 'arena> Node<'ast, 'arena> { for item in node.attribute_lists.iter() { f(Node::AttributeList(item)); } + for item in node.modifiers.iter() { f(Node::Modifier(item)); } + f(Node::LocalIdentifier(&node.name)); for item in node.parameter_list.iter() { f(Node::FunctionLikeParameterList(item)); } + f(Node::PropertyHookBody(&node.body)); } Node::PropertyHookAbstractBody(_) => {} @@ -1339,8 +1419,9 @@ impl<'ast, 'arena> Node<'ast, 'arena> { Node::TraitUse(node) => { f(Node::Keyword(&node.r#use)); for item in node.trait_names.iter() { - f(Node::Identifier(item)); + f(Node::ClassLikeReference(item)); } + f(Node::TraitUseSpecification(&node.specification)); } Node::TraitUseAbsoluteMethodReference(node) => { @@ -1356,6 +1437,7 @@ impl<'ast, 'arena> Node<'ast, 'arena> { for item in adaptation.trait_names.iter() { f(Node::Identifier(item)); } + f(Node::Terminator(&adaptation.terminator)); } TraitUseAdaptation::Alias(adaptation) => { @@ -1448,6 +1530,9 @@ impl<'ast, 'arena> Node<'ast, 'arena> { } f(Node::Keyword(&node.class)); f(Node::LocalIdentifier(&node.name)); + if let Some(generic_parameters) = &node.generic_parameters { + f(Node::GenericParameterList(generic_parameters)); + } for item in node.extends.iter() { f(Node::Extends(item)); } @@ -1483,6 +1568,9 @@ impl<'ast, 'arena> Node<'ast, 'arena> { } f(Node::Keyword(&node.interface)); f(Node::LocalIdentifier(&node.name)); + if let Some(generic_parameters) = &node.generic_parameters { + f(Node::GenericParameterList(generic_parameters)); + } for item in node.extends.iter() { f(Node::Extends(item)); } @@ -1494,8 +1582,13 @@ impl<'ast, 'arena> Node<'ast, 'arena> { for item in node.attribute_lists.iter() { f(Node::AttributeList(item)); } + f(Node::Keyword(&node.r#trait)); f(Node::LocalIdentifier(&node.name)); + if let Some(generic_parameters) = &node.generic_parameters { + f(Node::GenericParameterList(generic_parameters)); + } + for item in node.members.iter() { f(Node::ClassLikeMember(item)); } @@ -1833,14 +1926,21 @@ impl<'ast, 'arena> Node<'ast, 'arena> { for item in node.attribute_lists.iter() { f(Node::AttributeList(item)); } + if let Some(r#static) = &node.r#static { f(Node::Keyword(r#static)); } + f(Node::Keyword(&node.r#fn)); + if let Some(generic_parameters) = &node.generic_parameters { + f(Node::GenericParameterList(generic_parameters)); + } + f(Node::FunctionLikeParameterList(&node.parameter_list)); if let Some(return_type_hint) = &node.return_type_hint { f(Node::FunctionLikeReturnTypeHint(return_type_hint)); } + f(Node::Expression(node.expression)); } Node::Closure(node) => { @@ -1848,6 +1948,9 @@ impl<'ast, 'arena> Node<'ast, 'arena> { f(Node::AttributeList(item)); } f(Node::Keyword(&node.function)); + if let Some(generic_parameters) = &node.generic_parameters { + f(Node::GenericParameterList(generic_parameters)); + } f(Node::FunctionLikeParameterList(&node.parameter_list)); if let Some(use_clause) = &node.use_clause { f(Node::ClosureUseClause(use_clause)); @@ -1868,8 +1971,13 @@ impl<'ast, 'arena> Node<'ast, 'arena> { for item in node.attribute_lists.iter() { f(Node::AttributeList(item)); } + f(Node::Keyword(&node.function)); f(Node::LocalIdentifier(&node.name)); + if let Some(generic_parameters) = &node.generic_parameters { + f(Node::GenericParameterList(generic_parameters)); + } + f(Node::FunctionLikeParameterList(&node.parameter_list)); if let Some(return_type_hint) = &node.return_type_hint { f(Node::FunctionLikeReturnTypeHint(return_type_hint)); @@ -1932,6 +2040,10 @@ impl<'ast, 'arena> Node<'ast, 'arena> { f(Node::Keyword(&node.new)); f(Node::Expression(node.class)); + if let Some(turbofish) = &node.turbofish { + f(Node::Turbofish(turbofish)); + } + if let Some(argument_list) = &node.argument_list { f(Node::ArgumentList(argument_list)); } @@ -2327,6 +2439,7 @@ impl<'ast, 'arena> Node<'ast, 'arena> { Hint::Nullable(nullable_hint) => f(Node::NullableHint(nullable_hint)), Hint::Union(union_hint) => f(Node::UnionHint(union_hint)), Hint::Intersection(intersection_hint) => f(Node::IntersectionHint(intersection_hint)), + Hint::Generic(generic_hint) => f(Node::GenericHint(generic_hint)), Hint::Null(keyword) | Hint::True(keyword) | Hint::False(keyword) @@ -2345,6 +2458,37 @@ impl<'ast, 'arena> Node<'ast, 'arena> { | Hint::Mixed(local_identifier) | Hint::Iterable(local_identifier) => f(Node::LocalIdentifier(local_identifier)), }, + Node::GenericHint(node) => { + f(Node::Hint(node.base)); + f(Node::GenericArgumentList(&node.arguments)); + } + Node::GenericArgumentList(node) => { + for arg in node.arguments.iter() { + f(Node::Hint(arg)); + } + } + Node::GenericParameterList(node) => { + for param in node.parameters.iter() { + f(Node::GenericParameter(param)); + } + } + Node::GenericParameter(node) => { + f(Node::LocalIdentifier(&node.name)); + if let Some(bound) = &node.bound { + f(Node::GenericParameterBound(bound)); + } + + if let Some(default) = &node.default { + f(Node::GenericParameterDefault(default)); + } + } + Node::GenericParameterBound(node) => f(Node::Hint(&node.hint)), + Node::GenericParameterDefault(node) => f(Node::Hint(&node.hint)), + Node::Turbofish(node) => { + for arg in node.arguments.iter() { + f(Node::Hint(arg)); + } + } Node::IntersectionHint(node) => { f(Node::Hint(node.left)); f(Node::Hint(node.right)); @@ -2535,6 +2679,14 @@ impl HasSpan for Node<'_, '_> { Self::FunctionLikeParameterDefaultValue(node) => node.span(), Self::FunctionLikeParameterList(node) => node.span(), Self::FunctionLikeReturnTypeHint(node) => node.span(), + Self::ClassLikeReference(node) => node.span(), + Self::GenericArgumentList(node) => node.span(), + Self::GenericHint(node) => node.span(), + Self::GenericParameter(node) => node.span(), + Self::GenericParameterBound(node) => node.span(), + Self::GenericParameterDefault(node) => node.span(), + Self::GenericParameterList(node) => node.span(), + Self::Turbofish(node) => node.span(), Self::Global(node) => node.span(), Self::Goto(node) => node.span(), Self::Label(node) => node.span(), diff --git a/crates/syntax/src/lexer/mod.rs b/crates/syntax/src/lexer/mod.rs index 89e168784..7ec9c4066 100644 --- a/crates/syntax/src/lexer/mod.rs +++ b/crates/syntax/src/lexer/mod.rs @@ -385,6 +385,7 @@ impl<'input> Lexer<'input> { [b'<', b'<', b'='] => (TokenKind::LeftShiftEqual, 3), [b'>', b'>', b'='] => (TokenKind::RightShiftEqual, 3), [b'*', b'*', b'='] => (TokenKind::AsteriskAsteriskEqual, 3), + [b':', b':', b'<'] => (TokenKind::ColonColonLessThan, 3), [b'<', b'<', b'<'] if matches_start_of_heredoc_document(&self.input, 0) => { let (length, whitespaces, label_length) = read_start_of_heredoc_document(&self.input, false, 0); diff --git a/crates/syntax/src/macros.rs b/crates/syntax/src/macros.rs index c2c483172..c432a7f9e 100644 --- a/crates/syntax/src/macros.rs +++ b/crates/syntax/src/macros.rs @@ -228,6 +228,9 @@ macro_rules! T { ("::") => { $crate::token::TokenKind::ColonColon }; + ("::<") => { + $crate::token::TokenKind::ColonColonLessThan + }; ("==") => { $crate::token::TokenKind::EqualEqual }; diff --git a/crates/syntax/src/parser/internal/attribute.rs b/crates/syntax/src/parser/internal/attribute.rs index 7981d2bcd..41a30c1c2 100644 --- a/crates/syntax/src/parser/internal/attribute.rs +++ b/crates/syntax/src/parser/internal/attribute.rs @@ -24,6 +24,10 @@ impl<'arena> Parser<'_, 'arena> { } pub(crate) fn parse_attribute(&mut self) -> Result, ParseError> { - Ok(Attribute { name: self.parse_identifier()?, argument_list: self.parse_optional_argument_list()? }) + Ok(Attribute { + name: self.parse_identifier()?, + turbofish: self.parse_optional_turbofish()?, + argument_list: self.parse_optional_argument_list()?, + }) } } diff --git a/crates/syntax/src/parser/internal/class_like/inheritance.rs b/crates/syntax/src/parser/internal/class_like/inheritance.rs index 2e682e077..c52cb7ec8 100644 --- a/crates/syntax/src/parser/internal/class_like/inheritance.rs +++ b/crates/syntax/src/parser/internal/class_like/inheritance.rs @@ -1,4 +1,5 @@ use crate::T; +use crate::ast::ast::ClassLikeReference; use crate::ast::ast::Extends; use crate::ast::ast::Implements; use crate::ast::sequence::TokenSeparatedSequence; @@ -14,7 +15,7 @@ impl<'arena> Parser<'_, 'arena> { let mut types = self.new_vec(); let mut commas = self.new_vec(); loop { - types.push(self.parse_identifier()?); + types.push(self.parse_class_like_reference()?); match self.stream.peek_kind(0)? { Some(T![","]) => { @@ -39,7 +40,7 @@ impl<'arena> Parser<'_, 'arena> { let mut types = self.new_vec(); let mut commas = self.new_vec(); loop { - types.push(self.parse_identifier()?); + types.push(self.parse_class_like_reference()?); match self.stream.peek_kind(0)? { Some(T![","]) => { @@ -54,4 +55,12 @@ impl<'arena> Parser<'_, 'arena> { _ => None, }) } + + /// Parse `Name` or `Name` for use in `extends`, `implements`, + /// and `use Trait` clauses. + pub(crate) fn parse_class_like_reference(&mut self) -> Result, ParseError> { + let name = self.parse_identifier()?; + let generic_arguments = self.parse_optional_generic_argument_list()?; + Ok(ClassLikeReference { name, generic_arguments }) + } } diff --git a/crates/syntax/src/parser/internal/class_like/method.rs b/crates/syntax/src/parser/internal/class_like/method.rs index 1efd21e4b..9e3c2ccf6 100644 --- a/crates/syntax/src/parser/internal/class_like/method.rs +++ b/crates/syntax/src/parser/internal/class_like/method.rs @@ -20,6 +20,7 @@ impl<'arena> Parser<'_, 'arena> { function: self.expect_keyword(T!["function"])?, ampersand: if self.stream.is_at(T!["&"])? { Some(self.stream.eat_span(T!["&"])?) } else { None }, name: self.parse_local_identifier()?, + generic_parameters: self.parse_optional_generic_parameter_list()?, parameter_list: self.parse_function_like_parameter_list()?, return_type_hint: self.parse_optional_function_like_return_type_hint()?, body: self.parse_method_body()?, diff --git a/crates/syntax/src/parser/internal/class_like/mod.rs b/crates/syntax/src/parser/internal/class_like/mod.rs index 1b87e5a84..5779ff1cc 100644 --- a/crates/syntax/src/parser/internal/class_like/mod.rs +++ b/crates/syntax/src/parser/internal/class_like/mod.rs @@ -28,6 +28,7 @@ impl<'arena> Parser<'_, 'arena> { attribute_lists: attributes, interface: self.expect_keyword(T!["interface"])?, name: self.parse_local_identifier()?, + generic_parameters: self.parse_optional_generic_parameter_list()?, extends: self.parse_optional_extends()?, left_brace: self.stream.eat_span(T!["{"])?, members: { @@ -80,6 +81,7 @@ impl<'arena> Parser<'_, 'arena> { modifiers, class: self.expect_keyword(T!["class"])?, name: self.parse_local_identifier()?, + generic_parameters: self.parse_optional_generic_parameter_list()?, extends: self.parse_optional_extends()?, implements: self.parse_optional_implements()?, left_brace: self.stream.eat_span(T!["{"])?, @@ -163,6 +165,7 @@ impl<'arena> Parser<'_, 'arena> { attribute_lists: attributes, r#trait: self.expect_keyword(T!["trait"])?, name: self.parse_local_identifier()?, + generic_parameters: self.parse_optional_generic_parameter_list()?, left_brace: self.stream.eat_span(T!["{"])?, members: { let mut members = self.new_vec(); diff --git a/crates/syntax/src/parser/internal/class_like/trait_use.rs b/crates/syntax/src/parser/internal/class_like/trait_use.rs index 10a4fc173..8e0dd8f13 100644 --- a/crates/syntax/src/parser/internal/class_like/trait_use.rs +++ b/crates/syntax/src/parser/internal/class_like/trait_use.rs @@ -26,7 +26,7 @@ impl<'arena> Parser<'_, 'arena> { break; } - traits.push(self.parse_identifier()?); + traits.push(self.parse_class_like_reference()?); match self.stream.peek_kind(0)? { Some(T![","]) => { diff --git a/crates/syntax/src/parser/internal/clone.rs b/crates/syntax/src/parser/internal/clone.rs index 16ade5993..e0419916d 100644 --- a/crates/syntax/src/parser/internal/clone.rs +++ b/crates/syntax/src/parser/internal/clone.rs @@ -68,6 +68,7 @@ impl<'arena> Parser<'_, 'arena> { span: clone.span, value: clone.value, }))), + turbofish: None, argument_list: partial_args, }))); } @@ -86,6 +87,7 @@ impl<'arena> Parser<'_, 'arena> { span: clone.span, value: clone.value, }))), + turbofish: None, argument_list: partial_args.into_argument_list(self.arena), }))); } diff --git a/crates/syntax/src/parser/internal/construct.rs b/crates/syntax/src/parser/internal/construct.rs index 9e3bb2a42..416dfae5f 100644 --- a/crates/syntax/src/parser/internal/construct.rs +++ b/crates/syntax/src/parser/internal/construct.rs @@ -80,6 +80,7 @@ impl<'arena> Parser<'_, 'arena> { function: self.arena.alloc(Expression::Identifier(Identifier::Local( LocalIdentifier { span: exit.span, value: exit.value }, ))), + turbofish: None, argument_list: partial_args, }, ))); @@ -106,6 +107,7 @@ impl<'arena> Parser<'_, 'arena> { function: self.arena.alloc(Expression::Identifier(Identifier::Local( LocalIdentifier { span: die.span, value: die.value }, ))), + turbofish: None, argument_list: partial_args, }, ))); diff --git a/crates/syntax/src/parser/internal/expression.rs b/crates/syntax/src/parser/internal/expression.rs index 7eb297aca..c20f85afb 100644 --- a/crates/syntax/src/parser/internal/expression.rs +++ b/crates/syntax/src/parser/internal/expression.rs @@ -329,17 +329,37 @@ impl<'arena> Parser<'_, 'arena> { let operator = self.stream.lookahead(0)?.ok_or_else(|| self.stream.unexpected(None, &[]))?; Ok(self.arena.alloc(match operator.kind { + T!["::<"] => { + let turbofish = Some(self.parse_turbofish()?); + let partial_args = self.parse_partial_argument_list()?; + + if partial_args.has_placeholders() { + Expression::PartialApplication(PartialApplication::Function(FunctionPartialApplication { + function: lhs, + turbofish, + argument_list: partial_args, + })) + } else { + Expression::Call(Call::Function(FunctionCall { + function: lhs, + turbofish, + argument_list: partial_args.into_argument_list(self.arena), + })) + } + } T!["("] => { let partial_args = self.parse_partial_argument_list()?; if partial_args.has_placeholders() { Expression::PartialApplication(PartialApplication::Function(FunctionPartialApplication { function: lhs, + turbofish: None, argument_list: partial_args, })) } else { Expression::Call(Call::Function(FunctionCall { function: lhs, + turbofish: None, argument_list: partial_args.into_argument_list(self.arena), })) } @@ -365,9 +385,11 @@ impl<'arena> Parser<'_, 'arena> { T!["::"] => { let double_colon = self.stream.consume_span()?; let selector = self.parse_classlike_member_selector()?; + let has_turbofish = matches!(self.stream.peek_kind(0)?, Some(T!["::<"])); let current = self.stream.lookahead(0)?.ok_or_else(|| self.stream.unexpected(None, &[]))?; - if Precedence::CallDim > precedence && matches!(current.kind, T!["("]) { + if Precedence::CallDim > precedence && (has_turbofish || matches!(current.kind, T!["("])) { + let turbofish = if has_turbofish { Some(self.parse_turbofish()?) } else { None }; let partial_args = self.parse_partial_argument_list()?; if partial_args.has_placeholders() { @@ -376,6 +398,7 @@ impl<'arena> Parser<'_, 'arena> { class: lhs, double_colon, method: selector, + turbofish, argument_list: partial_args, }, )) @@ -384,6 +407,7 @@ impl<'arena> Parser<'_, 'arena> { class: lhs, double_colon, method: selector, + turbofish, argument_list: partial_args.into_argument_list(self.arena), })) } @@ -423,8 +447,12 @@ impl<'arena> Parser<'_, 'arena> { T!["->"] => { let arrow = self.stream.consume_span()?; let selector = self.parse_classlike_member_selector()?; + let has_turbofish = matches!(self.stream.peek_kind(0)?, Some(T!["::<"])); - if Precedence::CallDim > precedence && matches!(self.stream.peek_kind(0)?, Some(T!["("])) { + if Precedence::CallDim > precedence + && (has_turbofish || matches!(self.stream.peek_kind(0)?, Some(T!["("]))) + { + let turbofish = if has_turbofish { Some(self.parse_turbofish()?) } else { None }; let partial_args = self.parse_partial_argument_list()?; if partial_args.has_placeholders() { @@ -432,6 +460,7 @@ impl<'arena> Parser<'_, 'arena> { object: lhs, arrow, method: selector, + turbofish, argument_list: partial_args, })) } else { @@ -439,6 +468,7 @@ impl<'arena> Parser<'_, 'arena> { object: lhs, arrow, method: selector, + turbofish, argument_list: partial_args.into_argument_list(self.arena), })) } @@ -449,12 +479,17 @@ impl<'arena> Parser<'_, 'arena> { T!["?->"] => { let question_mark_arrow = self.stream.consume_span()?; let selector = self.parse_classlike_member_selector()?; + let has_turbofish = matches!(self.stream.peek_kind(0)?, Some(T!["::<"])); - if Precedence::CallDim > precedence && matches!(self.stream.peek_kind(0)?, Some(T!["("])) { + if Precedence::CallDim > precedence + && (has_turbofish || matches!(self.stream.peek_kind(0)?, Some(T!["("]))) + { + let turbofish = if has_turbofish { Some(self.parse_turbofish()?) } else { None }; Expression::Call(Call::NullSafeMethod(NullSafeMethodCall { object: lhs, question_mark_arrow, method: selector, + turbofish, argument_list: self.parse_argument_list()?, })) } else { diff --git a/crates/syntax/src/parser/internal/function_like/arrow_function.rs b/crates/syntax/src/parser/internal/function_like/arrow_function.rs index 2ba1abd2b..b5195dc73 100644 --- a/crates/syntax/src/parser/internal/function_like/arrow_function.rs +++ b/crates/syntax/src/parser/internal/function_like/arrow_function.rs @@ -15,6 +15,7 @@ impl<'arena> Parser<'_, 'arena> { r#static: self.maybe_expect_keyword(T!["static"])?, r#fn: self.expect_keyword(T!["fn"])?, ampersand: if self.stream.is_at(T!["&"])? { Some(self.stream.eat_span(T!["&"])?) } else { None }, + generic_parameters: self.parse_optional_generic_parameter_list()?, parameter_list: self.parse_function_like_parameter_list()?, return_type_hint: self.parse_optional_function_like_return_type_hint()?, arrow: self.stream.eat_span(T!["=>"])?, diff --git a/crates/syntax/src/parser/internal/function_like/closure.rs b/crates/syntax/src/parser/internal/function_like/closure.rs index 1d5422ac0..8d58c9482 100644 --- a/crates/syntax/src/parser/internal/function_like/closure.rs +++ b/crates/syntax/src/parser/internal/function_like/closure.rs @@ -17,6 +17,7 @@ impl<'arena> Parser<'_, 'arena> { r#static: self.maybe_expect_keyword(T!["static"])?, function: self.expect_keyword(T!["function"])?, ampersand: if self.stream.is_at(T!["&"])? { Some(self.stream.eat_span(T!["&"])?) } else { None }, + generic_parameters: self.parse_optional_generic_parameter_list()?, parameter_list: self.parse_function_like_parameter_list()?, use_clause: self.parse_optional_closure_use_clause()?, return_type_hint: self.parse_optional_function_like_return_type_hint()?, diff --git a/crates/syntax/src/parser/internal/function_like/function.rs b/crates/syntax/src/parser/internal/function_like/function.rs index 1e9e14b63..11e0baa34 100644 --- a/crates/syntax/src/parser/internal/function_like/function.rs +++ b/crates/syntax/src/parser/internal/function_like/function.rs @@ -15,6 +15,7 @@ impl<'arena> Parser<'_, 'arena> { function: self.expect_keyword(T!["function"])?, ampersand: if self.stream.is_at(T!["&"])? { Some(self.stream.eat_span(T!["&"])?) } else { None }, name: self.parse_local_identifier()?, + generic_parameters: self.parse_optional_generic_parameter_list()?, parameter_list: self.parse_function_like_parameter_list()?, return_type_hint: self.parse_optional_function_like_return_type_hint()?, body: self.parse_block()?, diff --git a/crates/syntax/src/parser/internal/generic.rs b/crates/syntax/src/parser/internal/generic.rs new file mode 100644 index 000000000..56055b8af --- /dev/null +++ b/crates/syntax/src/parser/internal/generic.rs @@ -0,0 +1,199 @@ +use mago_database::file::HasFileId; +use mago_span::Position; +use mago_span::Span; + +use crate::T; +use crate::ast::ast::GenericArgumentList; +use crate::ast::ast::GenericParameter; +use crate::ast::ast::GenericParameterBound; +use crate::ast::ast::GenericParameterDefault; +use crate::ast::ast::GenericParameterList; +use crate::ast::ast::GenericVariance; +use crate::ast::ast::Hint; +use crate::ast::ast::Turbofish; +use crate::ast::sequence::TokenSeparatedSequence; +use crate::error::ParseError; +use crate::parser::Parser; +use crate::token::TokenKind; + +impl<'arena> Parser<'_, 'arena> { + /// Returns `true` when the next significant token is a `<` and could + /// plausibly open a generic argument or parameter list at this position. + /// + /// Callers are expected to gate this on PHP-version support; the parser + /// itself is lenient and lets semantics reject pre-8.6 usage. + pub(crate) fn is_at_generic_open_angle(&mut self) -> Result { + Ok(matches!(self.stream.peek_kind(0)?, Some(T!["<"]))) + } + + /// Parse `` if the next token is `<`; otherwise `None`. + pub(crate) fn parse_optional_generic_parameter_list( + &mut self, + ) -> Result>, ParseError> { + if self.is_at_generic_open_angle()? { Ok(Some(self.parse_generic_parameter_list()?)) } else { Ok(None) } + } + + /// Parse `` given that the next token is `<`. + /// + /// Each entry is `[+|-] NAME [: Bound] [= Default]`. + pub(crate) fn parse_generic_parameter_list(&mut self) -> Result, ParseError> { + let less_than = self.stream.eat_span(T!["<"])?; + + let mut items = self.new_vec(); + let mut separators = self.new_vec(); + loop { + if self.is_at_close_angle()? { + break; + } + + match self.parse_generic_parameter() { + Ok(parameter) => items.push(parameter), + Err(err) => { + self.errors.push(err); + break; + } + } + + if matches!(self.stream.peek_kind(0)?, Some(T![","])) { + separators.push(self.stream.consume()?); + } else { + break; + } + } + + let greater_than = self.eat_close_angle()?; + + Ok(GenericParameterList { less_than, parameters: TokenSeparatedSequence::new(items, separators), greater_than }) + } + + fn parse_generic_parameter(&mut self) -> Result, ParseError> { + let variance = match self.stream.peek_kind(0)? { + Some(T!["+"]) => Some(GenericVariance::Covariant(self.stream.eat_span(T!["+"])?)), + Some(T!["-"]) => Some(GenericVariance::Contravariant(self.stream.eat_span(T!["-"])?)), + _ => None, + }; + + let name = self.parse_local_identifier()?; + + let bound = if matches!(self.stream.peek_kind(0)?, Some(T![":"])) { + let colon = self.stream.eat_span(T![":"])?; + let hint = self.parse_type_hint()?; + Some(GenericParameterBound { colon, hint }) + } else { + None + }; + + let default = if matches!(self.stream.peek_kind(0)?, Some(T!["="])) { + let equals = self.stream.eat_span(T!["="])?; + let hint = self.parse_type_hint()?; + Some(GenericParameterDefault { equals, hint }) + } else { + None + }; + + Ok(GenericParameter { variance, name, bound, default }) + } + + /// Parse `` if the next token is `<`; otherwise `None`. + /// + /// The type-hint parser handles this inline today, so this helper exists + /// for upcoming inheritance / `instanceof` / `catch` work that needs to + /// attach a generic argument list to a class-name reference. + #[allow(dead_code)] + pub(crate) fn parse_optional_generic_argument_list( + &mut self, + ) -> Result>, ParseError> { + if self.is_at_generic_open_angle()? { Ok(Some(self.parse_generic_argument_list()?)) } else { Ok(None) } + } + + /// Parse `` given that the next token is `<`. + pub(crate) fn parse_generic_argument_list(&mut self) -> Result, ParseError> { + let less_than = self.stream.eat_span(T!["<"])?; + let (arguments, greater_than) = self.parse_generic_argument_list_tail()?; + Ok(GenericArgumentList { less_than, arguments, greater_than }) + } + + /// Parse `::` if the next token is `::<`; otherwise `None`. + pub(crate) fn parse_optional_turbofish(&mut self) -> Result>, ParseError> { + if matches!(self.stream.peek_kind(0)?, Some(T!["::<"])) { Ok(Some(self.parse_turbofish()?)) } else { Ok(None) } + } + + /// Parse `::` given that the next token is `::<`. + pub(crate) fn parse_turbofish(&mut self) -> Result, ParseError> { + let turbofish = self.stream.eat_span(T!["::<"])?; + let (arguments, greater_than) = self.parse_generic_argument_list_tail()?; + Ok(Turbofish { turbofish, arguments, greater_than }) + } + + /// Parse the comma-separated body and closing `>` shared by every + /// generic-argument list shape (type-use lists and turbofish). + fn parse_generic_argument_list_tail( + &mut self, + ) -> Result<(TokenSeparatedSequence<'arena, Hint<'arena>>, Span), ParseError> { + let mut items = self.new_vec(); + let mut separators = self.new_vec(); + loop { + if self.is_at_close_angle()? { + break; + } + + match self.parse_type_hint() { + Ok(hint) => items.push(hint), + Err(err) => { + self.errors.push(err); + break; + } + } + + if matches!(self.stream.peek_kind(0)?, Some(T![","])) { + separators.push(self.stream.consume()?); + } else { + break; + } + } + + let greater_than = self.eat_close_angle()?; + Ok((TokenSeparatedSequence::new(items, separators), greater_than)) + } + + /// True if the next token closes a generic angle bracket: a real `>` or + /// the first half of a `>>` (right-shift), or a pending virtual `>` + /// recorded from a previous split. + pub(crate) fn is_at_close_angle(&mut self) -> Result { + if self.state.pending_close_angles > 0 { + return Ok(true); + } + + Ok(matches!(self.stream.peek_kind(0)?, Some(T![">"] | TokenKind::RightShift))) + } + + /// Consume one closing `>` token, splitting `>>` when needed so a nested + /// generic list can close with the second half on the next call. + pub(crate) fn eat_close_angle(&mut self) -> Result { + if self.state.pending_close_angles > 0 { + self.state.pending_close_angles -= 1; + let offset = self.state.pending_close_angle_offset; + let start = Position::new(offset); + let end = Position::new(offset + 1); + return Ok(Span::new(self.stream.file_id(), start, end)); + } + + match self.stream.peek_kind(0)? { + Some(T![">"]) => Ok(self.stream.eat_span(T![">"])?), + Some(TokenKind::RightShift) => { + let token = self.stream.consume()?; + let first_offset = token.start.offset; + let second_offset = first_offset + 1; + self.state.pending_close_angles = 1; + self.state.pending_close_angle_offset = second_offset; + let start = Position::new(first_offset); + let end = Position::new(second_offset); + Ok(Span::new(self.stream.file_id(), start, end)) + } + _ => { + let token = self.stream.lookahead(0)?; + Err(self.stream.unexpected(token, &[T![">"]])) + } + } + } +} diff --git a/crates/syntax/src/parser/internal/instantiation.rs b/crates/syntax/src/parser/internal/instantiation.rs index 83c7bf737..dad934e5c 100644 --- a/crates/syntax/src/parser/internal/instantiation.rs +++ b/crates/syntax/src/parser/internal/instantiation.rs @@ -9,6 +9,7 @@ impl<'arena> Parser<'_, 'arena> { Ok(Instantiation { new: self.expect_keyword(T!["new"])?, class: self.arena.alloc(self.parse_expression_with_precedence(Precedence::New)?), + turbofish: self.parse_optional_turbofish()?, argument_list: self.parse_optional_argument_list()?, }) } diff --git a/crates/syntax/src/parser/internal/mod.rs b/crates/syntax/src/parser/internal/mod.rs index dd149d6a0..fe77d65db 100644 --- a/crates/syntax/src/parser/internal/mod.rs +++ b/crates/syntax/src/parser/internal/mod.rs @@ -16,6 +16,7 @@ pub(crate) mod declare; pub(crate) mod echo; pub(crate) mod expression; pub(crate) mod function_like; +pub(crate) mod generic; pub(crate) mod global; pub(crate) mod goto; pub(crate) mod halt_compiler; diff --git a/crates/syntax/src/parser/internal/type_hint.rs b/crates/syntax/src/parser/internal/type_hint.rs index abc8607f5..881105d77 100644 --- a/crates/syntax/src/parser/internal/type_hint.rs +++ b/crates/syntax/src/parser/internal/type_hint.rs @@ -1,4 +1,5 @@ use crate::T; +use crate::ast::ast::GenericHint; use crate::ast::ast::Hint; use crate::ast::ast::IntersectionHint; use crate::ast::ast::NullableHint; @@ -86,6 +87,15 @@ impl<'arena> Parser<'_, 'arena> { } }; + // Wrap with generic arguments before any union/intersection composition: + // `Box|null` is `Box | null`, not `Box`. + let hint = if self.is_at_generic_open_angle()? { + let arguments = self.parse_generic_argument_list()?; + Hint::Generic(GenericHint { base: self.arena.alloc(hint), arguments }) + } else { + hint + }; + let next = self.stream.lookahead(0)?; Ok(match next.map(|t| t.kind) { Some(T!["|"]) => { diff --git a/crates/syntax/src/parser/mod.rs b/crates/syntax/src/parser/mod.rs index abbfc92ba..7eca5f791 100644 --- a/crates/syntax/src/parser/mod.rs +++ b/crates/syntax/src/parser/mod.rs @@ -26,6 +26,18 @@ pub struct State { pub within_indirect_variable: bool, pub within_string_interpolation: bool, pub recursion_depth: u16, + /// Number of virtual `>` tokens left to consume. + /// + /// Set when a `>>` (right-shift) token is consumed as the first half of a + /// closing angle on a nested generic argument or parameter list. The next + /// call to [`Parser::eat_close_angle`] sees this counter, returns a + /// synthetic single-`>` span for the second half, and decrements it. + pub pending_close_angles: u8, + /// Byte offset of the synthetic single `>` token waiting to be consumed. + /// + /// Tracks the start position of the residual `>` produced when a `>>` is + /// split. Only meaningful when `pending_close_angles > 0`. + pub pending_close_angle_offset: u32, } /// The main parser for PHP source code. diff --git a/crates/syntax/src/token/mod.rs b/crates/syntax/src/token/mod.rs index 6961957d6..01999ceaf 100644 --- a/crates/syntax/src/token/mod.rs +++ b/crates/syntax/src/token/mod.rs @@ -154,6 +154,7 @@ pub enum TokenKind { RealCast, // `(real)` FloatCast, // `(float)` ColonColon, // `::` + ColonColonLessThan, // `::<` (turbofish) EqualEqual, // `==` DoubleQuote, // `"` Else, // `else` @@ -335,6 +336,7 @@ impl Precedence { T!["("] => Self::CallDim, T!["["] => Self::ArrayDim, T!["->" | "?->" | "::"] => Self::ObjectAccess, + T!["::<"] => Self::CallDim, _ => Self::Lowest, } } @@ -540,7 +542,7 @@ impl TokenKind { #[inline] #[must_use] pub const fn is_postfix(&self) -> bool { - matches!(self, T!["++" | "--" | "(" | "[" | "->" | "?->" | "::"]) + matches!(self, T!["++" | "--" | "(" | "[" | "->" | "?->" | "::" | "::<"]) } #[inline] diff --git a/crates/syntax/src/walker/mod.rs b/crates/syntax/src/walker/mod.rs index f9eb5dd0f..91b3d7b0a 100644 --- a/crates/syntax/src/walker/mod.rs +++ b/crates/syntax/src/walker/mod.rs @@ -28,6 +28,7 @@ use crate::ast::ast::ClassLikeConstantSelector; use crate::ast::ast::ClassLikeMember; use crate::ast::ast::ClassLikeMemberExpressionSelector; use crate::ast::ast::ClassLikeMemberSelector; +use crate::ast::ast::ClassLikeReference; use crate::ast::ast::Clone; use crate::ast::ast::ClosingTag; use crate::ast::ast::Closure; @@ -80,6 +81,12 @@ use crate::ast::ast::FunctionLikeParameterDefaultValue; use crate::ast::ast::FunctionLikeParameterList; use crate::ast::ast::FunctionLikeReturnTypeHint; use crate::ast::ast::FunctionPartialApplication; +use crate::ast::ast::GenericArgumentList; +use crate::ast::ast::GenericHint; +use crate::ast::ast::GenericParameter; +use crate::ast::ast::GenericParameterBound; +use crate::ast::ast::GenericParameterDefault; +use crate::ast::ast::GenericParameterList; use crate::ast::ast::Global; use crate::ast::ast::Goto; use crate::ast::ast::HaltCompiler; @@ -198,6 +205,7 @@ use crate::ast::ast::TraitUseSpecification; use crate::ast::ast::Try; use crate::ast::ast::TryCatchClause; use crate::ast::ast::TryFinallyClause; +use crate::ast::ast::Turbofish; use crate::ast::ast::TypedUseItemList; use crate::ast::ast::TypedUseItemSequence; use crate::ast::ast::UnaryPostfix; @@ -574,6 +582,10 @@ generate_ast_walker! { 'arena Attribute as attribute => { walker.walk_identifier(&attribute.name, context); + if let Some(turbofish) = &attribute.turbofish { + walker.walk_turbofish(turbofish, context); + } + if let Some(argument_list) = &attribute.argument_list { walker.walk_argument_list(argument_list, context); } @@ -651,7 +663,7 @@ generate_ast_walker! { walker.walk_keyword(&extends.extends, context); for ty in &extends.types { - walker.walk_identifier(ty, context); + walker.walk_class_like_reference(ty, context); } } @@ -659,7 +671,14 @@ generate_ast_walker! { walker.walk_keyword(&implements.implements, context); for ty in &implements.types { - walker.walk_identifier(ty, context); + walker.walk_class_like_reference(ty, context); + } + } + + 'arena ClassLikeReference as class_like_reference => { + walker.walk_identifier(&class_like_reference.name, context); + if let Some(arguments) = &class_like_reference.generic_arguments { + walker.walk_generic_argument_list(arguments, context); } } @@ -674,6 +693,9 @@ generate_ast_walker! { walker.walk_keyword(&class.class, context); walker.walk_local_identifier(&class.name, context); + if let Some(generic_parameters) = &class.generic_parameters { + walker.walk_generic_parameter_list(generic_parameters, context); + } if let Some(extends) = &class.extends { walker.walk_extends(extends, context); } @@ -695,6 +717,10 @@ generate_ast_walker! { walker.walk_keyword(&interface.interface, context); walker.walk_local_identifier(&interface.name, context); + if let Some(generic_parameters) = &interface.generic_parameters { + walker.walk_generic_parameter_list(generic_parameters, context); + } + if let Some(extends) = &interface.extends { walker.walk_extends(extends, context); } @@ -712,6 +738,10 @@ generate_ast_walker! { walker.walk_keyword(&r#trait.r#trait, context); walker.walk_local_identifier(&r#trait.name, context); + if let Some(generic_parameters) = &r#trait.generic_parameters { + walker.walk_generic_parameter_list(generic_parameters, context); + } + for class_member in &r#trait.members { walker.walk_class_like_member(class_member, context); } @@ -766,7 +796,7 @@ generate_ast_walker! { walker.walk_keyword(&trait_use.r#use, context); for trait_name in &trait_use.trait_names { - walker.walk_identifier(trait_name, context); + walker.walk_class_like_reference(trait_name, context); } walker.walk_trait_use_specification(&trait_use.specification, context); @@ -1090,6 +1120,9 @@ generate_ast_walker! { walker.walk_keyword(&method.function, context); walker.walk_local_identifier(&method.name, context); + if let Some(generic_parameters) = &method.generic_parameters { + walker.walk_generic_parameter_list(generic_parameters, context); + } walker.walk_function_like_parameter_list(&method.parameter_list, context); if let Some(hint) = &method.return_type_hint { walker.walk_function_like_return_type_hint(hint, context); @@ -1148,6 +1181,9 @@ generate_ast_walker! { walker.walk_keyword(&function.function, context); walker.walk_local_identifier(&function.name, context); + if let Some(generic_parameters) = &function.generic_parameters { + walker.walk_generic_parameter_list(generic_parameters, context); + } walker.walk_function_like_parameter_list(&function.parameter_list, context); if let Some(hint) = &function.return_type_hint { walker.walk_function_like_return_type_hint(hint, context); @@ -1877,6 +1913,9 @@ generate_ast_walker! { } walker.walk_keyword(&closure.function, context); + if let Some(generic_parameters) = &closure.generic_parameters { + walker.walk_generic_parameter_list(generic_parameters, context); + } walker.walk_function_like_parameter_list(&closure.parameter_list, context); if let Some(use_clause) = &closure.use_clause { walker.walk_closure_use_clause(use_clause, context); @@ -1909,6 +1948,9 @@ generate_ast_walker! { } walker.walk_keyword(&arrow_function.r#fn, context); + if let Some(generic_parameters) = &arrow_function.generic_parameters { + walker.walk_generic_parameter_list(generic_parameters, context); + } walker.walk_function_like_parameter_list(&arrow_function.parameter_list, context); if let Some(return_type_hint) = &arrow_function.return_type_hint { @@ -2150,24 +2192,36 @@ generate_ast_walker! { 'arena FunctionCall as function_call => { walker.walk_expression(function_call.function, context); + if let Some(turbofish) = &function_call.turbofish { + walker.walk_turbofish(turbofish, context); + } walker.walk_argument_list(&function_call.argument_list, context); } 'arena MethodCall as method_call => { walker.walk_expression(method_call.object, context); walker.walk_class_like_member_selector(&method_call.method, context); + if let Some(turbofish) = &method_call.turbofish { + walker.walk_turbofish(turbofish, context); + } walker.walk_argument_list(&method_call.argument_list, context); } 'arena NullSafeMethodCall as null_safe_method_call => { walker.walk_expression(null_safe_method_call.object, context); walker.walk_class_like_member_selector(&null_safe_method_call.method, context); + if let Some(turbofish) = &null_safe_method_call.turbofish { + walker.walk_turbofish(turbofish, context); + } walker.walk_argument_list(&null_safe_method_call.argument_list, context); } 'arena StaticMethodCall as static_method_call => { walker.walk_expression(static_method_call.class, context); walker.walk_class_like_member_selector(&static_method_call.method, context); + if let Some(turbofish) = &static_method_call.turbofish { + walker.walk_turbofish(turbofish, context); + } walker.walk_argument_list(&static_method_call.argument_list, context); } @@ -2188,18 +2242,27 @@ generate_ast_walker! { 'arena FunctionPartialApplication as function_partial_application => { walker.walk_expression(function_partial_application.function, context); + if let Some(turbofish) = &function_partial_application.turbofish { + walker.walk_turbofish(turbofish, context); + } walker.walk_partial_argument_list(&function_partial_application.argument_list, context); } 'arena MethodPartialApplication as method_partial_application => { walker.walk_expression(method_partial_application.object, context); walker.walk_class_like_member_selector(&method_partial_application.method, context); + if let Some(turbofish) = &method_partial_application.turbofish { + walker.walk_turbofish(turbofish, context); + } walker.walk_partial_argument_list(&method_partial_application.argument_list, context); } 'arena StaticMethodPartialApplication as static_method_partial_application => { walker.walk_expression(static_method_partial_application.class, context); walker.walk_class_like_member_selector(&static_method_partial_application.method, context); + if let Some(turbofish) = &static_method_partial_application.turbofish { + walker.walk_turbofish(turbofish, context); + } walker.walk_partial_argument_list(&static_method_partial_application.argument_list, context); } @@ -2300,6 +2363,9 @@ generate_ast_walker! { 'arena Instantiation as instantiation => { walker.walk_keyword(&instantiation.new, context); walker.walk_expression(instantiation.class, context); + if let Some(turbofish) = &instantiation.turbofish { + walker.walk_turbofish(turbofish, context); + } if let Some(argument_list) = &instantiation.argument_list { walker.walk_argument_list(argument_list, context); } @@ -2331,6 +2397,9 @@ generate_ast_walker! { Hint::Intersection(intersection_hint) => { walker.walk_intersection_hint(intersection_hint, context); } + Hint::Generic(generic_hint) => { + walker.walk_generic_hint(generic_hint, context); + } Hint::Null(keyword) | Hint::True(keyword) | Hint::False(keyword) | @@ -2373,6 +2442,47 @@ generate_ast_walker! { walker.walk_hint(intersection_hint.right, context); } + 'arena GenericHint as generic_hint => { + walker.walk_hint(generic_hint.base, context); + walker.walk_generic_argument_list(&generic_hint.arguments, context); + } + + 'arena GenericArgumentList as generic_argument_list => { + for argument in generic_argument_list.arguments.iter() { + walker.walk_hint(argument, context); + } + } + + 'arena GenericParameterList as generic_parameter_list => { + for parameter in generic_parameter_list.parameters.iter() { + walker.walk_generic_parameter(parameter, context); + } + } + + 'arena GenericParameter as generic_parameter => { + walker.walk_local_identifier(&generic_parameter.name, context); + if let Some(bound) = &generic_parameter.bound { + walker.walk_generic_parameter_bound(bound, context); + } + if let Some(default) = &generic_parameter.default { + walker.walk_generic_parameter_default(default, context); + } + } + + 'arena GenericParameterBound as generic_parameter_bound => { + walker.walk_hint(&generic_parameter_bound.hint, context); + } + + 'arena GenericParameterDefault as generic_parameter_default => { + walker.walk_hint(&generic_parameter_default.hint, context); + } + + 'arena Turbofish as turbofish => { + for argument in turbofish.arguments.iter() { + walker.walk_hint(argument, context); + } + } + 'arena Keyword as keyword => { // Do nothing by default } diff --git a/crates/syntax/tests/parser.rs b/crates/syntax/tests/parser.rs index bfa7894f3..46c5617c1 100644 --- a/crates/syntax/tests/parser.rs +++ b/crates/syntax/tests/parser.rs @@ -1305,4 +1305,39 @@ mod parser { smoke_test!(issue_1713_exit_partial_application_variadic, " 'Foo', default, => 'Bar', };"); smoke_test!(issue_1713_yield_unary_minus, " {}"); + smoke_test!(generic_class_two_params, " {}"); + smoke_test!(generic_class_bound, " {}"); + smoke_test!(generic_class_default, " {}"); + smoke_test!(generic_class_bound_and_default, " {}"); + smoke_test!(generic_class_covariant, " {}"); + smoke_test!(generic_class_contravariant, " {}"); + smoke_test!(generic_class_variance_with_bound, " {}"); + smoke_test!(generic_interface, " {}"); + smoke_test!(generic_trait, " {}"); + smoke_test!(generic_function, "(T $v): T { return $v; }"); + smoke_test!(generic_method, "(callable $f): array {} }"); + smoke_test!(generic_closure, "(K $k, V $v): array { return [$k, $v]; };"); + smoke_test!(generic_arrow_function, "(T $x): T => $x;"); + smoke_test!(generic_type_hint_in_parameter, " $b): void {}"); + smoke_test!(generic_type_hint_in_return, " { return new Box; }"); + smoke_test!(generic_type_hint_two_args, " { return new Map; }"); + smoke_test!(turbofish_function_call, "(1);"); + smoke_test!(turbofish_method_call, "map::($f);"); + smoke_test!(turbofish_nullsafe_method_call, "map::($f);"); + smoke_test!(turbofish_static_method_call, "();"); + smoke_test!(turbofish_new, "(1);"); + smoke_test!(turbofish_new_no_args, ";"); + smoke_test!(turbofish_attribute, "] class C {}"); + smoke_test!(turbofish_first_class_callable, "(...);"); + smoke_test!(generic_extends, " {}"); + smoke_test!(generic_extends_multiple_args, " {}"); + smoke_test!(generic_implements, ", Countable {}"); + smoke_test!(generic_interface_extends, " {}"); + smoke_test!(generic_trait_use, "; }"); + smoke_test!(generic_trait_use_multi, ", Other; }"); + smoke_test!(generic_extends_no_args, " Result< }) } +#[test] +fn test_turbofish_token_is_single_3byte() -> Result<(), SyntaxError> { + let code = b""; + let expected = &[ + TokenKind::OpenTag, + TokenKind::Whitespace, + TokenKind::Identifier, + TokenKind::ColonColonLessThan, + TokenKind::Identifier, + TokenKind::GreaterThan, + ]; + + test_lexer(code, expected).map_err(|err| { + panic!("unexpected error: {err}"); + }) +} + +#[test] +fn test_turbofish_is_whitespace_sensitive() -> Result<(), SyntaxError> { + // `:: <` must stay as ColonColon then LessThan, NOT a single turbofish. + let code = b""; + let expected = &[ + TokenKind::OpenTag, + TokenKind::Whitespace, + TokenKind::Identifier, + TokenKind::ColonColon, + TokenKind::Whitespace, + TokenKind::LessThan, + TokenKind::Identifier, + TokenKind::GreaterThan, + ]; + + test_lexer(code, expected).map_err(|err| { + panic!("unexpected error: {err}"); + }) +} + pub const KEYWORD_TYPES: [(&[u8], TokenKind); 84] = [ (b"eval", TokenKind::Eval), (b"die", TokenKind::Die),