Skip to content

Commit

Permalink
feat: support attributes on constants for PHP 8.5 (#64)
Browse files Browse the repository at this point in the history
Signed-off-by: azjezz <azjezz@protonmail.com>
  • Loading branch information
azjezz authored Feb 1, 2025
1 parent 7ed0072 commit 605f6b7
Show file tree
Hide file tree
Showing 12 changed files with 106 additions and 23 deletions.
3 changes: 3 additions & 0 deletions crates/ast/src/ast/constant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,20 @@ use serde::Serialize;
use mago_span::HasSpan;
use mago_span::Span;

use crate::ast::attribute::AttributeList;
use crate::ast::expression::Expression;
use crate::ast::identifier::LocalIdentifier;
use crate::ast::keyword::Keyword;
use crate::ast::terminator::Terminator;
use crate::sequence::Sequence;
use crate::sequence::TokenSeparatedSequence;

/// Represents a constant statement in PHP.
///
/// Example: `const FOO = 1;` or `const BAR = 2, QUX = 3, BAZ = 4;`
#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
pub struct Constant {
pub attribute_lists: Sequence<AttributeList>,
pub r#const: Keyword,
pub items: TokenSeparatedSequence<ConstantItem>,
pub terminator: Terminator,
Expand Down
5 changes: 3 additions & 2 deletions crates/ast/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1157,8 +1157,9 @@ impl<'a> Node<'a> {
vec![Node::Expression(&node.class), Node::ClassLikeMemberSelector(&node.method)]
}
Node::Constant(node) => {
let mut children = vec![Node::Keyword(&node.r#const)];

let mut children = vec![];
children.extend(node.attribute_lists.iter().map(Node::AttributeList));
children.push(Node::Keyword(&node.r#const));
children.extend(node.items.iter().map(Node::ConstantItem));
children.push(Node::Terminator(&node.terminator));

Expand Down
39 changes: 25 additions & 14 deletions crates/formatter/src/format/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1247,24 +1247,35 @@ impl<'a> Format<'a> for ConstantItem {
impl<'a> Format<'a> for Constant {
fn format(&'a self, f: &mut Formatter<'a>) -> Document<'a> {
wrap!(f, self, Constant, {
let mut contents = vec![self.r#const.format(f)];
let attributes =
if let Some(attributes) = misc::print_attribute_list_sequence(f, &self.attribute_lists, false) {
attributes
} else {
Document::empty()
};

if self.items.len() == 1 {
contents.push(Document::space());
contents.push(self.items.as_slice()[0].format(f));
} else if !self.items.is_empty() {
contents.push(Document::Indent(vec![Document::Line(Line::default())]));
let constant = {
let mut contents = vec![self.r#const.format(f)];

contents.push(Document::Indent(Document::join(
self.items.iter().map(|v| v.format(f)).collect(),
Separator::CommaLine,
)));
contents.push(Document::Line(Line::softline()));
}
if self.items.len() == 1 {
contents.push(Document::space());
contents.push(self.items.as_slice()[0].format(f));
} else if !self.items.is_empty() {
contents.push(Document::Indent(vec![Document::Line(Line::default())]));

contents.push(self.terminator.format(f));
contents.push(Document::Indent(Document::join(
self.items.iter().map(|v| v.format(f)).collect(),
Separator::CommaLine,
)));
contents.push(Document::Line(Line::softline()));
}

Document::Group(Group::new(contents))
contents.push(self.terminator.format(f));

Document::Group(Group::new(contents))
};

Document::Group(Group::new(vec![attributes, constant]))
})
}
}
Expand Down
19 changes: 19 additions & 0 deletions crates/formatter/tests/format/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,22 @@ pub fn test_inline_html() {

test_format(code, expected, FormatSettings::default())
}

#[test]
pub fn test_php_85_constant_attributes() {
let code = indoc! {r#"
<?php
#[Deprecated]
const FOO = 'foo';
"#};

let expected = indoc! {r#"
<?php
#[Deprecated]
const FOO = 'foo';
"#};

test_format(code, expected, FormatSettings::default())
}
7 changes: 6 additions & 1 deletion crates/parser/src/internal/constant.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use mago_ast::ast::*;
use mago_ast::sequence::TokenSeparatedSequence;
use mago_ast::Sequence;
use mago_token::T;

use crate::error::ParseError;
Expand All @@ -9,8 +10,12 @@ use crate::internal::terminator::parse_terminator;
use crate::internal::token_stream::TokenStream;
use crate::internal::utils;

pub fn parse_constant(stream: &mut TokenStream<'_, '_>) -> Result<Constant, ParseError> {
pub fn parse_constant_with_attributes(
stream: &mut TokenStream<'_, '_>,
attribute_lists: Sequence<AttributeList>,
) -> Result<Constant, ParseError> {
Ok(Constant {
attribute_lists,
r#const: utils::expect_keyword(stream, T!["const"])?,
items: {
let mut items = vec![];
Expand Down
5 changes: 3 additions & 2 deletions crates/parser/src/internal/statement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::internal::class_like::parse_class_with_attributes;
use crate::internal::class_like::parse_enum_with_attributes;
use crate::internal::class_like::parse_interface_with_attributes;
use crate::internal::class_like::parse_trait_with_attributes;
use crate::internal::constant::parse_constant;
use crate::internal::constant::parse_constant_with_attributes;
use crate::internal::control_flow::r#if::parse_if;
use crate::internal::control_flow::switch::parse_switch;
use crate::internal::declare::parse_declare;
Expand Down Expand Up @@ -60,6 +60,7 @@ pub fn parse_statement(stream: &mut TokenStream<'_, '_>) -> Result<Statement, Pa
T!["trait"] => Statement::Trait(parse_trait_with_attributes(stream, attributes)?),
T!["enum"] => Statement::Enum(parse_enum_with_attributes(stream, attributes)?),
T!["class"] => Statement::Class(parse_class_with_attributes(stream, attributes)?),
T!["const"] => Statement::Constant(parse_constant_with_attributes(stream, attributes)?),
T!["function"] => {
// unlike when we have modifiers, here, we don't know if this is meant to be a closure or a function
parse_closure_or_function(stream, attributes)?
Expand Down Expand Up @@ -120,7 +121,7 @@ pub fn parse_statement(stream: &mut TokenStream<'_, '_>) -> Result<Statement, Pa
}
T!["__halt_compiler"] => Statement::HaltCompiler(parse_halt_compiler(stream)?),
T![";"] => Statement::Noop(utils::expect(stream, T![";"])?.span),
T!["const"] => Statement::Constant(parse_constant(stream)?),
T!["const"] => Statement::Constant(parse_constant_with_attributes(stream, Sequence::empty())?),
T!["if"] => Statement::If(parse_if(stream)?),
T!["switch"] => Statement::Switch(parse_switch(stream)?),
T!["foreach"] => Statement::Foreach(parse_foreach(stream)?),
Expand Down
1 change: 1 addition & 0 deletions crates/php-version/src/feature.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,5 @@ pub enum Feature {
TrailingCommaInFunctionCalls,
TrailingCommaInClosureUseList,
NewInInitializers,
ConstantAttribute,
}
2 changes: 1 addition & 1 deletion crates/php-version/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ impl PHPVersion {
| Feature::HighlightStringDoesNotReturnFalse
| Feature::PropertyHooks
| Feature::NewWithoutParentheses => self.0 >= 0x08_04_00,
Feature::ClosureInConstantExpressions => self.0 >= 0x08_05_00,
Feature::ClosureInConstantExpressions | Feature::ConstantAttribute => self.0 >= 0x08_05_00,
Feature::CallableInstanceMethods
| Feature::LegacyConstructor
| Feature::UnsetCast
Expand Down
4 changes: 4 additions & 0 deletions crates/reflection/src/constant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use mago_source::SourceIdentifier;
use mago_span::HasSpan;
use mago_span::Span;

use crate::attribute::AttributeReflection;
use crate::identifier::Name;
use crate::r#type::TypeReflection;
use crate::Reflection;
Expand All @@ -19,6 +20,9 @@ use crate::Reflection;
/// and separated even when declared in a single statement, such as `const A = 1, B = 2;`.
#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub struct ConstantReflection {
/// Collection of attributes applied to the constant.
pub attribute_reflections: Vec<AttributeReflection>,

/// The name of the constant.
pub name: Name,

Expand Down
5 changes: 5 additions & 0 deletions crates/reflector/src/internal/reflect/constant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@ use mago_reflection::identifier::Name;
use mago_span::*;

use crate::internal::context::Context;
use crate::internal::reflect::attribute::reflect_attributes;

pub fn reflect_constant(constant: &Constant, context: &mut Context<'_>) -> Vec<ConstantReflection> {
let attribute_reflections = reflect_attributes(&constant.attribute_lists, context);

let mut reflections = vec![];
for item in constant.items.iter() {
let name = context.names.get(&item.name);

reflections.push(ConstantReflection {
attribute_reflections: attribute_reflections.clone(),
name: Name::new(*name, item.name.span),
type_reflection: mago_typing::infere(context.interner, context.source, context.names, &item.value),
item_span: item.span(),
Expand Down Expand Up @@ -48,6 +52,7 @@ pub fn reflect_defined_constant(define: &FunctionCall, context: &mut Context<'_>
let name = context.interner.intern(name);

Some(ConstantReflection {
attribute_reflections: Default::default(),
name: Name::new(name, name_span),
type_reflection: mago_typing::infere(context.interner, context.source, context.names, arguments[1].value()),
item_span: define.span(),
Expand Down
35 changes: 32 additions & 3 deletions crates/semantics/src/walker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3551,15 +3551,19 @@ impl Walker<Context<'_>> for SemanticsWalker {
}
}

fn walk_in_attribute(&self, attribute: &Attribute, context: &mut Context<'_>) {
fn walk_in_attribute_list(&self, attribute_list: &AttributeList, context: &mut Context<'_>) {
if !context.version.is_supported(Feature::Attribute) {
context.report(
Issue::error("Attributes are only available in PHP 8.0 and above.")
.with_annotation(Annotation::primary(attribute.span()).with_message("Attribute defined here."))
.with_annotation(
Annotation::primary(attribute_list.span()).with_message("Attribute list used here."),
)
.with_help("Upgrade to PHP 8.0 or above to use attributes."),
);
}
}

fn walk_in_attribute(&self, attribute: &Attribute, context: &mut Context<'_>) {
let name = context.interner.lookup(&attribute.name.value());
if let Some(list) = &attribute.arguments {
for argument in list.arguments.iter() {
Expand Down Expand Up @@ -3876,7 +3880,6 @@ impl Walker<Context<'_>> for SemanticsWalker {
fn walk_in_function_like_parameter_list(
&self,
function_like_parameter_list: &FunctionLikeParameterList,

context: &mut Context<'_>,
) {
let mut last_variadic = None;
Expand Down Expand Up @@ -4453,6 +4456,32 @@ impl Walker<Context<'_>> for SemanticsWalker {
.with_help("Use `get_class($object)` instead to make the code compatible with PHP 7.4 and earlier versions, or upgrade to PHP 8.0 or later."),
);
}

fn walk_in_constant(&self, constant: &Constant, context: &mut Context<'_>) {
if !context.version.is_supported(Feature::ConstantAttribute) {
for attribute_list in constant.attribute_lists.iter() {
context.report(
Issue::error("Constant attributes are only available in PHP 8.5 and above.")
.with_annotation(
Annotation::primary(attribute_list.span()).with_message("Attribute list used here."),
)
.with_help("Upgrade to PHP 8.5 or later to use constant attributes."),
);
}
}

for item in constant.items.iter() {
if !item.value.is_constant(context.version, true) {
context.report(
Issue::error("Constant value must be a constant expression.")
.with_annotation(
Annotation::primary(item.value.span()).with_message("This is not a constant expression."),
)
.with_help("Ensure the constant value is a constant expression."),
);
}
}
}
}

/// Defines the semantics of magic methods.
Expand Down
4 changes: 4 additions & 0 deletions crates/walker/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -829,6 +829,10 @@ generate_ast_walker! {
}

Constant as constant => {
for attribute_list in constant.attribute_lists.iter() {
walker.walk_attribute_list(attribute_list, context);
}

walker.walk_keyword(&constant.r#const, context);
for item in constant.items.iter() {
walker.walk_constant_item(item, context);
Expand Down

0 comments on commit 605f6b7

Please sign in to comment.