Skip to main content

php_rs_parser/
diagnostics.rs

1use crate::PhpVersion;
2use php_ast::Span;
3use std::borrow::Cow;
4use thiserror::Error;
5
6/// A parse error or diagnostic emitted during parsing.
7///
8/// The parser recovers from all errors and always produces a complete AST,
9/// so errors are informational rather than fatal. Each variant carries a
10/// [`Span`] identifying the source location.
11#[derive(Debug, Clone, Error)]
12#[non_exhaustive]
13pub enum ParseError {
14    /// A specific token was expected but a different one was found.
15    #[error("expected {expected}, found {found}")]
16    #[non_exhaustive]
17    Expected {
18        /// Description of the expected token.
19        expected: Cow<'static, str>,
20        /// Description of the token actually found, e.g. `"';'"` or
21        /// `"identifier"`. A description rather than the lexer's token kind,
22        /// so that new tokens are not a breaking change for this type.
23        found: Cow<'static, str>,
24        /// Source range of the error.
25        span: Span,
26    },
27
28    /// A string literal was opened but never closed.
29    #[error("unterminated string literal")]
30    #[non_exhaustive]
31    UnterminatedString {
32        /// Source range of the error.
33        span: Span,
34    },
35
36    /// A required token was missing after another construct.
37    #[error("expected {expected} after {after}")]
38    #[non_exhaustive]
39    ExpectedAfter {
40        /// Description of the expected token.
41        expected: Cow<'static, str>,
42        /// Construct the token should follow.
43        after: Cow<'static, str>,
44        /// Source range of the error.
45        span: Span,
46    },
47
48    /// A delimiter (parenthesis, bracket, brace) was opened but never closed.
49    #[error("missing closing {delimiter}")]
50    #[non_exhaustive]
51    UnclosedDelimiter {
52        /// The expected closing delimiter, e.g. `"')'"`.
53        delimiter: Cow<'static, str>,
54        /// Where the delimiter was opened.
55        opened_at: Span,
56        /// Source range of the error.
57        span: Span,
58    },
59
60    /// A closing delimiter with no delimiter open, such as a `}` after the last block.
61    #[error("unmatched {delimiter}")]
62    #[non_exhaustive]
63    UnmatchedDelimiter {
64        /// The closing delimiter, e.g. `"'}'"`.
65        delimiter: Cow<'static, str>,
66        /// Source range of the error.
67        span: Span,
68    },
69
70    /// A construct that is syntactically valid but semantically forbidden
71    /// (e.g. `(unset)` cast, deprecated syntax). Equivalent to a PHP fatal.
72    #[error("{message}")]
73    #[non_exhaustive]
74    Forbidden {
75        /// Rule violated.
76        rule: ForbiddenRule,
77        /// Error message.
78        message: Cow<'static, str>,
79        /// Source range of the error.
80        span: Span,
81    },
82
83    /// Statements, expressions or string interpolations are nested deeper than
84    /// the parser's recursion budget. The innermost construct is replaced by an
85    /// error node so that parsing stays within a bounded stack.
86    #[error("maximum nesting depth exceeded")]
87    #[non_exhaustive]
88    NestingTooDeep {
89        /// Source range of the error.
90        span: Span,
91    },
92
93    /// The source is larger than 4 GiB, so byte offsets no longer fit in a
94    /// [`Span`]. Nothing is parsed and the program is empty.
95    #[error("source is {len} bytes; the maximum is 4 GiB")]
96    #[non_exhaustive]
97    SourceTooLarge {
98        /// Length of the rejected source in bytes.
99        len: usize,
100        /// Always empty (`0..0`).
101        span: Span,
102    },
103
104    /// Syntax that requires a newer PHP version than the targeted one.
105    /// Emitted by [`crate::parse_with`] when the source uses features
106    /// unavailable in the specified [`crate::PhpVersion`].
107    #[error("'{feature}' requires PHP {required} or higher")]
108    #[non_exhaustive]
109    VersionTooLow {
110        /// Name of the feature used.
111        feature: Cow<'static, str>,
112        /// Minimum PHP version required.
113        required: PhpVersion,
114        /// PHP version targeted.
115        used: PhpVersion,
116        /// Source range of the error.
117        span: Span,
118    },
119}
120
121impl ParseError {
122    /// Source range of the error.
123    pub fn span(&self) -> Span {
124        match self {
125            ParseError::Expected { span, .. }
126            | ParseError::UnterminatedString { span }
127            | ParseError::ExpectedAfter { span, .. }
128            | ParseError::UnclosedDelimiter { span, .. }
129            | ParseError::UnmatchedDelimiter { span, .. }
130            | ParseError::Forbidden { span, .. }
131            | ParseError::NestingTooDeep { span }
132            | ParseError::SourceTooLarge { span, .. }
133            | ParseError::VersionTooLow { span, .. } => *span,
134        }
135    }
136
137    /// Rule violated, for [`ParseError::Forbidden`].
138    pub fn forbidden_rule(&self) -> Option<ForbiddenRule> {
139        match self {
140            ParseError::Forbidden { rule, .. } => Some(*rule),
141            _ => None,
142        }
143    }
144
145    /// When PHP reports the error: [`Phase::Compile`] only for a compile-time [`ForbiddenRule`].
146    pub fn phase(&self) -> Phase {
147        self.forbidden_rule()
148            .map_or(Phase::Parse, ForbiddenRule::phase)
149    }
150
151    /// Stable kebab-case identifier: the variant's, or the rule's for [`ParseError::Forbidden`].
152    pub fn code(&self) -> &'static str {
153        match self {
154            ParseError::Expected { .. } => "expected",
155            ParseError::UnterminatedString { .. } => "unterminated-string",
156            ParseError::ExpectedAfter { .. } => "expected-after",
157            ParseError::UnclosedDelimiter { .. } => "unclosed-delimiter",
158            ParseError::UnmatchedDelimiter { .. } => "unmatched-delimiter",
159            ParseError::Forbidden { rule, .. } => rule.as_str(),
160            ParseError::NestingTooDeep { .. } => "nesting-too-deep",
161            ParseError::SourceTooLarge { .. } => "source-too-large",
162            ParseError::VersionTooLow { .. } => "version-too-low",
163        }
164    }
165
166    /// Whether the AST around the error is a recovery guess rather than what the source says.
167    pub fn leaves_ast_incomplete(&self) -> bool {
168        !matches!(
169            self,
170            ParseError::Forbidden { .. } | ParseError::VersionTooLow { .. }
171        )
172    }
173}
174
175/// When PHP reports an error, which `php -l` prints as a parse error or a fatal error.
176#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
177pub enum Phase {
178    /// While lexing or parsing.
179    Parse,
180    /// While compiling.
181    Compile,
182}
183
184/// The identifier given to `ForbiddenRule::from_str` names no rule.
185#[derive(Debug, Clone, PartialEq, Eq, Error)]
186#[error("unknown forbidden rule")]
187pub struct UnknownForbiddenRule;
188
189macro_rules! forbidden_rules {
190    ($($(#[$doc:meta])* $rule:ident = $id:literal, $phase:ident;)*) => {
191        /// The PHP rule a [`ParseError::Forbidden`] reports.
192        ///
193        /// Rules and their identifiers are stable API; message wording is not.
194        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
195        #[non_exhaustive]
196        pub enum ForbiddenRule {
197            $($(#[$doc])* $rule,)*
198        }
199
200        impl ForbiddenRule {
201            /// Every rule, in declaration order.
202            pub const ALL: &'static [ForbiddenRule] = &[$(ForbiddenRule::$rule),*];
203
204            /// Stable kebab-case identifier, e.g. `"reassign-this"`.
205            pub fn as_str(self) -> &'static str {
206                match self {
207                    $(ForbiddenRule::$rule => $id,)*
208                }
209            }
210
211            /// When PHP reports the rule.
212            pub fn phase(self) -> Phase {
213                match self {
214                    $(ForbiddenRule::$rule => Phase::$phase,)*
215                }
216            }
217        }
218    };
219}
220
221forbidden_rules! {
222    /// `self`, `static` or `parent` used outside a class.
223    ClassKeywordOutsideClass = "class-keyword-outside-class", Compile;
224    /// `parent` used in a class that has no parent.
225    ParentWithoutParentClass = "parent-without-parent-class", Compile;
226    /// `__toString()` declares a return type other than `string`.
227    ToStringReturnType = "to-string-return-type", Compile;
228    /// A magic method other than `__toString()` declares a return type it may not have.
229    MagicMethodReturnType = "magic-method-return-type", Compile;
230    /// An internal attribute is applied to a target it does not allow.
231    AttributeTargetMismatch = "attribute-target-mismatch", Compile;
232    /// `#[Attribute]` is applied to an interface, trait, enum or abstract class.
233    AttributeOnInvalidClass = "attribute-on-invalid-class", Compile;
234    /// `#[Deprecated]` is applied to a class-like declaration.
235    DeprecatedOnInvalidTarget = "deprecated-on-invalid-target", Compile;
236    /// `#[AllowDynamicProperties]` is applied to an interface, trait, enum or readonly class.
237    AllowDynamicPropertiesOnInvalidClass = "allow-dynamic-properties-on-invalid-class", Compile;
238    /// A bare `return;` in a function that declares a return type.
239    MissingReturnValue = "missing-return-value", Compile;
240    /// A generator declares a return type that `Generator` does not satisfy.
241    GeneratorReturnType = "generator-return-type", Compile;
242    /// A readonly property has a default value.
243    ReadonlyPropertyDefault = "readonly-property-default", Compile;
244    /// Two parameters share a name.
245    DuplicateParameter = "duplicate-parameter", Compile;
246    /// `$this` is assigned or used as a `catch` variable.
247    ReassignThis = "reassign-this", Compile;
248    /// A `[`, `{` or `${` in an interpolated string is never closed.
249    UnclosedInterpolation = "unclosed-interpolation", Parse;
250    /// An interpolated array index is empty.
251    EmptyInterpolationIndex = "empty-interpolation-index", Parse;
252    /// A `${}` interpolation names no variable.
253    EmptyInterpolationVariableName = "empty-interpolation-variable-name", Parse;
254    /// The index in a `${name[...]}` interpolation is not closed.
255    UnclosedInterpolationIndex = "unclosed-interpolation-index", Parse;
256    /// A class constant is interpolated with `{$...}`.
257    ClassConstantInterpolation = "class-constant-interpolation", Parse;
258    /// A simple interpolation `"$a[...]"` has an invalid offset.
259    InvalidInterpolationOffset = "invalid-interpolation-offset", Parse;
260    /// A numeric literal has invalid digits, e.g. `08`.
261    InvalidNumericLiteral = "invalid-numeric-literal", Parse;
262    /// A `\u{...}` escape is empty or out of range.
263    InvalidCodepointEscape = "invalid-codepoint-escape", Parse;
264    /// A heredoc's closing indentation mixes tabs and spaces.
265    MixedHeredocIndentation = "mixed-heredoc-indentation", Parse;
266    /// A heredoc line is indented less than its closing marker.
267    HeredocBodyIndentation = "heredoc-body-indentation", Parse;
268    /// A namespaced name contains whitespace.
269    WhitespaceInNamespacedName = "whitespace-in-namespaced-name", Parse;
270    /// A member has more than one visibility modifier.
271    MultipleVisibilityModifiers = "multiple-visibility-modifiers", Parse;
272    /// A property has more than one set-visibility modifier.
273    MultipleSetVisibilityModifiers = "multiple-set-visibility-modifiers", Parse;
274    /// A modifier is repeated.
275    DuplicateModifier = "duplicate-modifier", Parse;
276    /// A declaration is both `abstract` and `final`.
277    AbstractFinalConflict = "abstract-final-conflict", Parse;
278    /// A class constant has a modifier constants cannot have.
279    InvalidConstantModifier = "invalid-constant-modifier", Parse;
280    /// A modifier is used where it cannot apply, e.g. `static` on a parameter.
281    ModifierNotAllowed = "modifier-not-allowed", Parse;
282    /// A reserved keyword names a declaration.
283    KeywordAsDeclarationName = "keyword-as-declaration-name", Parse;
284    /// A function is named `assert`, which PHP compiles specially.
285    CustomAssertFunction = "custom-assert-function", Compile;
286    /// A reserved type name names a class.
287    ReservedClassName = "reserved-class-name", Parse;
288    /// A statement that must be at the outermost scope is nested.
289    OutermostScopeOnly = "outermost-scope-only", Parse;
290    /// An intersection type is used in a union without parentheses.
291    UnparenthesizedIntersectionInUnion = "unparenthesized-intersection-in-union", Parse;
292    /// A parenthesized intersection type is not part of a union.
293    ParenthesizedIntersectionOutsideUnion = "parenthesized-intersection-outside-union", Parse;
294    /// `static` is used as a type where it cannot be.
295    StaticTypePosition = "static-type-position", Parse;
296    /// An attribute group `#[]` is empty.
297    EmptyAttributeList = "empty-attribute-list", Parse;
298    /// An argument is passed as `&$x`.
299    CallTimePassByReference = "call-time-pass-by-reference", Parse;
300    /// An offset is read with curly braces, `$a{0}`.
301    CurlyOffsetAccess = "curly-offset-access", Parse;
302    /// A `(void)` cast is used as an expression.
303    VoidCastInExpression = "void-cast-in-expression", Parse;
304    /// A `(real)` cast is used.
305    RealCast = "real-cast", Parse;
306    /// Non-associative operators are chained, e.g. `1 < 2 < 3`.
307    NonAssociativeChain = "non-associative-chain", Parse;
308    /// An increment or decrement is assigned to.
309    IncDecAsAssignTarget = "inc-dec-as-assign-target", Parse;
310    /// An expression that cannot be assigned to is assigned to.
311    InvalidAssignTarget = "invalid-assign-target", Parse;
312    /// A destructuring pattern is used with a compound assignment.
313    CompoundAssignToDestructuring = "compound-assign-to-destructuring", Parse;
314    /// A destructuring pattern is assigned by reference.
315    ReferenceAssignToDestructuring = "reference-assign-to-destructuring", Parse;
316    /// `++` or `--` is applied to something other than a variable.
317    IncDecNonVariable = "inc-dec-non-variable", Parse;
318    /// An expression that cannot be dereferenced is dereferenced.
319    InvalidDereference = "invalid-dereference", Parse;
320    /// An expression that cannot be written to is written to.
321    InvalidWriteContext = "invalid-write-context", Parse;
322    /// `list()` is used outside an assignment or `foreach` target.
323    ListOutsideDestructuring = "list-outside-destructuring", Parse;
324    /// An array literal has an empty element outside destructuring.
325    EmptyArrayElement = "empty-array-element", Compile;
326    /// A `new` expression is assigned by reference.
327    ReferenceAssignNew = "reference-assign-new", Parse;
328    /// A comma-separated property declaration has hooks.
329    HooksOnPropertyList = "hooks-on-property-list", Parse;
330    /// The right side of `instanceof` is not a class name, variable or parenthesized expression.
331    InvalidInstanceofOperand = "invalid-instanceof-operand", Parse;
332    /// More than one `;` precedes a switch's first case.
333    SwitchLeadingSemicolons = "switch-leading-semicolons", Parse;
334    /// Partial function application is used in a `new` expression.
335    PartialApplicationInNew = "partial-application-in-new", Parse;
336    /// A call combines a `?` or `...` placeholder with argument unpacking.
337    PartialApplicationWithUnpacking = "partial-application-with-unpacking", Compile;
338    /// `new A()` is dereferenced without parentheses before PHP 8.4.
339    UnparenthesizedNewDereference = "unparenthesized-new-dereference", Parse;
340    /// `static` is used as a standalone expression.
341    StaticStandaloneExpression = "static-standalone-expression", Parse;
342    /// A parenthesized expression is assigned by reference.
343    ReferenceToParenthesized = "reference-to-parenthesized", Parse;
344    /// A value that has no reference is assigned by reference.
345    ReferenceToNonReferenceable = "reference-to-non-referenceable", Parse;
346    /// `self`, `parent` or `static` is written fully qualified, e.g. `\self`.
347    QualifiedClassKeyword = "qualified-class-keyword", Compile;
348    /// A reserved name is extended, implemented or used as a trait.
349    ReservedClassReference = "reserved-class-reference", Compile;
350    /// A reserved type name names an enum.
351    ReservedEnumName = "reserved-enum-name", Compile;
352    /// An interface uses a trait.
353    TraitInInterface = "trait-in-interface", Compile;
354    /// A class that is not abstract has an abstract method or hook.
355    AbstractMemberInConcreteClass = "abstract-member-in-concrete-class", Compile;
356    /// A class declares a property twice.
357    DuplicateProperty = "duplicate-property", Compile;
358    /// A class declares a method twice.
359    DuplicateMethod = "duplicate-method", Compile;
360    /// A class declares a constant or enum case twice.
361    DuplicateClassConstant = "duplicate-class-constant", Compile;
362    /// A class constant is named `class`.
363    ClassConstantNamedClass = "class-constant-named-class", Compile;
364    /// A typed class constant's value does not match its type.
365    ClassConstantValueType = "class-constant-value-type", Compile;
366    /// A parameter's or property's literal default value does not match its type.
367    DefaultValueType = "default-value-type", Compile;
368    /// A private class constant is `final`.
369    PrivateFinalConstant = "private-final-constant", Compile;
370    /// An interface constant is not public.
371    InvalidInterfaceConstantModifier = "invalid-interface-constant-modifier", Compile;
372    /// An interface method is not public, or is `final` or `abstract`.
373    InvalidInterfaceMethodModifier = "invalid-interface-method-modifier", Compile;
374    /// An interface property is `abstract`, `final`, protected or private.
375    InvalidInterfacePropertyModifier = "invalid-interface-property-modifier", Compile;
376    /// An interface method has a body.
377    InterfaceMethodBody = "interface-method-body", Compile;
378    /// A method that is not abstract has no body.
379    MissingMethodBody = "missing-method-body", Compile;
380    /// An abstract method has a body.
381    AbstractMethodBody = "abstract-method-body", Compile;
382    /// An abstract method is private.
383    PrivateAbstractMethod = "private-abstract-method", Compile;
384    /// An anonymous class method is abstract.
385    AbstractAnonymousClassMethod = "abstract-anonymous-class-method", Compile;
386    /// An enum method is abstract.
387    AbstractEnumMethod = "abstract-enum-method", Compile;
388    /// A magic method takes the wrong number of parameters.
389    MagicMethodArgumentCount = "magic-method-argument-count", Compile;
390    /// A magic method takes a parameter by reference.
391    MagicMethodByReference = "magic-method-by-reference", Compile;
392    /// A magic method is static when it must not be, or the reverse.
393    MagicMethodStatic = "magic-method-static", Compile;
394    /// A magic method declares a parameter type it may not have.
395    MagicMethodParameterType = "magic-method-parameter-type", Compile;
396    /// A static property is readonly.
397    StaticReadonlyProperty = "static-readonly-property", Compile;
398    /// A readonly property has no type.
399    UntypedReadonlyProperty = "untyped-readonly-property", Compile;
400    /// A readonly property has hooks.
401    ReadonlyPropertyHooks = "readonly-property-hooks", Compile;
402    /// A property with set visibility has no type.
403    UntypedAsymmetricProperty = "untyped-asymmetric-property", Compile;
404    /// A property's set visibility is wider than its visibility.
405    SetVisibilityWiderThanVisibility = "set-visibility-wider-than-visibility", Compile;
406    /// A private property is `final`.
407    PrivateFinalProperty = "private-final-property", Compile;
408    /// A property hook of a private property is `final`.
409    PrivateFinalPropertyHook = "private-final-property-hook", Compile;
410    /// An abstract property has no abstract hook.
411    AbstractPropertyWithoutAbstractHook = "abstract-property-without-abstract-hook", Compile;
412    /// An interface declares a property without hooks.
413    InterfacePropertyWithoutHooks = "interface-property-without-hooks", Compile;
414    /// A static property has hooks.
415    StaticPropertyHooks = "static-property-hooks", Compile;
416    /// A virtual hooked property has a default value.
417    VirtualPropertyDefault = "virtual-property-default", Compile;
418    /// A virtual property has set visibility.
419    VirtualPropertyAsymmetricVisibility = "virtual-property-asymmetric-visibility", Compile;
420    /// A property hook list is empty.
421    EmptyHookList = "empty-hook-list", Compile;
422    /// A property declares the same hook twice.
423    DuplicateHook = "duplicate-hook", Compile;
424    /// A `get` hook has a parameter list.
425    GetHookParameters = "get-hook-parameters", Compile;
426    /// A `set` hook does not have exactly one parameter.
427    SetHookParameterCount = "set-hook-parameter-count", Compile;
428    /// A `set` hook parameter is by reference.
429    SetHookParameterByReference = "set-hook-parameter-by-reference", Compile;
430    /// A `set` hook parameter is variadic.
431    SetHookParameterVariadic = "set-hook-parameter-variadic", Compile;
432    /// A `set` hook parameter has a default value.
433    SetHookParameterDefault = "set-hook-parameter-default", Compile;
434    /// A `set` hook parameter's type does not accept the property's type.
435    SetHookParameterType = "set-hook-parameter-type", Compile;
436    /// A backed property with a `set` hook has a by-reference `get` hook.
437    ByReferenceGetHookWithSet = "by-reference-get-hook-with-set", Compile;
438    /// A hook of a property that is not abstract has no body.
439    MissingHookBody = "missing-hook-body", Compile;
440    /// An interface property hook has a body.
441    AbstractHookBody = "abstract-hook-body", Compile;
442    /// An abstract property hook is `final`.
443    AbstractFinalHook = "abstract-final-hook", Compile;
444    /// An abstract property hook is private.
445    PrivateAbstractHook = "private-abstract-hook", Compile;
446    /// A parameter outside a constructor is promoted.
447    PromotedPropertyOutsideConstructor = "promoted-property-outside-constructor", Compile;
448    /// A parameter of an abstract constructor is promoted.
449    PromotedPropertyInAbstractConstructor = "promoted-property-in-abstract-constructor", Compile;
450    /// A promoted parameter is variadic.
451    VariadicPromotedProperty = "variadic-promoted-property", Compile;
452    /// An enum's backing type is not `int` or `string`.
453    InvalidEnumBackingType = "invalid-enum-backing-type", Compile;
454    /// An enum case is named `class`.
455    EnumCaseNamedClass = "enum-case-named-class", Compile;
456    /// A case of a backed enum has no value.
457    MissingEnumCaseValue = "missing-enum-case-value", Compile;
458    /// A case of a pure enum has a value.
459    PureEnumCaseValue = "pure-enum-case-value", Compile;
460    /// A backed enum case value does not match the backing type (PHP 8.1 only).
461    EnumCaseValueType = "enum-case-value-type", Compile;
462    /// Two cases of a backed enum have the same value (PHP 8.1 only).
463    DuplicateEnumCaseValue = "duplicate-enum-case-value", Compile;
464    /// An enum declares a property.
465    EnumProperty = "enum-property", Compile;
466    /// A trait alias is `static` or `abstract`.
467    InvalidTraitAliasModifier = "invalid-trait-alias-modifier", Compile;
468    /// A `/*` comment is never closed.
469    UnterminatedComment = "unterminated-comment", Parse;
470    /// A named class-like is declared inside a class member.
471    NestedClassDeclaration = "nested-class-declaration", Compile;
472    /// Braced and unbraced namespace declarations are mixed.
473    MixedNamespaceStyles = "mixed-namespace-styles", Compile;
474    /// Code other than `declare` precedes the first namespace declaration.
475    NamespaceNotFirst = "namespace-not-first", Compile;
476    /// Code follows a braced namespace declaration.
477    CodeOutsideNamespace = "code-outside-namespace", Compile;
478    /// A namespace declaration is nested.
479    NestedNamespace = "nested-namespace", Compile;
480    /// A `use` import reuses a name already imported.
481    UseNameConflict = "use-name-conflict", Compile;
482    /// A `use` import is aliased to a reserved class name.
483    SpecialClassNameAlias = "special-class-name-alias", Compile;
484    /// `strict_types` or `encoding` is not declared first.
485    DeclareNotFirstStatement = "declare-not-first-statement", Compile;
486    /// A `declare` directive's value is not a literal.
487    NonLiteralDeclareValue = "non-literal-declare-value", Compile;
488    /// `strict_types` uses block mode or a value other than 0 or 1.
489    InvalidStrictTypes = "invalid-strict-types", Compile;
490    /// Attributes apply to a `const` statement declaring several constants.
491    AttributesOnMultipleConstants = "attributes-on-multiple-constants", Compile;
492    /// An attribute argument list unpacks with `...`.
493    AttributeArgumentUnpacking = "attribute-argument-unpacking", Compile;
494    /// An attribute argument list is a first-class callable `(...)`.
495    AttributeArgumentClosure = "attribute-argument-closure", Compile;
496    /// A `void` function returns a value.
497    VoidReturnValue = "void-return-value", Compile;
498    /// A `never` function returns.
499    NeverReturn = "never-return", Compile;
500    /// A variadic parameter has a default value.
501    VariadicParameterDefault = "variadic-parameter-default", Compile;
502    /// A parameter follows a variadic one.
503    VariadicParameterNotLast = "variadic-parameter-not-last", Compile;
504    /// `$this` is declared as a parameter, closure `use`, global or static variable.
505    ThisAsDeclaredVariable = "this-as-declared-variable", Compile;
506    /// A superglobal is declared as a parameter or closure `use` variable.
507    SuperglobalAsDeclaredVariable = "superglobal-as-declared-variable", Compile;
508    /// A static variable is declared twice in one function.
509    DuplicateStaticVariable = "duplicate-static-variable", Compile;
510    /// `break` or `continue` names more loop levels than enclose it.
511    BreakOutOfLoop = "break-out-of-loop", Compile;
512    /// `break` or `continue` has an operand that is not a positive integer literal.
513    InvalidBreakOperand = "invalid-break-operand", Compile;
514    /// `break`, `continue` or `goto` jumps into or out of a `finally` block.
515    JumpAcrossFinally = "jump-across-finally", Compile;
516    /// A `goto` label is defined twice.
517    DuplicateLabel = "duplicate-label", Compile;
518    /// `goto` names an undefined label.
519    UndefinedGotoLabel = "undefined-goto-label", Compile;
520    /// `goto` jumps into a loop or `switch`.
521    GotoIntoLoop = "goto-into-loop", Compile;
522    /// A `switch` has more than one `default`.
523    DuplicateSwitchDefault = "duplicate-switch-default", Compile;
524    /// A `match` has more than one `default` arm.
525    DuplicateMatchDefault = "duplicate-match-default", Compile;
526    /// A `foreach` key is taken by reference.
527    ReferenceKey = "reference-key", Compile;
528    /// A `foreach` key is a destructuring list.
529    ListAsForeachKey = "list-as-foreach-key", Compile;
530    /// `catch` names `self`, `parent` or `static`.
531    ClassKeywordInCatch = "class-keyword-in-catch", Compile;
532    /// `parent::$x::get()` or `::set()` is called outside the matching hook.
533    InvalidParentHookCall = "invalid-parent-hook-call", Compile;
534    /// `static::class` is used in a constant expression.
535    StaticClassInConstantExpression = "static-class-in-constant-expression", Compile;
536    /// `$a[]` is read or unset.
537    AppendOutsideWrite = "append-outside-write", Compile;
538    /// `$GLOBALS` is written to as a whole, appended to or referenced.
539    InvalidGlobalsWrite = "invalid-globals-write", Compile;
540    /// `mixed` or `null` is marked nullable.
541    InvalidNullableType = "invalid-nullable-type", Compile;
542    /// `void`, `never` or `mixed` is part of a nullable or union type.
543    StandaloneTypeInComposite = "standalone-type-in-composite", Compile;
544    /// A builtin type is part of an intersection type.
545    BuiltinTypeInIntersection = "builtin-type-in-intersection", Compile;
546    /// A type is redundant with another in the same declaration.
547    RedundantType = "redundant-type", Compile;
548    /// A type appears twice in a union or intersection type.
549    DuplicateType = "duplicate-type", Compile;
550    /// A parameter is typed `void` or `never`.
551    InvalidParameterType = "invalid-parameter-type", Compile;
552    /// A property or constant has a type it may not have, e.g. `callable`.
553    InvalidMemberType = "invalid-member-type", Compile;
554    /// A builtin type is written with a namespace, e.g. `\int`.
555    QualifiedBuiltinType = "qualified-builtin-type", Compile;
556    /// A qualified type name ends in a reserved name.
557    ReservedTypeName = "reserved-type-name", Compile;
558    /// `isset()` is applied to the result of an expression.
559    IssetOnExpression = "isset-on-expression", Compile;
560    /// A `new` expression is turned into a first-class callable `(...)`.
561    ClosureForNew = "closure-for-new", Compile;
562    /// `yield` is used outside a function.
563    YieldOutsideFunction = "yield-outside-function", Compile;
564    /// `yield from` is used in a by-reference generator.
565    YieldFromInByReferenceGenerator = "yield-from-in-by-reference-generator", Compile;
566    /// A positional argument follows a named one.
567    PositionalAfterNamedArgument = "positional-after-named-argument", Compile;
568    /// A positional argument follows argument unpacking, as in `f(...$a, 1)`.
569    PositionalAfterUnpacking = "positional-after-unpacking", Compile;
570    /// A method name PHP folds to a value other than a string, as in `$o->{1}()`.
571    MethodNameNotString = "method-name-not-string", Compile;
572    /// An `(unset)` cast is used.
573    UnsetCast = "unset-cast", Compile;
574    /// The result of a builtin such as `clone` is written to.
575    BuiltinResultWrite = "builtin-result-write", Compile;
576    /// The result of a function or method call is written to, e.g. `f() = 1`.
577    CallResultWrite = "call-result-write", Compile;
578    /// A temporary value is written to, e.g. `(1)[0] = 2`.
579    TemporaryExpressionWrite = "temporary-expression-write", Compile;
580    /// A nullsafe chain is written to.
581    NullsafeWrite = "nullsafe-write", Compile;
582    /// A nullsafe chain is taken by reference.
583    ReferenceToNullsafe = "reference-to-nullsafe", Compile;
584    /// A nullsafe call is turned into a first-class callable `(...)`.
585    NullsafeClosureCreation = "nullsafe-closure-creation", Compile;
586    /// A destructuring element cannot be assigned to.
587    UnwritableDestructuringElement = "unwritable-destructuring-element", Compile;
588    /// `$this` is unset.
589    UnsetThis = "unset-this", Compile;
590    /// A closure `use` variable has the name of a parameter.
591    ClosureUseShadowsParameter = "closure-use-shadows-parameter", Compile;
592    /// A closure `use` list names a variable twice.
593    DuplicateClosureUseVariable = "duplicate-closure-use-variable", Compile;
594    /// A numeric literal is used as a class name, e.g. `(1)::A`.
595    LiteralClassName = "literal-class-name", Compile;
596    /// `::class` is applied to `true`, `false`, `null` or an array literal.
597    ClassFetchOnLiteral = "class-fetch-on-literal", Compile;
598    /// `::class` is applied to a value PHP folds at compile time, e.g. `(-1)::class`.
599    ClassFetchOnFoldedValue = "class-fetch-on-folded-value", Compile;
600    /// `::class` is applied to a dynamic class name before PHP 8.0.
601    DynamicClassFetch = "dynamic-class-fetch", Compile;
602    /// PHP 7.4 folds arithmetic on an array, or `~` on a bool, null or array, e.g. `[] - 1`.
603    UnsupportedOperandTypes = "unsupported-operand-types", Compile;
604    /// A constant expression contains something PHP cannot evaluate at compile time.
605    InvalidConstantExpression = "invalid-constant-expression", Compile;
606    /// A destructuring list is empty.
607    EmptyList = "empty-list", Compile;
608    /// A keyed destructuring list has an empty entry.
609    EmptyKeyedArrayEntry = "empty-keyed-array-entry", Compile;
610    /// A destructuring list mixes keyed and unkeyed entries.
611    MixedKeyedEntries = "mixed-keyed-entries", Compile;
612    /// A destructuring pattern mixes `[]` and `list()`.
613    MixedListSyntax = "mixed-list-syntax", Compile;
614    /// A nested destructuring target is written with `array()`, as in `[array($a)] = $b`.
615    LongArrayInDestructuring = "long-array-in-destructuring", Compile;
616    /// A destructuring list uses `...`.
617    SpreadInDestructuring = "spread-in-destructuring", Compile;
618    /// An array literal PHP folds unpacks a value that is not an array, as in `[...1]`.
619    UnpackNonArray = "unpack-non-array", Compile;
620    /// Before PHP 8.1, an array literal PHP folds unpacks an array with a string key.
621    UnpackStringKeys = "unpack-string-keys", Compile;
622    /// Nested ternaries are not parenthesized, e.g. `a ? b : c ? d : e`.
623    UnparenthesizedNestedTernary = "unparenthesized-nested-ternary", Compile;
624    /// An arrow function on the right of `|>` is not parenthesized.
625    UnparenthesizedArrowInPipe = "unparenthesized-arrow-in-pipe", Compile;
626    /// A `{$...}` interpolation is not a variable, property, index or call.
627    InvalidCurlyInterpolation = "invalid-curly-interpolation", Compile;
628    /// A non-repeatable attribute is applied twice.
629    RepeatedAttribute = "repeated-attribute", Compile;
630    /// `#[\NoDiscard]` is applied to a property hook or a method it cannot apply to.
631    NoDiscardOnInvalidTarget = "no-discard-on-invalid-target", Compile;
632    /// `#[\NoDiscard]` is applied to a function that returns no value.
633    NoDiscardWithoutReturnValue = "no-discard-without-return-value", Compile;
634    /// A control character appears in code.
635    UnexpectedCharacter = "unexpected-character", Parse;
636    /// A cast token such as `(int)` where no cast can start, as in `foo(int)`.
637    UnexpectedCast = "unexpected-cast", Parse;
638    /// Not yet classified.
639    Other = "other", Compile;
640}
641
642impl std::fmt::Display for ForbiddenRule {
643    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
644        f.write_str(self.as_str())
645    }
646}
647
648impl std::str::FromStr for ForbiddenRule {
649    type Err = UnknownForbiddenRule;
650
651    fn from_str(s: &str) -> Result<Self, Self::Err> {
652        ForbiddenRule::ALL
653            .iter()
654            .copied()
655            .find(|rule| rule.as_str() == s)
656            .ok_or(UnknownForbiddenRule)
657    }
658}
659
660#[cfg(test)]
661mod tests {
662    use super::{ForbiddenRule, ParseError, Span};
663    use crate::PhpVersion;
664
665    /// `(rule, version, source, reported span text)` for sites that report only at some versions.
666    const VERSION_GATED: &[(ForbiddenRule, PhpVersion, &str, &str)] = &[
667        (
668            ForbiddenRule::EnumCaseValueType,
669            PhpVersion::Php81,
670            "<?php enum E: int { case A = \"a\"; }",
671            "\"a\"",
672        ),
673        (
674            ForbiddenRule::DuplicateEnumCaseValue,
675            PhpVersion::Php81,
676            "<?php enum E: int { case A = 1; case B = 1; }",
677            "1",
678        ),
679        (
680            ForbiddenRule::PartialApplicationWithUnpacking,
681            PhpVersion::Php86,
682            "<?php f(...$a, ?);",
683            "?",
684        ),
685        (
686            ForbiddenRule::NeverReturn,
687            PhpVersion::Php81,
688            "<?php $f = fn(): never => 1;",
689            "1",
690        ),
691        (
692            ForbiddenRule::UnpackStringKeys,
693            PhpVersion::Php80,
694            "<?php $a = [...['a' => 1]];",
695            "['a' => 1]",
696        ),
697        (
698            ForbiddenRule::DynamicClassFetch,
699            PhpVersion::Php74,
700            "<?php $a::class;",
701            "$a",
702        ),
703        (
704            ForbiddenRule::UnsupportedOperandTypes,
705            PhpVersion::Php74,
706            "<?php $a = [] - 1;",
707            "[] - 1",
708        ),
709        (
710            ForbiddenRule::QualifiedBuiltinType,
711            PhpVersion::Php85,
712            "<?php class A { function m(): namespace\\static {} }",
713            "namespace\\static",
714        ),
715        (
716            ForbiddenRule::QualifiedBuiltinType,
717            PhpVersion::Php85,
718            "<?php function f(): \\static {}",
719            "\\static",
720        ),
721        (
722            ForbiddenRule::QualifiedClassKeyword,
723            PhpVersion::Php84,
724            "<?php function f(): \\static {}",
725            "\\static",
726        ),
727    ];
728
729    /// `(source, reported span text)` for each site that reports `rule`.
730    fn samples(rule: ForbiddenRule) -> &'static [(&'static str, &'static str)] {
731        match rule {
732            ForbiddenRule::ClassKeywordOutsideClass => &[
733                ("<?php function f() { self::g(); }", "self"),
734                ("<?php function f() { return parent::$x::get(); }", "parent"),
735            ],
736            ForbiddenRule::ParentWithoutParentClass => {
737                &[("<?php class A { function f() { parent::f(); } }", "parent")]
738            }
739            ForbiddenRule::ToStringReturnType => &[(
740                "<?php class A { function __toString(): int { return 1; } }",
741                "int",
742            )],
743            ForbiddenRule::MagicMethodReturnType => &[
744                (
745                    "<?php class A { function __isset($n): int { return 1; } }",
746                    "int",
747                ),
748                ("<?php class A { function __construct(): void {} }", "void"),
749            ],
750            ForbiddenRule::AttributeTargetMismatch => {
751                &[("<?php #[Override] class A {}", "Override")]
752            }
753            ForbiddenRule::AttributeOnInvalidClass => {
754                &[("<?php #[Attribute] trait T {}", "Attribute")]
755            }
756            ForbiddenRule::DeprecatedOnInvalidTarget => {
757                &[("<?php #[Deprecated] class A {}", "Deprecated")]
758            }
759            ForbiddenRule::AllowDynamicPropertiesOnInvalidClass => &[(
760                "<?php #[AllowDynamicProperties] interface I {}",
761                "AllowDynamicProperties",
762            )],
763            ForbiddenRule::MissingReturnValue => {
764                &[("<?php function f(): int { return; }", "return;")]
765            }
766            ForbiddenRule::GeneratorReturnType => &[
767                ("<?php function f(): int { yield 1; }", "int"),
768                (
769                    "<?php class A { public int $x { set { yield 1; } } }",
770                    "set { yield 1; }",
771                ),
772            ],
773            ForbiddenRule::ReadonlyPropertyDefault => {
774                &[("<?php class A { public readonly int $x = 1; }", "1")]
775            }
776            ForbiddenRule::DuplicateParameter => &[("<?php function f($a, $a) {}", "$a")],
777            ForbiddenRule::ReassignThis => &[
778                ("<?php function f() { $this = 1; }", "$this"),
779                ("<?php try {} catch (E $this) {}", "$this"),
780            ],
781            ForbiddenRule::UnclosedInterpolation => &[("<?php echo \"{$a\";", "{$a\";"), ("<?php echo \"$a[0\";", "[0"), ("<?php echo \"${a\";", "${a\";"),],
782            ForbiddenRule::EmptyInterpolationIndex => &[("<?php echo \"$a[]\";", "[]"), ("<?php echo \"${a[]}\";", "[]"),],
783            ForbiddenRule::EmptyInterpolationVariableName => &[("<?php echo \"${}\";", "${}"),],
784            ForbiddenRule::UnclosedInterpolationIndex => &[("<?php echo \"${a[0]x}\";", "${a[0]x}"),],
785            ForbiddenRule::ClassConstantInterpolation => &[("<?php echo \"{$a::B}\";", "$a::B"),],
786            ForbiddenRule::InvalidInterpolationOffset => &[("<?php echo \"$a[ 1]\";", " 1"),],
787            ForbiddenRule::InvalidNumericLiteral => &[("<?php echo 08;", "08"), ("<?php echo 1_;", "1_"),],
788            ForbiddenRule::InvalidCodepointEscape => &[("<?php echo \"\\u{}\";", "\\u{}"), ("<?php echo \"\\u{110000}\";", "\\u{110000}"),],
789            ForbiddenRule::MixedHeredocIndentation => &[("<?php echo <<<A\n \tx\n \tA;\n", " \t"),],
790            ForbiddenRule::HeredocBodyIndentation => &[("<?php echo <<<A\nx\n  A;\n", "x"),],
791            ForbiddenRule::WhitespaceInNamespacedName => &[("<?php echo A \\ B;", "A \\ B"),],
792            ForbiddenRule::MultipleVisibilityModifiers => &[("<?php class A { public private $x; }", "public private"), ("<?php function f() {} class B { function __construct(public private $x) {} }", "public"),],
793            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)"),],
794            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"),],
795            ForbiddenRule::AbstractFinalConflict => &[("<?php abstract final class A {}", "abstract final"), ("<?php abstract class A { abstract final function f(); }", "abstract final"),],
796            ForbiddenRule::InvalidConstantModifier => &[("<?php class A { static const X = 1; }", "const"), ("<?php enum E { static const X = 1; }", "const"),],
797            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"),],
798            ForbiddenRule::KeywordAsDeclarationName => &[("<?php function list() {}", "list"), ("<?php interface list {}", "list"), ("<?php #[A] enum list {}", "list"),],
799            ForbiddenRule::CustomAssertFunction => &[("<?php namespace A; function Assert() {}", "Assert"),],
800            ForbiddenRule::ReservedClassName => &[("<?php class int {}", "int"),],
801            ForbiddenRule::OutermostScopeOnly => &[("<?php function f() { __halt_compiler(); }", "__halt_compiler"),],
802            ForbiddenRule::UnparenthesizedIntersectionInUnion => &[("<?php function f((A|B)&C $x) {}", "(A|B)"),],
803            ForbiddenRule::ParenthesizedIntersectionOutsideUnion => &[("<?php function f((A&B) $x) {}", "(A&B)"),],
804            ForbiddenRule::StaticTypePosition => &[("<?php class A { public ?static $p; }", "static"), ("<?php function f(?static $x) {}", "static"),],
805            ForbiddenRule::EmptyAttributeList => &[("<?php #[] function f() {}", "#[]"),],
806            ForbiddenRule::CallTimePassByReference => &[("<?php f(&$x);", "&"),],
807            ForbiddenRule::CurlyOffsetAccess => &[("<?php echo $a{0};", "{"),],
808            ForbiddenRule::VoidCastInExpression => &[("<?php $x = (void) f();", "(void) f()"),],
809            ForbiddenRule::RealCast => &[("<?php $x = (real) 1;", "real"),],
810            ForbiddenRule::NonAssociativeChain => &[("<?php $x = 1 < 2 < 3;", "<"),],
811            ForbiddenRule::IncDecAsAssignTarget => &[("<?php $a++ = 1;", "="),],
812            ForbiddenRule::InvalidAssignTarget => &[("<?php 1 = 2;", "="),],
813            ForbiddenRule::CompoundAssignToDestructuring => &[("<?php [$a] += 1;", "+="),],
814            ForbiddenRule::ReferenceAssignToDestructuring => &[("<?php [$a] = &$b;", "&"),],
815            ForbiddenRule::IncDecNonVariable => &[("<?php A++;", "A"),],
816            ForbiddenRule::InvalidDereference => &[("<?php echo 1[0];", "1"),],
817            ForbiddenRule::InvalidWriteContext => &[("<?php unset(A);", "A"),],
818            ForbiddenRule::ListOutsideDestructuring => &[("<?php $x = list($a);", "list($a)"),],
819            ForbiddenRule::EmptyArrayElement => &[("<?php $x = [1, , 2];", ","),],
820            ForbiddenRule::ReferenceAssignNew => &[("<?php $x = &new A;", "new A"),],
821            ForbiddenRule::HooksOnPropertyList => &[("<?php class A { public $x, $y { get => 1; } }", "{"),],
822            ForbiddenRule::InvalidInstanceofOperand => &[("<?php $x instanceof 1;", "1"),],
823            ForbiddenRule::SwitchLeadingSemicolons => &[("<?php switch ($x) {;; case 1: }", ";"),],
824            ForbiddenRule::PartialApplicationInNew => &[("<?php new A(?);", "?"),],
825            ForbiddenRule::UnparenthesizedNewDereference => &[("<?php new A::B;", "::"),],
826            ForbiddenRule::StaticStandaloneExpression => &[("<?php static;", "static"),],
827            ForbiddenRule::ReferenceToParenthesized => &[("<?php $x = &($y);", "($y)"),],
828            ForbiddenRule::ReferenceToNonReferenceable => &[("<?php $x = &1;", "1"), ("<?php [&$a] = 1;", "1"),],
829            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"),],
830            ForbiddenRule::ReservedClassReference => &[("<?php class A extends self {}", "self"),],
831            ForbiddenRule::ReservedEnumName => &[("<?php enum int {}", "int"),],
832            ForbiddenRule::TraitInInterface => &[("<?php interface I { use T; }", "use T;"),],
833            ForbiddenRule::AbstractMemberInConcreteClass => &[("<?php class A { abstract function f(); }", "abstract function f();"), ("<?php class A { abstract public $x { get; } }", "abstract public $x { get; }"),],
834            ForbiddenRule::DuplicateProperty => &[("<?php class A { public $x; public $x; }", "public $x"), ("<?php class A { public $x; function __construct(public $x) {} }", "public $x"),],
835            ForbiddenRule::DuplicateMethod => &[("<?php class A { function f() {} function F() {} }", "function F() {}"),],
836            ForbiddenRule::DuplicateClassConstant => &[("<?php class A { const X = 1; const X = 2; }", "X"),],
837            ForbiddenRule::ClassConstantNamedClass => &[("<?php class A { const class = 1; }", "class"),],
838            ForbiddenRule::ClassConstantValueType => &[("<?php class A { const int X = \"a\"; }", "\"a\""),],
839            ForbiddenRule::DefaultValueType => &[("<?php function f(int $x = \"a\") {}", "\"a\""), ("<?php class A { public int $x = null; }", "null"),],
840            ForbiddenRule::PrivateFinalConstant => &[("<?php class A { final private const X = 1; }", "final private const X = 1;"), ("<?php enum E { final private const X = 1; }", "final private const X = 1;"),],
841            ForbiddenRule::InvalidInterfaceConstantModifier => &[("<?php interface I { private const X = 1; }", "private const X = 1;"),],
842            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();"),],
843            ForbiddenRule::InvalidInterfacePropertyModifier => &[("<?php interface I { final public $x { get; } }", "final public $x"),],
844            ForbiddenRule::InterfaceMethodBody => &[("<?php interface I { function f() {} }", "function f() {}"),],
845            ForbiddenRule::MissingMethodBody => &[("<?php class A { function f(); }", "function f();"),],
846            ForbiddenRule::AbstractMethodBody => &[("<?php abstract class A { abstract function f() {} }", "abstract function f() {}"),],
847            ForbiddenRule::PrivateAbstractMethod => &[("<?php abstract class A { abstract private function f(); }", "abstract private function f();"),],
848            ForbiddenRule::AbstractAnonymousClassMethod => &[("<?php new class { abstract function f(); };", "abstract function f();"),],
849            ForbiddenRule::AbstractEnumMethod => &[("<?php enum E { abstract function f(); }", "abstract"),],
850            ForbiddenRule::MagicMethodArgumentCount => &[("<?php class A { function __get() {} }", "function __get() {}"),],
851            ForbiddenRule::MagicMethodByReference => &[("<?php class A { function __get(&$n) {} }", "&$n"),],
852            ForbiddenRule::MagicMethodStatic => &[("<?php class A { static function __get($n) {} }", "static function __get($n) {}"),],
853            ForbiddenRule::MagicMethodParameterType => &[("<?php class A { function __get(int $n) {} }", "int"),],
854            ForbiddenRule::StaticReadonlyProperty => &[("<?php class A { public static readonly int $x; }", "public static readonly"), ("<?php class A { public readonly static int $x; }", "public readonly static"),],
855            ForbiddenRule::UntypedReadonlyProperty => &[("<?php class A { public readonly $x; }", "public readonly $x"), ("<?php class A { function __construct(public readonly $x) {} }", "public readonly"),],
856            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;"),],
857            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"),],
858            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)"),],
859            ForbiddenRule::PrivateFinalProperty => &[("<?php class A { final private $x; }", "final private $x"),],
860            ForbiddenRule::PrivateFinalPropertyHook => &[("<?php class A { private $x { final get => 1; } }", "final get => 1;"),],
861            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; }"),],
862            ForbiddenRule::InterfacePropertyWithoutHooks => &[("<?php interface I { public $x; }", "public $x"),],
863            ForbiddenRule::StaticPropertyHooks => &[("<?php class A { public static $x { get => 1; } }", "public static $x { get => 1; }"),],
864            ForbiddenRule::VirtualPropertyDefault => &[("<?php class A { public int $x = 1 { get => 2; } }", "public int $x = 1 { get => 2; }"),],
865            ForbiddenRule::VirtualPropertyAsymmetricVisibility => &[("<?php class A { public private(set) int $x { get => 1; } }", "public private(set) int $x { get => 1; }"),],
866            ForbiddenRule::EmptyHookList => &[("<?php class A { public $x {} }", "{"),],
867            ForbiddenRule::DuplicateHook => &[("<?php class A { public $x { get => 1; get => 2; } }", "get => 2;"), ("<?php class A { public $x { set {} set {} } }", "set {}"),],
868            ForbiddenRule::GetHookParameters => &[("<?php class A { public $x { get() => 1; } }", "("),],
869            ForbiddenRule::SetHookParameterCount => &[("<?php class A { public $x { set($a, $b) {} } }", "("),],
870            ForbiddenRule::SetHookParameterByReference => &[("<?php class A { public $x { set(&$v) {} } }", "&$v"),],
871            ForbiddenRule::SetHookParameterVariadic => &[("<?php class A { public $x { set(...$v) {} } }", "...$v"),],
872            ForbiddenRule::SetHookParameterDefault => &[("<?php class A { public $x { set($v = 1) {} } }", "$v = 1"),],
873            ForbiddenRule::SetHookParameterType => &[("<?php class A { public int $x { set(string $v) {} } }", "string $v"),],
874            ForbiddenRule::ByReferenceGetHookWithSet => &[("<?php class A { public $x { &get => $this->x; set { $this->x = $value; } } }", "&get => $this->x;"),],
875            ForbiddenRule::MissingHookBody => &[("<?php class A { public $x { get; } }", "get;"),],
876            ForbiddenRule::AbstractHookBody => &[("<?php interface I { public $x { get => 1; } }", "get => 1;"),],
877            ForbiddenRule::AbstractFinalHook => &[("<?php abstract class A { abstract public $x { final get; } }", "final get;"),],
878            ForbiddenRule::PrivateAbstractHook => &[("<?php abstract class A { abstract private $x { get; } }", "get;"),],
879            ForbiddenRule::PromotedPropertyOutsideConstructor => &[("<?php function f(public $x) {}", "public"), ("<?php class A { function f(public $x) {} }", "public"),],
880            ForbiddenRule::PromotedPropertyInAbstractConstructor => &[("<?php abstract class A { abstract function __construct(public $x); }", "public $x"),],
881            ForbiddenRule::VariadicPromotedProperty => &[("<?php class A { function __construct(public ...$x) {} }", "public ...$x"),],
882            ForbiddenRule::InvalidEnumBackingType => &[("<?php enum E: float {}", "float"),],
883            ForbiddenRule::EnumCaseNamedClass => &[("<?php enum E { case class; }", "class"),],
884            ForbiddenRule::MissingEnumCaseValue => &[("<?php enum E: int { case A; }", "A"),],
885            ForbiddenRule::PureEnumCaseValue => &[("<?php enum E { case A = 1; }", "="),],
886            ForbiddenRule::EnumProperty => &[("<?php enum E { public int $a; }", "$a"), ("<?php enum E { var $a, $b; }", "$a"),],
887            ForbiddenRule::InvalidTraitAliasModifier => &[("<?php class A { use T { f as static g; } }", "static"),],
888            ForbiddenRule::UnterminatedComment => &[("<?php /* x", "/* x"),],
889            ForbiddenRule::NestedClassDeclaration => &[("<?php class A { function f() { class B {} } }", "B"),],
890            ForbiddenRule::MixedNamespaceStyles => &[("<?php namespace A; namespace B {}", "namespace B {}"),],
891            ForbiddenRule::NamespaceNotFirst => &[("<?php echo 1; namespace A;", "echo 1;"),],
892            ForbiddenRule::CodeOutsideNamespace => &[("<?php namespace A {} echo 1;", "echo 1;"),],
893            ForbiddenRule::NestedNamespace => &[("<?php namespace A { namespace B {} }", "namespace B {}"),],
894            ForbiddenRule::UseNameConflict => &[("<?php use A\\B; use C\\B;", "C\\B"),],
895            ForbiddenRule::SpecialClassNameAlias => &[("<?php use A as self;", "A as self"),],
896            ForbiddenRule::DeclareNotFirstStatement => &[("<?php echo 1; declare(strict_types=1);", "strict_types=1"), ("<?php echo 1; declare(encoding='UTF-8');", "encoding='UTF-8'"),],
897            ForbiddenRule::NonLiteralDeclareValue => &[("<?php declare(ticks=A);", "ticks=A"), ("<?php declare(encoding=A);", "encoding=A"),],
898            ForbiddenRule::InvalidStrictTypes => &[("<?php declare(strict_types=2);", "strict_types=2"), ("<?php declare(strict_types=1) {}", "strict_types=1"),],
899            ForbiddenRule::AttributesOnMultipleConstants => &[("<?php #[A] const X = 1, Y = 2;", "const X = 1, Y = 2;"),],
900            ForbiddenRule::AttributeArgumentUnpacking => &[("<?php #[A(...$a)] function f() {}", "...$a"),],
901            ForbiddenRule::AttributeArgumentClosure => &[("<?php #[A(...)] function f() {}", "(...)"),],
902            ForbiddenRule::VoidReturnValue => &[("<?php function f(): void { return 1; }", "return 1;"), ("<?php $f = fn(): void => 1;", "1"),],
903            ForbiddenRule::NeverReturn => &[("<?php function f(): never { return; }", "return;"),],
904            ForbiddenRule::VariadicParameterDefault => &[("<?php function f(...$a = []) {}", "...$a = []"),],
905            ForbiddenRule::VariadicParameterNotLast => &[("<?php function f(...$a, $b) {}", "$b"),],
906            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"),],
907            ForbiddenRule::SuperglobalAsDeclaredVariable => &[("<?php $f = function () use ($_GET) {};", "$_GET"), ("<?php function f($_GET) {}", "$_GET"),],
908            ForbiddenRule::DuplicateStaticVariable => &[("<?php function f() { static $a; static $a; }", "$a"),],
909            ForbiddenRule::BreakOutOfLoop => &[("<?php break;", "break"), ("<?php while (1) { break 2; }", "break"),],
910            ForbiddenRule::InvalidBreakOperand => &[("<?php while (1) { break $a; }", "break"), ("<?php while (1) { break 0; }", "break"),],
911            ForbiddenRule::JumpAcrossFinally => &[("<?php while (1) { try {} finally { break; } }", "break"), ("<?php try {} finally { goto a; } a:", "goto a;"),],
912            ForbiddenRule::DuplicateLabel => &[("<?php a: a:", "a:"),],
913            ForbiddenRule::UndefinedGotoLabel => &[("<?php goto a;", "goto a;"),],
914            ForbiddenRule::GotoIntoLoop => &[("<?php goto a; while (1) { a: }", "goto a;"),],
915            ForbiddenRule::DuplicateSwitchDefault => &[("<?php switch ($x) { default: default: }", "default"),],
916            ForbiddenRule::DuplicateMatchDefault => &[("<?php match ($x) { default => 1, default => 2 };", "default"),],
917            ForbiddenRule::ReferenceKey => &[("<?php foreach ($a as &$k => $v) {}", "&$k"),],
918            ForbiddenRule::ListAsForeachKey => &[("<?php foreach ($a as [$k] => $v) {}", "[$k]"),],
919            ForbiddenRule::ClassKeywordInCatch => &[("<?php try {} catch (self $e) {}", "self"),],
920            ForbiddenRule::InvalidParentHookCall => &[("<?php class A extends B { function f() { parent::$x::get(); } }", "parent::$x::get()"),],
921            ForbiddenRule::StaticClassInConstantExpression => &[("<?php class A { const X = static::class; }", "static"),],
922            ForbiddenRule::AppendOutsideWrite => &[("<?php $x = $a[];", "$a[]"), ("<?php unset($a[]);", "$a[]"),],
923            ForbiddenRule::InvalidGlobalsWrite => &[("<?php $GLOBALS[] = 1;", "$GLOBALS[]"), ("<?php $GLOBALS = [];", "$GLOBALS"), ("<?php $x = &$GLOBALS;", "$GLOBALS"),],
924            ForbiddenRule::InvalidNullableType => &[("<?php function f(?mixed $x) {}", "mixed"), ("<?php function f(?null $x) {}", "null"),],
925            ForbiddenRule::StandaloneTypeInComposite => &[("<?php function f(): ?void {}", "void"), ("<?php function f(): void|int {}", "void"),],
926            ForbiddenRule::BuiltinTypeInIntersection => &[("<?php function f(int&A $x) {}", "int"),],
927            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"),],
928            ForbiddenRule::DuplicateType => &[("<?php function f(int|int $x) {}", "int"),],
929            ForbiddenRule::InvalidParameterType => &[("<?php function f(void $x) {}", "void"),],
930            ForbiddenRule::InvalidMemberType => &[("<?php class A { public callable $x; }", "callable"),],
931            ForbiddenRule::QualifiedBuiltinType => &[("<?php function f(\\int $x) {}", "\\int"),],
932            ForbiddenRule::ReservedTypeName => &[("<?php function f(A\\int $x) {}", "A\\int"),],
933            ForbiddenRule::IssetOnExpression => &[("<?php isset(f());", "f()"),],
934            ForbiddenRule::ClosureForNew => &[("<?php new A(...);", "(...)"), ("<?php new class(...) {};", "(...)"),],
935            ForbiddenRule::YieldOutsideFunction => &[("<?php yield 1;", "yield"),],
936            ForbiddenRule::YieldFromInByReferenceGenerator => &[("<?php function &f() { yield from g(); }", "yield from g()"),],
937            ForbiddenRule::PositionalAfterNamedArgument => &[("<?php f(a: 1, 2);", "2"),],
938            ForbiddenRule::PositionalAfterUnpacking => &[("<?php f(...$a, 2);", "2"),],
939            ForbiddenRule::MethodNameNotString => &[("<?php $o->{1}();", "1"), ("<?php A::{-1}();", "-1"),],
940            ForbiddenRule::UnsetCast => &[("<?php $x = (unset) 1;", "unset"),],
941            ForbiddenRule::BuiltinResultWrite => &[("<?php (clone $a)->b = 1;", "(clone $a)->b"), ("<?php $x = &f(...);", "f(...)"),],
942            ForbiddenRule::CallResultWrite => &[("<?php f() = 1;", "f()"), ("<?php $a->f()++;", "$a->f()"), ("<?php unset(A::f());", "A::f()"), ("<?php [f(...)] = $a;", "f(...)"), ("<?php foreach ($a as $k => $b?->f()) {}", "$b?->f()"),],
943            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]"),],
944            ForbiddenRule::NullsafeWrite => &[("<?php $a?->b = 1;", "$a?->b"), ("<?php $a?->b++;", "$a?->b"), ("<?php unset($a?->b);", "$a?->b"),],
945            ForbiddenRule::ReferenceToNullsafe => &[("<?php $x = &$a?->b;", "$a?->b"), ("<?php [&$x] = $a?->b;", "$a?->b"), ("<?php function &f() { return $a?->b; }", "$a?->b"),],
946            ForbiddenRule::NullsafeClosureCreation => &[("<?php $a?->b(...);", "$a?->b(...)"),],
947            ForbiddenRule::UnwritableDestructuringElement => &[("<?php [A] = $a;", "A"), ("<?php foreach ($a as [A]) {}", "A"),],
948            ForbiddenRule::UnsetThis => &[("<?php function f() { unset($this); }", "$this"),],
949            ForbiddenRule::ClosureUseShadowsParameter => &[("<?php $f = function ($a) use ($a) {};", "$a"),],
950            ForbiddenRule::DuplicateClosureUseVariable => &[("<?php $f = function () use ($a, $a) {};", "$a"),],
951            ForbiddenRule::LiteralClassName => &[("<?php (1)::A;", "(1)"), ("<?php (1)::class;", "(1)"),],
952            ForbiddenRule::ClassFetchOnLiteral => &[("<?php (null)::class;", "(null)"),],
953            ForbiddenRule::ClassFetchOnFoldedValue => &[("<?php echo (-1)::class;", "(-1)"),],
954            ForbiddenRule::InvalidConstantExpression => &[("<?php const X = $a;", "$a"),],
955            ForbiddenRule::EmptyList => &[("<?php [] = $a;", "[]"),],
956            ForbiddenRule::EmptyKeyedArrayEntry => &[("<?php ['a' => $y, , 'b' => $z] = $a;", ","),],
957            ForbiddenRule::MixedKeyedEntries => &[("<?php ['a' => $x, $y] = $a;", "$y"),],
958            ForbiddenRule::MixedListSyntax => &[("<?php [list($a)] = $b;", "list($a)"),],
959            ForbiddenRule::LongArrayInDestructuring => &[("<?php [array($a)] = $b;", "array($a)"),],
960            ForbiddenRule::SpreadInDestructuring => &[("<?php [...$a] = $b;", "...$a"),],
961            ForbiddenRule::UnpackNonArray => &[("<?php $a = [1, ...2];", "2"),],
962            ForbiddenRule::UnparenthesizedNestedTernary => &[("<?php $x = 1 ? 2 : 3 ? 4 : 5;", "?"),],
963            ForbiddenRule::UnparenthesizedArrowInPipe => &[("<?php $x = 1 |> fn($a) => $a;", "fn($a) => $a"),],
964            ForbiddenRule::InvalidCurlyInterpolation => &[("<?php echo \"{$a + 1}\";", "$a + 1"),],
965            ForbiddenRule::RepeatedAttribute => &[("<?php #[Override, Override] function f() {}", "Override"),],
966            ForbiddenRule::NoDiscardOnInvalidTarget => &[("<?php class A { public $x { #[NoDiscard] get => 1; } }", "NoDiscard"), ("<?php class A { #[NoDiscard] function __construct() {} }", "NoDiscard"),],
967            ForbiddenRule::NoDiscardWithoutReturnValue => &[("<?php #[NoDiscard] function f(): void {}", "NoDiscard"),],
968            ForbiddenRule::UnexpectedCharacter => &[("<?php $a\x01\x02 = 1;", "\x01\x02"), ("<?php $a =\x0c1;", "\x0c"),],
969            ForbiddenRule::UnexpectedCast => &[("<?php foo(int);", "(int)"), ("<?php if ( float ) {}", "( float )"),],
970            ForbiddenRule::DynamicClassFetch
971            | ForbiddenRule::UnpackStringKeys
972            | ForbiddenRule::UnsupportedOperandTypes
973            | ForbiddenRule::PartialApplicationWithUnpacking
974            | ForbiddenRule::EnumCaseValueType
975            | ForbiddenRule::DuplicateEnumCaseValue
976            | ForbiddenRule::Other => &[],
977        }
978    }
979
980    #[test]
981    fn each_rule_reports_its_span() {
982        for &rule in ForbiddenRule::ALL {
983            if rule == ForbiddenRule::Other {
984                continue;
985            }
986            let gated = VERSION_GATED.iter().any(|&(r, ..)| r == rule);
987            assert!(gated || !samples(rule).is_empty(), "{rule:?} has no sample");
988            for &(src, text) in samples(rule) {
989                let errors = crate::parse(src).errors;
990                let spans: Vec<&str> = errors
991                    .iter()
992                    .filter(|e| e.forbidden_rule() == Some(rule))
993                    .map(|e| &src[e.span().start as usize..e.span().end as usize])
994                    .collect();
995                assert_eq!(spans, [text], "{rule:?} in {src:?}: {errors:?}");
996            }
997        }
998    }
999
1000    #[test]
1001    fn version_gated_rules_report_their_span() {
1002        for &(rule, version, src, text) in VERSION_GATED {
1003            let options = crate::ParseOptions::default().version(version);
1004            let errors = crate::parse_with(src, &options).errors;
1005            let spans: Vec<&str> = errors
1006                .iter()
1007                .filter(|e| e.forbidden_rule() == Some(rule))
1008                .map(|e| &src[e.span().start as usize..e.span().end as usize])
1009                .collect();
1010            assert_eq!(spans, [text], "{rule:?} in {src:?}: {errors:?}");
1011        }
1012    }
1013
1014    #[test]
1015    fn relative_static_type_is_valid_in_a_class_before_8_5() {
1016        let options = crate::ParseOptions::default().version(PhpVersion::Php84);
1017        let src = "<?php class A { function m(): namespace\\static {} }";
1018        assert_eq!(crate::parse_with(src, &options).errors.len(), 0);
1019    }
1020
1021    #[test]
1022    fn identifiers_are_unique_and_parse_back() {
1023        let mut ids: Vec<&str> = ForbiddenRule::ALL.iter().map(|r| r.as_str()).collect();
1024        ids.sort_unstable();
1025        ids.dedup();
1026        assert_eq!(ids.len(), ForbiddenRule::ALL.len());
1027        for &rule in ForbiddenRule::ALL {
1028            assert_eq!(rule.as_str().parse(), Ok(rule));
1029        }
1030        assert_eq!(
1031            "no-such-rule".parse::<ForbiddenRule>(),
1032            Err(super::UnknownForbiddenRule)
1033        );
1034    }
1035
1036    #[test]
1037    fn codes_are_unique_and_only_syntax_errors_leave_the_ast_incomplete() {
1038        let span = Span::new(0, 0);
1039        let incomplete = [
1040            ParseError::Expected {
1041                expected: "';'".into(),
1042                found: "'}'".into(),
1043                span,
1044            },
1045            ParseError::UnterminatedString { span },
1046            ParseError::ExpectedAfter {
1047                expected: "';'".into(),
1048                after: "expression".into(),
1049                span,
1050            },
1051            ParseError::UnclosedDelimiter {
1052                delimiter: "')'".into(),
1053                opened_at: span,
1054                span,
1055            },
1056            ParseError::UnmatchedDelimiter {
1057                delimiter: "'}'".into(),
1058                span,
1059            },
1060            ParseError::NestingTooDeep { span },
1061            ParseError::SourceTooLarge { len: 0, span },
1062        ];
1063        let complete = [
1064            ParseError::Forbidden {
1065                rule: ForbiddenRule::ReassignThis,
1066                message: "Cannot re-assign $this".into(),
1067                span,
1068            },
1069            ParseError::VersionTooLow {
1070                feature: "match".into(),
1071                required: PhpVersion::Php80,
1072                used: PhpVersion::Php74,
1073                span,
1074            },
1075        ];
1076        assert!(incomplete.iter().all(ParseError::leaves_ast_incomplete));
1077        assert!(!complete.iter().any(ParseError::leaves_ast_incomplete));
1078        assert_eq!(complete[0].code(), "reassign-this");
1079        let mut codes: Vec<&str> = ForbiddenRule::ALL.iter().map(|r| r.as_str()).collect();
1080        codes.extend(
1081            incomplete
1082                .iter()
1083                .chain(&complete[1..])
1084                .map(ParseError::code),
1085        );
1086        let count = codes.len();
1087        codes.sort_unstable();
1088        codes.dedup();
1089        assert_eq!(codes.len(), count);
1090    }
1091
1092    #[test]
1093    fn rule_adds_no_size() {
1094        assert_eq!(std::mem::size_of::<ParseError>(), 64);
1095    }
1096}