Skip to content

fix(parser): accept semi-reserved keywords as method names in trait adaptations - #2286

Open
phcorp wants to merge 4 commits into
illegalstudio:mainfrom
phcorp:fix/trait-adaptation-keyword-names
Open

phcorp wants to merge 4 commits into
illegalstudio:mainfrom
phcorp:fix/trait-adaptation-keyword-names

Conversation

@phcorp

@phcorp phcorp commented Nov 8, 2026 •

Copy link
Copy Markdown

Summary

PHP accepts any semi-reserved word as a method name in a trait adaptation, just as it does in a method declaration. tempest/highlight relies on this:

final class Highlighter {
    use Patterns {
        match as protected matchContent;   // bare target: a keyword
        Patterns::list as print;           // `Trait::` target and alias: keywords
    }
}

elephc rejected this with Expected method or trait name in adaptation, and A::print insteadof B failed with Expected method name after 'TraitName::'. Every adaptation position now takes a keyword name, following PHP's grammar:

trait_method_reference:          identifier | class_name :: identifier
trait_alias:                     trait_method_reference as T_STRING
                               | trait_method_reference as reserved_non_modifiers
                               | trait_method_reference as member_modifier identifier
                               | trait_method_reference as member_modifier

Directly after as, a modifier is still read as the visibility change (as public). After a visibility, the alias can be any word, modifiers included: as protected static aliases the method to static().

Repro

<?php
trait A { public function print() { return "A"; } public function list() { return "Al"; } }
trait B { public function print() { return "B"; } public function list() { return "Bl"; } }
class C {
    use A, B {
        A::print insteadof B;
        B::list insteadof A;
        B::print as function;
        A::list as protected list2;
    }
    public function l2() { return $this->list2(); }
}
$c = new C();
echo $c->print(), $c->list(), $c->function(), $c->l2(), "\n";
output
main error: Expected method name after 'TraitName::' in adaptation
this branch ABlBAl
php 8.5 ABlBAl

Each adaptation form, compared across main, the branch and PHP 8.5:

adaptation php 8.5 main this branch
match as protected matchContent ok error ok
function as f / static as st / namespace as ns (bare keyword target) ok error ok
T::enum as protected readonly ok error ok
foo as protected static ok error ok
foo as list, foo as protected list ok ok ok
foo as class ok error ok
foo as static fatal: Cannot use "static" as method modifier error error (Trait alias adaptation requires a visibility and/or alias name)

How it works

src/parser/stmt/oop/traits.rs:

  • Bare target (L180-L219). When the token cannot start a class-like name (name_starts_at is false) but spells a word, the parser takes that word as the method. A name that can start a class name (A::foo, \Ns\A::foo, namespace\A::foo) still goes through parse_name. A bare namespace that is not followed by \ falls through to the keyword path.

  • Trait::method target. Accepts any word through the same helper.

  • Alias (L76-L90):

    let alias = match tokens.get(*pos).map(|(t, _)| t) {
        Some(Token::Public | Token::Protected | Token::Private | Token::Static
            | Token::Abstract | Token::Final | Token::ReadOnly) if visibility.is_none() => None,
        _ => adaptation_bareword(tokens, *pos).inspect(|_| *pos += 1),
    };
  • adaptation_bareword is a thin wrapper over keyword_name::bareword_name_from_token, the helper that method declarations and ->/:: member names already use.

Tests

  • test_parse_trait_adaptation_keyword_method_names (parser): covers the bare, qualified and visibility-only forms, and list as private static, which aliases to static.
  • test_trait_adaptation_keyword_method_names (codegen): runs the tempest/highlight shape and prints [m:x]|l|l.
  • test_trait_insteadof_and_modifier_alias_with_keyword_names (codegen): A::print insteadof B, B::list insteadof A, B::print as function and A::list as protected static. It prints A|Bl|B|Al, as PHP does.
  • test_error_trait_alias_modifier_without_visibility_is_not_an_alias and test_error_trait_adaptation_scoped_target_requires_method_name (errors): foo as static; and T::$foo as bar; are still rejected.

The parser and codegen tests fail on main. Runs on this branch:

  • cargo test --test parser_tests trait: 7 passed
  • cargo test --test error_tests trait: 115 passed
  • ELEPHC_PHP_CHECK=1 cargo test --test codegen_tests traits: 18 passed

Note

CI status: this PR comes from a fork, so the CI, PDO Live Databases and PHP extension host SDK workflows are waiting for maintainer approval (action_required). The focused tests above ran locally on macOS arm64, and the codegen tests were also compared with PHP 8.5.12 (ELEPHC_PHP_CHECK=1). This is a front-end change (parser/checker only) with no target-specific code.

Limits

  • foo as static; is still a parse error. PHP parses it and then fails at compile time with a different message. Both reject it.
  • Calling a method with a keyword name ($x->list(), K::print(), $x->match()) already worked on main and is unchanged. The magician eval() parser already accepts keyword names in adaptations: eval of list as protected print; TT::list as function; prints ll.

Follow-ups

@github-actions github-actions Bot added area:parser Touches parsing or AST construction. size:xs Very small pull request. type:fix Corrects broken or incompatible behavior. labels Nov 8, 2026
@phcorp
phcorp marked this pull request as ready for review November 8, 2026 20:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:parser Touches parsing or AST construction. size:s Small pull request. type:fix Corrects broken or incompatible behavior.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant