Skip to content

[FR] Enums: sniff to enforce that non-public methods use private instead of protected #442

Description

@rodrigoprimo

Setting the scene

PHP 8.1 introduced enumerations (enums). Enums do not support inheritance: an enum cannot extend another enum or be extended, which means a protected member is functionally equivalent to private.

PER-CS 2.0 added a dedicated section on enumerations and codifies this: in an enum, a non-public method must be declared private, never protected.

Neither PHP_CodeSniffer itself nor PHPCSExtra contains a sniff to handle this.

Proposed new sniff: Universal.Enums.EnumProtectedMember

To address this rule from PER:

Non-public methods MUST use private instead of protected, as enums do not support inheritance.

Notes for the implementation

Registering [T_FUNCTION, T_CONST] is a good entry point. From each token, PHPCSUtils\Utils\Scopes::validDirectScope($phpcsFile, $stackPtr, \T_ENUM) confirms the member's direct wrapping scope is an enum. From there the sniff locates the T_PROTECTED token and flags it.

PER states this rule for methods only, but enum constants can also be declared protected, and the same reasoning applies: enums are implicitly final, so a protected constant is functionally identical to a private one.

The fix replaces the protected keyword with private.

Suggested error codes:

  • MethodFound
  • ConstantFound

Note: I opened an issue suggesting that the PER-CS rule be updated to cover constants as well: php-fig/per-coding-style#150. Using a separate error code means the error for constants can be excluded when implementing the PER-CS 2.0 standard.

Describe the solution you'd like

A new sniff as outlined above.

The sniff should be able to flag and auto-fix the following:

enum Suit: string
{
    case Hearts = 'H';

    // OK.
    public function color(): string
    {
        return 'Red';
    }

    // OK.
    private function helper(): string
    {
        return 'ok';
    }

    // Error: non-public method MUST use `private`, not `protected`. Auto-fixed to `private`.
    protected function bad(): string
    {
        return 'nope';
    }

    // Error: `protected` constant in an enum. Auto-fixed to `private`.
    protected const SECRET = 'x';

    // Not flagged: method in an anonymous class nested inside an enum method is out of scope.
    public function makeHelper(): object
    {
        return new class {
            protected function stillFine(): string
            {
                return 'ok';
            }
        };
    }
}

Also see the examples outlined in the PER documents (rules + migration guide).

Additional context (optional)

This ticket is the result of a detailed analysis of the rules as outlined in PER-CS 2.0, as well as a critical look at what's still missing rule-wise.

  • I intend to create a pull request to implement this feature.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions