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