use crate::PhpVersion;
use php_ast::Span;
use std::borrow::Cow;
use thiserror::Error;
#[derive(Debug, Clone, Error)]
#[non_exhaustive]
pub enum ParseError {
#[error("expected {expected}, found {found}")]
#[non_exhaustive]
Expected {
expected: Cow<'static, str>,
found: Cow<'static, str>,
span: Span,
},
#[error("unterminated string literal")]
#[non_exhaustive]
UnterminatedString {
span: Span,
},
#[error("expected {expected} after {after}")]
#[non_exhaustive]
ExpectedAfter {
expected: Cow<'static, str>,
after: Cow<'static, str>,
span: Span,
},
#[error("missing closing {delimiter}")]
#[non_exhaustive]
UnclosedDelimiter {
delimiter: Cow<'static, str>,
opened_at: Span,
span: Span,
},
#[error("{message}")]
#[non_exhaustive]
Forbidden {
rule: ForbiddenRule,
message: Cow<'static, str>,
span: Span,
},
#[error("maximum nesting depth exceeded")]
#[non_exhaustive]
NestingTooDeep {
span: Span,
},
#[error("source is {len} bytes; the maximum is 4 GiB")]
#[non_exhaustive]
SourceTooLarge {
len: usize,
span: Span,
},
#[error("'{feature}' requires PHP {required} or higher")]
#[non_exhaustive]
VersionTooLow {
feature: Cow<'static, str>,
required: PhpVersion,
used: PhpVersion,
span: Span,
},
}
impl ParseError {
pub fn span(&self) -> Span {
match self {
ParseError::Expected { span, .. }
| ParseError::UnterminatedString { span }
| ParseError::ExpectedAfter { span, .. }
| ParseError::UnclosedDelimiter { span, .. }
| ParseError::Forbidden { span, .. }
| ParseError::NestingTooDeep { span }
| ParseError::SourceTooLarge { span, .. }
| ParseError::VersionTooLow { span, .. } => *span,
}
}
pub fn forbidden_rule(&self) -> Option<ForbiddenRule> {
match self {
ParseError::Forbidden { rule, .. } => Some(*rule),
_ => None,
}
}
pub fn code(&self) -> &'static str {
match self {
ParseError::Expected { .. } => "expected",
ParseError::UnterminatedString { .. } => "unterminated-string",
ParseError::ExpectedAfter { .. } => "expected-after",
ParseError::UnclosedDelimiter { .. } => "unclosed-delimiter",
ParseError::Forbidden { rule, .. } => rule.as_str(),
ParseError::NestingTooDeep { .. } => "nesting-too-deep",
ParseError::SourceTooLarge { .. } => "source-too-large",
ParseError::VersionTooLow { .. } => "version-too-low",
}
}
pub fn leaves_ast_incomplete(&self) -> bool {
!matches!(
self,
ParseError::Forbidden { .. } | ParseError::VersionTooLow { .. }
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Phase {
Parse,
Compile,
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[error("unknown forbidden rule")]
pub struct UnknownForbiddenRule;
macro_rules! forbidden_rules {
($($(#[$doc:meta])* $rule:ident = $id:literal, $phase:ident;)*) => {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ForbiddenRule {
$($(#[$doc])* $rule,)*
}
impl ForbiddenRule {
pub const ALL: &'static [ForbiddenRule] = &[$(ForbiddenRule::$rule),*];
pub fn as_str(self) -> &'static str {
match self {
$(ForbiddenRule::$rule => $id,)*
}
}
pub fn phase(self) -> Phase {
match self {
$(ForbiddenRule::$rule => Phase::$phase,)*
}
}
}
};
}
forbidden_rules! {
ClassKeywordOutsideClass = "class-keyword-outside-class", Compile;
ParentWithoutParentClass = "parent-without-parent-class", Compile;
ToStringReturnType = "to-string-return-type", Compile;
MagicMethodReturnType = "magic-method-return-type", Compile;
AttributeTargetMismatch = "attribute-target-mismatch", Compile;
AttributeOnInvalidClass = "attribute-on-invalid-class", Compile;
DeprecatedOnInvalidTarget = "deprecated-on-invalid-target", Compile;
AllowDynamicPropertiesOnInvalidClass = "allow-dynamic-properties-on-invalid-class", Compile;
MissingReturnValue = "missing-return-value", Compile;
GeneratorReturnType = "generator-return-type", Compile;
ReadonlyPropertyDefault = "readonly-property-default", Compile;
DuplicateParameter = "duplicate-parameter", Compile;
ReassignThis = "reassign-this", Compile;
UnclosedInterpolation = "unclosed-interpolation", Parse;
EmptyInterpolationIndex = "empty-interpolation-index", Parse;
EmptyInterpolationVariableName = "empty-interpolation-variable-name", Parse;
UnclosedInterpolationIndex = "unclosed-interpolation-index", Parse;
ClassConstantInterpolation = "class-constant-interpolation", Parse;
InvalidInterpolationOffset = "invalid-interpolation-offset", Parse;
InvalidNumericLiteral = "invalid-numeric-literal", Parse;
InvalidCodepointEscape = "invalid-codepoint-escape", Parse;
MixedHeredocIndentation = "mixed-heredoc-indentation", Parse;
HeredocBodyIndentation = "heredoc-body-indentation", Parse;
WhitespaceInNamespacedName = "whitespace-in-namespaced-name", Parse;
MultipleVisibilityModifiers = "multiple-visibility-modifiers", Parse;
MultipleSetVisibilityModifiers = "multiple-set-visibility-modifiers", Parse;
DuplicateModifier = "duplicate-modifier", Parse;
AbstractFinalConflict = "abstract-final-conflict", Parse;
InvalidConstantModifier = "invalid-constant-modifier", Parse;
ModifierNotAllowed = "modifier-not-allowed", Parse;
KeywordAsDeclarationName = "keyword-as-declaration-name", Parse;
ReservedClassName = "reserved-class-name", Parse;
OutermostScopeOnly = "outermost-scope-only", Parse;
UnparenthesizedIntersectionInUnion = "unparenthesized-intersection-in-union", Parse;
ParenthesizedIntersectionOutsideUnion = "parenthesized-intersection-outside-union", Parse;
StaticTypePosition = "static-type-position", Parse;
EmptyAttributeList = "empty-attribute-list", Parse;
CallTimePassByReference = "call-time-pass-by-reference", Parse;
CurlyOffsetAccess = "curly-offset-access", Parse;
VoidCastInExpression = "void-cast-in-expression", Parse;
RealCast = "real-cast", Parse;
NonAssociativeChain = "non-associative-chain", Parse;
IncDecAsAssignTarget = "inc-dec-as-assign-target", Parse;
InvalidAssignTarget = "invalid-assign-target", Parse;
CompoundAssignToDestructuring = "compound-assign-to-destructuring", Parse;
ReferenceAssignToDestructuring = "reference-assign-to-destructuring", Parse;
IncDecNonVariable = "inc-dec-non-variable", Parse;
InvalidDereference = "invalid-dereference", Parse;
InvalidWriteContext = "invalid-write-context", Parse;
ListOutsideDestructuring = "list-outside-destructuring", Parse;
EmptyArrayElement = "empty-array-element", Compile;
ReferenceAssignNew = "reference-assign-new", Parse;
HooksOnPropertyList = "hooks-on-property-list", Parse;
InvalidInstanceofOperand = "invalid-instanceof-operand", Parse;
SwitchLeadingSemicolons = "switch-leading-semicolons", Parse;
PartialApplicationInNew = "partial-application-in-new", Parse;
UnparenthesizedNewDereference = "unparenthesized-new-dereference", Parse;
StaticStandaloneExpression = "static-standalone-expression", Parse;
ReferenceToParenthesized = "reference-to-parenthesized", Parse;
ReferenceToNonReferenceable = "reference-to-non-referenceable", Parse;
QualifiedClassKeyword = "qualified-class-keyword", Compile;
ReservedClassReference = "reserved-class-reference", Compile;
ReservedEnumName = "reserved-enum-name", Compile;
TraitInInterface = "trait-in-interface", Compile;
AbstractMemberInConcreteClass = "abstract-member-in-concrete-class", Compile;
DuplicateProperty = "duplicate-property", Compile;
DuplicateMethod = "duplicate-method", Compile;
DuplicateClassConstant = "duplicate-class-constant", Compile;
ClassConstantNamedClass = "class-constant-named-class", Compile;
ClassConstantValueType = "class-constant-value-type", Compile;
PrivateFinalConstant = "private-final-constant", Compile;
InvalidInterfaceConstantModifier = "invalid-interface-constant-modifier", Compile;
InvalidInterfaceMethodModifier = "invalid-interface-method-modifier", Compile;
InvalidInterfacePropertyModifier = "invalid-interface-property-modifier", Compile;
InterfaceMethodBody = "interface-method-body", Compile;
MissingMethodBody = "missing-method-body", Compile;
AbstractMethodBody = "abstract-method-body", Compile;
PrivateAbstractMethod = "private-abstract-method", Compile;
AbstractAnonymousClassMethod = "abstract-anonymous-class-method", Compile;
AbstractEnumMethod = "abstract-enum-method", Compile;
MagicMethodArgumentCount = "magic-method-argument-count", Compile;
MagicMethodByReference = "magic-method-by-reference", Compile;
MagicMethodStatic = "magic-method-static", Compile;
MagicMethodParameterType = "magic-method-parameter-type", Compile;
StaticReadonlyProperty = "static-readonly-property", Compile;
UntypedReadonlyProperty = "untyped-readonly-property", Compile;
ReadonlyPropertyHooks = "readonly-property-hooks", Compile;
UntypedAsymmetricProperty = "untyped-asymmetric-property", Compile;
SetVisibilityWiderThanVisibility = "set-visibility-wider-than-visibility", Compile;
PrivateFinalProperty = "private-final-property", Compile;
AbstractPropertyWithoutAbstractHook = "abstract-property-without-abstract-hook", Compile;
InterfacePropertyWithoutHooks = "interface-property-without-hooks", Compile;
StaticPropertyHooks = "static-property-hooks", Compile;
VirtualPropertyDefault = "virtual-property-default", Compile;
VirtualPropertyAsymmetricVisibility = "virtual-property-asymmetric-visibility", Compile;
EmptyHookList = "empty-hook-list", Compile;
DuplicateHook = "duplicate-hook", Compile;
GetHookParameters = "get-hook-parameters", Compile;
SetHookParameterCount = "set-hook-parameter-count", Compile;
SetHookParameterByReference = "set-hook-parameter-by-reference", Compile;
SetHookParameterVariadic = "set-hook-parameter-variadic", Compile;
SetHookParameterType = "set-hook-parameter-type", Compile;
ByReferenceGetHookWithSet = "by-reference-get-hook-with-set", Compile;
MissingHookBody = "missing-hook-body", Compile;
AbstractHookBody = "abstract-hook-body", Compile;
AbstractFinalHook = "abstract-final-hook", Compile;
PrivateAbstractHook = "private-abstract-hook", Compile;
PromotedPropertyOutsideConstructor = "promoted-property-outside-constructor", Compile;
PromotedPropertyInAbstractConstructor = "promoted-property-in-abstract-constructor", Compile;
VariadicPromotedProperty = "variadic-promoted-property", Compile;
InvalidEnumBackingType = "invalid-enum-backing-type", Compile;
EnumCaseNamedClass = "enum-case-named-class", Compile;
MissingEnumCaseValue = "missing-enum-case-value", Compile;
PureEnumCaseValue = "pure-enum-case-value", Compile;
InvalidTraitAliasModifier = "invalid-trait-alias-modifier", Compile;
UnterminatedComment = "unterminated-comment", Parse;
NestedClassDeclaration = "nested-class-declaration", Compile;
MixedNamespaceStyles = "mixed-namespace-styles", Compile;
NamespaceNotFirst = "namespace-not-first", Compile;
CodeOutsideNamespace = "code-outside-namespace", Compile;
NestedNamespace = "nested-namespace", Compile;
UseNameConflict = "use-name-conflict", Compile;
SpecialClassNameAlias = "special-class-name-alias", Compile;
DeclareNotFirstStatement = "declare-not-first-statement", Compile;
NonLiteralDeclareValue = "non-literal-declare-value", Compile;
InvalidStrictTypes = "invalid-strict-types", Compile;
AttributesOnMultipleConstants = "attributes-on-multiple-constants", Compile;
AttributeArgumentUnpacking = "attribute-argument-unpacking", Compile;
AttributeArgumentClosure = "attribute-argument-closure", Compile;
VoidReturnValue = "void-return-value", Compile;
NeverReturn = "never-return", Compile;
VariadicParameterDefault = "variadic-parameter-default", Compile;
VariadicParameterNotLast = "variadic-parameter-not-last", Compile;
ThisAsDeclaredVariable = "this-as-declared-variable", Compile;
SuperglobalAsDeclaredVariable = "superglobal-as-declared-variable", Compile;
DuplicateStaticVariable = "duplicate-static-variable", Compile;
BreakOutOfLoop = "break-out-of-loop", Compile;
InvalidBreakOperand = "invalid-break-operand", Compile;
JumpAcrossFinally = "jump-across-finally", Compile;
DuplicateLabel = "duplicate-label", Compile;
UndefinedGotoLabel = "undefined-goto-label", Compile;
GotoIntoLoop = "goto-into-loop", Compile;
DuplicateSwitchDefault = "duplicate-switch-default", Compile;
DuplicateMatchDefault = "duplicate-match-default", Compile;
ReferenceKey = "reference-key", Compile;
ClassKeywordInCatch = "class-keyword-in-catch", Compile;
InvalidParentHookCall = "invalid-parent-hook-call", Compile;
StaticClassInConstantExpression = "static-class-in-constant-expression", Compile;
AppendOutsideWrite = "append-outside-write", Compile;
InvalidGlobalsWrite = "invalid-globals-write", Compile;
InvalidNullableType = "invalid-nullable-type", Compile;
StandaloneTypeInComposite = "standalone-type-in-composite", Compile;
BuiltinTypeInIntersection = "builtin-type-in-intersection", Compile;
RedundantType = "redundant-type", Compile;
DuplicateType = "duplicate-type", Compile;
InvalidParameterType = "invalid-parameter-type", Compile;
InvalidMemberType = "invalid-member-type", Compile;
QualifiedBuiltinType = "qualified-builtin-type", Compile;
ReservedTypeName = "reserved-type-name", Compile;
IssetOnExpression = "isset-on-expression", Compile;
ClosureForNew = "closure-for-new", Compile;
YieldOutsideFunction = "yield-outside-function", Compile;
YieldFromInByReferenceGenerator = "yield-from-in-by-reference-generator", Compile;
PositionalAfterNamedArgument = "positional-after-named-argument", Compile;
UnsetCast = "unset-cast", Compile;
BuiltinResultWrite = "builtin-result-write", Compile;
TemporaryExpressionWrite = "temporary-expression-write", Compile;
NullsafeWrite = "nullsafe-write", Compile;
ReferenceToNullsafe = "reference-to-nullsafe", Compile;
NullsafeClosureCreation = "nullsafe-closure-creation", Compile;
UnwritableDestructuringElement = "unwritable-destructuring-element", Compile;
UnsetThis = "unset-this", Compile;
ClosureUseShadowsParameter = "closure-use-shadows-parameter", Compile;
DuplicateClosureUseVariable = "duplicate-closure-use-variable", Compile;
LiteralClassName = "literal-class-name", Compile;
ClassFetchOnLiteral = "class-fetch-on-literal", Compile;
DynamicClassFetch = "dynamic-class-fetch", Compile;
InvalidConstantExpression = "invalid-constant-expression", Compile;
EmptyList = "empty-list", Compile;
EmptyKeyedArrayEntry = "empty-keyed-array-entry", Compile;
MixedKeyedEntries = "mixed-keyed-entries", Compile;
MixedListSyntax = "mixed-list-syntax", Compile;
SpreadInDestructuring = "spread-in-destructuring", Compile;
UnparenthesizedNestedTernary = "unparenthesized-nested-ternary", Compile;
UnparenthesizedArrowInPipe = "unparenthesized-arrow-in-pipe", Compile;
InvalidCurlyInterpolation = "invalid-curly-interpolation", Compile;
RepeatedAttribute = "repeated-attribute", Compile;
NoDiscardOnInvalidTarget = "no-discard-on-invalid-target", Compile;
NoDiscardWithoutReturnValue = "no-discard-without-return-value", Compile;
Other = "other", Compile;
}
impl std::fmt::Display for ForbiddenRule {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl std::str::FromStr for ForbiddenRule {
type Err = UnknownForbiddenRule;
fn from_str(s: &str) -> Result<Self, Self::Err> {
ForbiddenRule::ALL
.iter()
.copied()
.find(|rule| rule.as_str() == s)
.ok_or(UnknownForbiddenRule)
}
}
#[cfg(test)]
mod tests {
use super::{ForbiddenRule, ParseError, Span};
use crate::PhpVersion;
const VERSION_GATED: &[(ForbiddenRule, PhpVersion, &str, &str)] = &[
(
ForbiddenRule::NeverReturn,
PhpVersion::Php81,
"<?php $f = fn(): never => 1;",
"1",
),
(
ForbiddenRule::DynamicClassFetch,
PhpVersion::Php74,
"<?php $a::class;",
"$a",
),
(
ForbiddenRule::QualifiedBuiltinType,
PhpVersion::Php85,
"<?php class A { function m(): namespace\\static {} }",
"namespace\\static",
),
(
ForbiddenRule::QualifiedBuiltinType,
PhpVersion::Php85,
"<?php function f(): \\static {}",
"\\static",
),
(
ForbiddenRule::QualifiedClassKeyword,
PhpVersion::Php84,
"<?php function f(): \\static {}",
"\\static",
),
];
fn samples(rule: ForbiddenRule) -> &'static [(&'static str, &'static str)] {
match rule {
ForbiddenRule::ClassKeywordOutsideClass => &[
("<?php function f() { self::g(); }", "self"),
("<?php function f() { return parent::$x::get(); }", "parent"),
],
ForbiddenRule::ParentWithoutParentClass => {
&[("<?php class A { function f() { parent::f(); } }", "parent")]
}
ForbiddenRule::ToStringReturnType => &[(
"<?php class A { function __toString(): int { return 1; } }",
"int",
)],
ForbiddenRule::MagicMethodReturnType => &[
(
"<?php class A { function __isset($n): int { return 1; } }",
"int",
),
("<?php class A { function __construct(): void {} }", "void"),
],
ForbiddenRule::AttributeTargetMismatch => {
&[("<?php #[Override] class A {}", "Override")]
}
ForbiddenRule::AttributeOnInvalidClass => {
&[("<?php #[Attribute] trait T {}", "Attribute")]
}
ForbiddenRule::DeprecatedOnInvalidTarget => {
&[("<?php #[Deprecated] class A {}", "Deprecated")]
}
ForbiddenRule::AllowDynamicPropertiesOnInvalidClass => &[(
"<?php #[AllowDynamicProperties] interface I {}",
"AllowDynamicProperties",
)],
ForbiddenRule::MissingReturnValue => {
&[("<?php function f(): int { return; }", "return;")]
}
ForbiddenRule::GeneratorReturnType => &[
("<?php function f(): int { yield 1; }", "int"),
(
"<?php class A { public int $x { set { yield 1; } } }",
"set { yield 1; }",
),
],
ForbiddenRule::ReadonlyPropertyDefault => {
&[("<?php class A { public readonly int $x = 1; }", "1")]
}
ForbiddenRule::DuplicateParameter => &[("<?php function f($a, $a) {}", "$a")],
ForbiddenRule::ReassignThis => &[
("<?php function f() { $this = 1; }", "$this"),
("<?php try {} catch (E $this) {}", "$this"),
],
ForbiddenRule::UnclosedInterpolation => &[("<?php echo \"{$a\";", "{$a\";"), ("<?php echo \"$a[0\";", "[0"), ("<?php echo \"${a\";", "${a\";"),],
ForbiddenRule::EmptyInterpolationIndex => &[("<?php echo \"$a[]\";", "[]"), ("<?php echo \"${a[]}\";", "[]"),],
ForbiddenRule::EmptyInterpolationVariableName => &[("<?php echo \"${}\";", "${}"),],
ForbiddenRule::UnclosedInterpolationIndex => &[("<?php echo \"${a[0]x}\";", "${a[0]x}"),],
ForbiddenRule::ClassConstantInterpolation => &[("<?php echo \"{$a::B}\";", "$a::B"),],
ForbiddenRule::InvalidInterpolationOffset => &[("<?php echo \"$a[ 1]\";", " 1"),],
ForbiddenRule::InvalidNumericLiteral => &[("<?php echo 08;", "08"), ("<?php echo 1_;", "1_"),],
ForbiddenRule::InvalidCodepointEscape => &[("<?php echo \"\\u{}\";", "\\u{}"), ("<?php echo \"\\u{110000}\";", "\\u{110000}"),],
ForbiddenRule::MixedHeredocIndentation => &[("<?php echo <<<A\n \tx\n \tA;\n", " \t"),],
ForbiddenRule::HeredocBodyIndentation => &[("<?php echo <<<A\nx\n A;\n", "x"),],
ForbiddenRule::WhitespaceInNamespacedName => &[("<?php echo A \\ B;", "A \\ B"),],
ForbiddenRule::MultipleVisibilityModifiers => &[("<?php class A { public private $x; }", "public private"), ("<?php function f() {} class B { function __construct(public private $x) {} }", "public"),],
ForbiddenRule::MultipleSetVisibilityModifiers => &[("<?php class A { public private(set) protected(set) int $x; }", "public private(set) protected"), ("<?php class B { function __construct(public private(set) protected(set) int $x) {} }", "public private(set)"),],
ForbiddenRule::DuplicateModifier => &[("<?php class A { static static $x; }", "static"), ("<?php class A { public $x { final final get => 1; } }", "final"), ("<?php class B { function __construct(readonly readonly int $x) {} }", "readonly"),],
ForbiddenRule::AbstractFinalConflict => &[("<?php abstract final class A {}", "abstract final"), ("<?php abstract class A { abstract final function f(); }", "abstract final"),],
ForbiddenRule::InvalidConstantModifier => &[("<?php class A { static const X = 1; }", "const"), ("<?php enum E { static const X = 1; }", "const"),],
ForbiddenRule::ModifierNotAllowed => &[("<?php function f(static $x) {}", "static"), ("<?php class A { readonly function f() {} }", "readonly function f() {}"), ("<?php trait T { function f() {} } class A { use T { f as readonly g; } }", "readonly"),],
ForbiddenRule::KeywordAsDeclarationName => &[("<?php function list() {}", "list"), ("<?php interface list {}", "list"), ("<?php #[A] enum list {}", "list"),],
ForbiddenRule::ReservedClassName => &[("<?php class int {}", "int"),],
ForbiddenRule::OutermostScopeOnly => &[("<?php function f() { __halt_compiler(); }", "__halt_compiler"),],
ForbiddenRule::UnparenthesizedIntersectionInUnion => &[("<?php function f((A|B)&C $x) {}", "(A|B)"),],
ForbiddenRule::ParenthesizedIntersectionOutsideUnion => &[("<?php function f((A&B) $x) {}", "(A&B)"),],
ForbiddenRule::StaticTypePosition => &[("<?php class A { public ?static $p; }", "static"), ("<?php function f(?static $x) {}", "static"),],
ForbiddenRule::EmptyAttributeList => &[("<?php #[] function f() {}", "#[]"),],
ForbiddenRule::CallTimePassByReference => &[("<?php f(&$x);", "&"),],
ForbiddenRule::CurlyOffsetAccess => &[("<?php echo $a{0};", "{"),],
ForbiddenRule::VoidCastInExpression => &[("<?php $x = (void) f();", "(void) f()"),],
ForbiddenRule::RealCast => &[("<?php $x = (real) 1;", "real"),],
ForbiddenRule::NonAssociativeChain => &[("<?php $x = 1 < 2 < 3;", "<"),],
ForbiddenRule::IncDecAsAssignTarget => &[("<?php $a++ = 1;", "="),],
ForbiddenRule::InvalidAssignTarget => &[("<?php 1 = 2;", "="),],
ForbiddenRule::CompoundAssignToDestructuring => &[("<?php [$a] += 1;", "+="),],
ForbiddenRule::ReferenceAssignToDestructuring => &[("<?php [$a] = &$b;", "&"),],
ForbiddenRule::IncDecNonVariable => &[("<?php f()++;", "f()"),],
ForbiddenRule::InvalidDereference => &[("<?php echo 1[0];", "1"),],
ForbiddenRule::InvalidWriteContext => &[("<?php unset(f());", "f()"),],
ForbiddenRule::ListOutsideDestructuring => &[("<?php $x = list($a);", "list($a)"),],
ForbiddenRule::EmptyArrayElement => &[("<?php $x = [1, , 2];", ","),],
ForbiddenRule::ReferenceAssignNew => &[("<?php $x = &new A;", "new A"),],
ForbiddenRule::HooksOnPropertyList => &[("<?php class A { public $x, $y { get => 1; } }", "{"),],
ForbiddenRule::InvalidInstanceofOperand => &[("<?php $x instanceof 1;", "1"),],
ForbiddenRule::SwitchLeadingSemicolons => &[("<?php switch ($x) {;; case 1: }", ";"),],
ForbiddenRule::PartialApplicationInNew => &[("<?php new A(?);", "?"),],
ForbiddenRule::UnparenthesizedNewDereference => &[("<?php new A::B;", "::"),],
ForbiddenRule::StaticStandaloneExpression => &[("<?php static;", "static"),],
ForbiddenRule::ReferenceToParenthesized => &[("<?php $x = &($y);", "($y)"),],
ForbiddenRule::ReferenceToNonReferenceable => &[("<?php $x = &1;", "1"), ("<?php [&$a] = 1;", "1"),],
ForbiddenRule::QualifiedClassKeyword => &[("<?php try {} catch (\\self $e) {}", "\\self"), ("<?php function f() { return \\self::X; }", "\\self"), ("<?php function f(\\self $x) {}", "\\self"), ("<?php class A extends \\self {}", "\\self"),],
ForbiddenRule::ReservedClassReference => &[("<?php class A extends self {}", "self"),],
ForbiddenRule::ReservedEnumName => &[("<?php enum int {}", "int"),],
ForbiddenRule::TraitInInterface => &[("<?php interface I { use T; }", "use T;"),],
ForbiddenRule::AbstractMemberInConcreteClass => &[("<?php class A { abstract function f(); }", "abstract function f();"), ("<?php class A { abstract public $x { get; } }", "abstract public $x { get; }"),],
ForbiddenRule::DuplicateProperty => &[("<?php class A { public $x; public $x; }", "public $x"), ("<?php class A { public $x; function __construct(public $x) {} }", "public $x"),],
ForbiddenRule::DuplicateMethod => &[("<?php class A { function f() {} function F() {} }", "function F() {}"),],
ForbiddenRule::DuplicateClassConstant => &[("<?php class A { const X = 1; const X = 2; }", "X"),],
ForbiddenRule::ClassConstantNamedClass => &[("<?php class A { const class = 1; }", "class"),],
ForbiddenRule::ClassConstantValueType => &[("<?php class A { const int X = \"a\"; }", "\"a\""),],
ForbiddenRule::PrivateFinalConstant => &[("<?php class A { final private const X = 1; }", "final private const X = 1;"), ("<?php enum E { final private const X = 1; }", "const"),],
ForbiddenRule::InvalidInterfaceConstantModifier => &[("<?php interface I { private const X = 1; }", "private const X = 1;"),],
ForbiddenRule::InvalidInterfaceMethodModifier => &[("<?php interface I { private function f(); }", "private function f();"), ("<?php interface I { final function f(); }", "final function f();"), ("<?php interface I { abstract function f(); }", "abstract function f();"),],
ForbiddenRule::InvalidInterfacePropertyModifier => &[("<?php interface I { final public $x { get; } }", "final public $x"),],
ForbiddenRule::InterfaceMethodBody => &[("<?php interface I { function f() {} }", "function f() {}"),],
ForbiddenRule::MissingMethodBody => &[("<?php class A { function f(); }", "function f();"),],
ForbiddenRule::AbstractMethodBody => &[("<?php abstract class A { abstract function f() {} }", "abstract function f() {}"),],
ForbiddenRule::PrivateAbstractMethod => &[("<?php abstract class A { abstract private function f(); }", "abstract private function f();"),],
ForbiddenRule::AbstractAnonymousClassMethod => &[("<?php new class { abstract function f(); };", "abstract function f();"),],
ForbiddenRule::AbstractEnumMethod => &[("<?php enum E { abstract function f(); }", "abstract"),],
ForbiddenRule::MagicMethodArgumentCount => &[("<?php class A { function __get() {} }", "function __get() {}"),],
ForbiddenRule::MagicMethodByReference => &[("<?php class A { function __get(&$n) {} }", "&$n"),],
ForbiddenRule::MagicMethodStatic => &[("<?php class A { static function __get($n) {} }", "static function __get($n) {}"),],
ForbiddenRule::MagicMethodParameterType => &[("<?php class A { function __get(int $n) {} }", "int"),],
ForbiddenRule::StaticReadonlyProperty => &[("<?php class A { public static readonly int $x; }", "public static readonly"), ("<?php class A { public readonly static int $x; }", "public readonly static"),],
ForbiddenRule::UntypedReadonlyProperty => &[("<?php class A { public readonly $x; }", "public readonly $x"), ("<?php class A { function __construct(public readonly $x) {} }", "public readonly"),],
ForbiddenRule::ReadonlyPropertyHooks => &[("<?php class A { public readonly int $x { get => 1; } }", "get => 1;"), ("<?php class A { function __construct(public readonly int $x { get => 1; }) {} }", "get => 1;"),],
ForbiddenRule::UntypedAsymmetricProperty => &[("<?php class A { public private(set) $x; }", "public private(set) $x"), ("<?php class A { function __construct(public private(set) $x) {} }", "public private(set) $x"),],
ForbiddenRule::SetVisibilityWiderThanVisibility => &[("<?php class A { private public(set) int $x; }", "private public(set) int $x"), ("<?php class A { function __construct(private public(set) int $x) {} }", "private public(set)"),],
ForbiddenRule::PrivateFinalProperty => &[("<?php class A { final private $x; }", "final private $x"),],
ForbiddenRule::AbstractPropertyWithoutAbstractHook => &[("<?php abstract class A { abstract public $x; }", "abstract public $x"), ("<?php abstract class A { abstract public $x { get => 1; } }", "abstract public $x { get => 1; }"),],
ForbiddenRule::InterfacePropertyWithoutHooks => &[("<?php interface I { public $x; }", "public $x"),],
ForbiddenRule::StaticPropertyHooks => &[("<?php class A { public static $x { get => 1; } }", "public static $x { get => 1; }"),],
ForbiddenRule::VirtualPropertyDefault => &[("<?php class A { public int $x = 1 { get => 2; } }", "public int $x = 1 { get => 2; }"),],
ForbiddenRule::VirtualPropertyAsymmetricVisibility => &[("<?php class A { public private(set) int $x { get => 1; } }", "public private(set) int $x { get => 1; }"),],
ForbiddenRule::EmptyHookList => &[("<?php class A { public $x {} }", "{"),],
ForbiddenRule::DuplicateHook => &[("<?php class A { public $x { get => 1; get => 2; } }", "get => 2;"), ("<?php class A { public $x { set {} set {} } }", "set {}"),],
ForbiddenRule::GetHookParameters => &[("<?php class A { public $x { get() => 1; } }", "("),],
ForbiddenRule::SetHookParameterCount => &[("<?php class A { public $x { set($a, $b) {} } }", "("),],
ForbiddenRule::SetHookParameterByReference => &[("<?php class A { public $x { set(&$v) {} } }", "&$v"),],
ForbiddenRule::SetHookParameterVariadic => &[("<?php class A { public $x { set(...$v) {} } }", "...$v"),],
ForbiddenRule::SetHookParameterType => &[("<?php class A { public int $x { set(string $v) {} } }", "string $v"),],
ForbiddenRule::ByReferenceGetHookWithSet => &[("<?php class A { public $x { &get => $this->x; set { $this->x = $value; } } }", "&get => $this->x;"),],
ForbiddenRule::MissingHookBody => &[("<?php class A { public $x { get; } }", "get;"),],
ForbiddenRule::AbstractHookBody => &[("<?php interface I { public $x { get => 1; } }", "get => 1;"),],
ForbiddenRule::AbstractFinalHook => &[("<?php abstract class A { abstract public $x { final get; } }", "final get;"),],
ForbiddenRule::PrivateAbstractHook => &[("<?php abstract class A { abstract private $x { get; } }", "get;"),],
ForbiddenRule::PromotedPropertyOutsideConstructor => &[("<?php function f(public $x) {}", "public"), ("<?php class A { function f(public $x) {} }", "public"),],
ForbiddenRule::PromotedPropertyInAbstractConstructor => &[("<?php abstract class A { abstract function __construct(public $x); }", "public $x"),],
ForbiddenRule::VariadicPromotedProperty => &[("<?php class A { function __construct(public ...$x) {} }", "public ...$x"),],
ForbiddenRule::InvalidEnumBackingType => &[("<?php enum E: float {}", "float"),],
ForbiddenRule::EnumCaseNamedClass => &[("<?php enum E { case class; }", "class"),],
ForbiddenRule::MissingEnumCaseValue => &[("<?php enum E: int { case A; }", "A"),],
ForbiddenRule::PureEnumCaseValue => &[("<?php enum E { case A = 1; }", "="),],
ForbiddenRule::InvalidTraitAliasModifier => &[("<?php class A { use T { f as static g; } }", "static"),],
ForbiddenRule::UnterminatedComment => &[("<?php /* x", "/* x"),],
ForbiddenRule::NestedClassDeclaration => &[("<?php class A { function f() { class B {} } }", "B"),],
ForbiddenRule::MixedNamespaceStyles => &[("<?php namespace A; namespace B {}", "namespace B {}"),],
ForbiddenRule::NamespaceNotFirst => &[("<?php echo 1; namespace A;", "echo 1;"),],
ForbiddenRule::CodeOutsideNamespace => &[("<?php namespace A {} echo 1;", "echo 1;"),],
ForbiddenRule::NestedNamespace => &[("<?php namespace A { namespace B {} }", "namespace B {}"),],
ForbiddenRule::UseNameConflict => &[("<?php use A\\B; use C\\B;", "C\\B"),],
ForbiddenRule::SpecialClassNameAlias => &[("<?php use A as self;", "A as self"),],
ForbiddenRule::DeclareNotFirstStatement => &[("<?php echo 1; declare(strict_types=1);", "strict_types=1"), ("<?php echo 1; declare(encoding='UTF-8');", "encoding='UTF-8'"),],
ForbiddenRule::NonLiteralDeclareValue => &[("<?php declare(ticks=A);", "ticks=A"), ("<?php declare(encoding=A);", "encoding=A"),],
ForbiddenRule::InvalidStrictTypes => &[("<?php declare(strict_types=2);", "strict_types=2"), ("<?php declare(strict_types=1) {}", "strict_types=1"),],
ForbiddenRule::AttributesOnMultipleConstants => &[("<?php #[A] const X = 1, Y = 2;", "const X = 1, Y = 2;"),],
ForbiddenRule::AttributeArgumentUnpacking => &[("<?php #[A(...$a)] function f() {}", "...$a"),],
ForbiddenRule::AttributeArgumentClosure => &[("<?php #[A(...)] function f() {}", "(...)"),],
ForbiddenRule::VoidReturnValue => &[("<?php function f(): void { return 1; }", "return 1;"), ("<?php $f = fn(): void => 1;", "1"),],
ForbiddenRule::NeverReturn => &[("<?php function f(): never { return; }", "return;"),],
ForbiddenRule::VariadicParameterDefault => &[("<?php function f(...$a = []) {}", "...$a = []"),],
ForbiddenRule::VariadicParameterNotLast => &[("<?php function f(...$a, $b) {}", "$b"),],
ForbiddenRule::ThisAsDeclaredVariable => &[("<?php $f = function () use ($this) {};", "$this"), ("<?php function f($this) {}", "$this"), ("<?php function f() { global $this; }", "$this"), ("<?php function f() { static $this; }", "$this"),],
ForbiddenRule::SuperglobalAsDeclaredVariable => &[("<?php $f = function () use ($_GET) {};", "$_GET"), ("<?php function f($_GET) {}", "$_GET"),],
ForbiddenRule::DuplicateStaticVariable => &[("<?php function f() { static $a; static $a; }", "$a"),],
ForbiddenRule::BreakOutOfLoop => &[("<?php break;", "break"), ("<?php while (1) { break 2; }", "break"),],
ForbiddenRule::InvalidBreakOperand => &[("<?php while (1) { break $a; }", "break"), ("<?php while (1) { break 0; }", "break"),],
ForbiddenRule::JumpAcrossFinally => &[("<?php while (1) { try {} finally { break; } }", "break"), ("<?php try {} finally { goto a; } a:", "goto a;"),],
ForbiddenRule::DuplicateLabel => &[("<?php a: a:", "a:"),],
ForbiddenRule::UndefinedGotoLabel => &[("<?php goto a;", "goto a;"),],
ForbiddenRule::GotoIntoLoop => &[("<?php goto a; while (1) { a: }", "goto a;"),],
ForbiddenRule::DuplicateSwitchDefault => &[("<?php switch ($x) { default: default: }", "default"),],
ForbiddenRule::DuplicateMatchDefault => &[("<?php match ($x) { default => 1, default => 2 };", "default"),],
ForbiddenRule::ReferenceKey => &[("<?php foreach ($a as &$k => $v) {}", "&$k"),],
ForbiddenRule::ClassKeywordInCatch => &[("<?php try {} catch (self $e) {}", "self"),],
ForbiddenRule::InvalidParentHookCall => &[("<?php class A extends B { function f() { parent::$x::get(); } }", "parent::$x::get()"),],
ForbiddenRule::StaticClassInConstantExpression => &[("<?php class A { const X = static::class; }", "static"),],
ForbiddenRule::AppendOutsideWrite => &[("<?php $x = $a[];", "$a[]"), ("<?php unset($a[]);", "$a[]"),],
ForbiddenRule::InvalidGlobalsWrite => &[("<?php $GLOBALS[] = 1;", "$GLOBALS[]"), ("<?php $GLOBALS = [];", "$GLOBALS"), ("<?php $x = &$GLOBALS;", "$GLOBALS"),],
ForbiddenRule::InvalidNullableType => &[("<?php function f(?mixed $x) {}", "mixed"), ("<?php function f(?null $x) {}", "null"),],
ForbiddenRule::StandaloneTypeInComposite => &[("<?php function f(): ?void {}", "void"), ("<?php function f(): void|int {}", "void"),],
ForbiddenRule::BuiltinTypeInIntersection => &[("<?php function f(int&A $x) {}", "int"),],
ForbiddenRule::RedundantType => &[("<?php function f(object|A $x) {}", "object|A"), ("<?php function f((A&B)|A $x) {}", "A"), ("<?php function f(true|false $x) {}", "true|false"),],
ForbiddenRule::DuplicateType => &[("<?php function f(int|int $x) {}", "int"),],
ForbiddenRule::InvalidParameterType => &[("<?php function f(void $x) {}", "void"),],
ForbiddenRule::InvalidMemberType => &[("<?php class A { public callable $x; }", "callable"),],
ForbiddenRule::QualifiedBuiltinType => &[("<?php function f(\\int $x) {}", "\\int"),],
ForbiddenRule::ReservedTypeName => &[("<?php function f(A\\int $x) {}", "A\\int"),],
ForbiddenRule::IssetOnExpression => &[("<?php isset(f());", "f()"),],
ForbiddenRule::ClosureForNew => &[("<?php new A(...);", "(...)"), ("<?php new class(...) {};", "(...)"),],
ForbiddenRule::YieldOutsideFunction => &[("<?php yield 1;", "yield"),],
ForbiddenRule::YieldFromInByReferenceGenerator => &[("<?php function &f() { yield from g(); }", "yield from g()"),],
ForbiddenRule::PositionalAfterNamedArgument => &[("<?php f(a: 1, 2);", "2"),],
ForbiddenRule::UnsetCast => &[("<?php $x = (unset) 1;", "unset"),],
ForbiddenRule::BuiltinResultWrite => &[("<?php (clone $a)->b = 1;", "(clone $a)->b"), ("<?php $x = &f(...);", "f(...)"),],
ForbiddenRule::TemporaryExpressionWrite => &[("<?php (1)[0] = 2;", "(1)[0]"), ("<?php (1)[0]++;", "(1)[0]"), ("<?php unset((1)[0]);", "(1)[0]"), ("<?php $x = &(1)[0];", "(1)[0]"),],
ForbiddenRule::NullsafeWrite => &[("<?php $a?->b = 1;", "$a?->b"), ("<?php $a?->b++;", "$a?->b"), ("<?php unset($a?->b);", "$a?->b"),],
ForbiddenRule::ReferenceToNullsafe => &[("<?php $x = &$a?->b;", "$a?->b"), ("<?php [&$x] = $a?->b;", "$a?->b"), ("<?php function &f() { return $a?->b; }", "$a?->b"),],
ForbiddenRule::NullsafeClosureCreation => &[("<?php $a?->b(...);", "$a?->b(...)"),],
ForbiddenRule::UnwritableDestructuringElement => &[("<?php [f()] = $a;", "f()"), ("<?php foreach ($a as [f()]) {}", "[f()]"),],
ForbiddenRule::UnsetThis => &[("<?php function f() { unset($this); }", "$this"),],
ForbiddenRule::ClosureUseShadowsParameter => &[("<?php $f = function ($a) use ($a) {};", "$a"),],
ForbiddenRule::DuplicateClosureUseVariable => &[("<?php $f = function () use ($a, $a) {};", "$a"),],
ForbiddenRule::LiteralClassName => &[("<?php (1)::A;", "(1)"), ("<?php (1)::class;", "(1)"),],
ForbiddenRule::ClassFetchOnLiteral => &[("<?php (null)::class;", "(null)"),],
ForbiddenRule::InvalidConstantExpression => &[("<?php const X = $a;", "$a"),],
ForbiddenRule::EmptyList => &[("<?php [] = $a;", "[]"),],
ForbiddenRule::EmptyKeyedArrayEntry => &[("<?php ['a' => $y, , 'b' => $z] = $a;", ","),],
ForbiddenRule::MixedKeyedEntries => &[("<?php ['a' => $x, $y] = $a;", "$y"),],
ForbiddenRule::MixedListSyntax => &[("<?php [list($a)] = $b;", "list($a)"),],
ForbiddenRule::SpreadInDestructuring => &[("<?php [...$a] = $b;", "...$a"),],
ForbiddenRule::UnparenthesizedNestedTernary => &[("<?php $x = 1 ? 2 : 3 ? 4 : 5;", "?"),],
ForbiddenRule::UnparenthesizedArrowInPipe => &[("<?php $x = 1 |> fn($a) => $a;", "fn($a) => $a"),],
ForbiddenRule::InvalidCurlyInterpolation => &[("<?php echo \"{$a + 1}\";", "$a + 1"),],
ForbiddenRule::RepeatedAttribute => &[("<?php #[Override, Override] function f() {}", "Override"),],
ForbiddenRule::NoDiscardOnInvalidTarget => &[("<?php class A { public $x { #[NoDiscard] get => 1; } }", "NoDiscard"), ("<?php class A { #[NoDiscard] function __construct() {} }", "NoDiscard"),],
ForbiddenRule::NoDiscardWithoutReturnValue => &[("<?php #[NoDiscard] function f(): void {}", "NoDiscard"),],
ForbiddenRule::DynamicClassFetch | ForbiddenRule::Other => &[],
}
}
#[test]
fn each_rule_reports_its_span() {
for &rule in ForbiddenRule::ALL {
if rule == ForbiddenRule::Other {
continue;
}
let gated = VERSION_GATED.iter().any(|&(r, ..)| r == rule);
assert!(gated || !samples(rule).is_empty(), "{rule:?} has no sample");
for &(src, text) in samples(rule) {
let errors = crate::parse(src).errors;
let spans: Vec<&str> = errors
.iter()
.filter(|e| e.forbidden_rule() == Some(rule))
.map(|e| &src[e.span().start as usize..e.span().end as usize])
.collect();
assert_eq!(spans, [text], "{rule:?} in {src:?}: {errors:?}");
}
}
}
#[test]
fn version_gated_rules_report_their_span() {
for &(rule, version, src, text) in VERSION_GATED {
let options = crate::ParseOptions::default().version(version);
let errors = crate::parse_with(src, &options).errors;
let spans: Vec<&str> = errors
.iter()
.filter(|e| e.forbidden_rule() == Some(rule))
.map(|e| &src[e.span().start as usize..e.span().end as usize])
.collect();
assert_eq!(spans, [text], "{rule:?} in {src:?}: {errors:?}");
}
}
#[test]
fn relative_static_type_is_valid_in_a_class_before_8_5() {
let options = crate::ParseOptions::default().version(PhpVersion::Php84);
let src = "<?php class A { function m(): namespace\\static {} }";
assert_eq!(crate::parse_with(src, &options).errors.len(), 0);
}
#[test]
fn identifiers_are_unique_and_parse_back() {
let mut ids: Vec<&str> = ForbiddenRule::ALL.iter().map(|r| r.as_str()).collect();
ids.sort_unstable();
ids.dedup();
assert_eq!(ids.len(), ForbiddenRule::ALL.len());
for &rule in ForbiddenRule::ALL {
assert_eq!(rule.as_str().parse(), Ok(rule));
}
assert_eq!(
"no-such-rule".parse::<ForbiddenRule>(),
Err(super::UnknownForbiddenRule)
);
}
#[test]
fn codes_are_unique_and_only_syntax_errors_leave_the_ast_incomplete() {
let span = Span::new(0, 0);
let incomplete = [
ParseError::Expected {
expected: "';'".into(),
found: "'}'".into(),
span,
},
ParseError::UnterminatedString { span },
ParseError::ExpectedAfter {
expected: "';'".into(),
after: "expression".into(),
span,
},
ParseError::UnclosedDelimiter {
delimiter: "')'".into(),
opened_at: span,
span,
},
ParseError::NestingTooDeep { span },
ParseError::SourceTooLarge { len: 0, span },
];
let complete = [
ParseError::Forbidden {
rule: ForbiddenRule::ReassignThis,
message: "Cannot re-assign $this".into(),
span,
},
ParseError::VersionTooLow {
feature: "match".into(),
required: PhpVersion::Php80,
used: PhpVersion::Php74,
span,
},
];
assert!(incomplete.iter().all(ParseError::leaves_ast_incomplete));
assert!(!complete.iter().any(ParseError::leaves_ast_incomplete));
assert_eq!(complete[0].code(), "reassign-this");
let mut codes: Vec<&str> = ForbiddenRule::ALL.iter().map(|r| r.as_str()).collect();
codes.extend(
incomplete
.iter()
.chain(&complete[1..])
.map(ParseError::code),
);
let count = codes.len();
codes.sort_unstable();
codes.dedup();
assert_eq!(codes.len(), count);
}
#[test]
fn rule_adds_no_size() {
assert_eq!(std::mem::size_of::<ParseError>(), 64);
}
}