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