Rewrite php-mode as a CC Mode independent major mode#812
Draft
zonuexe wants to merge 23 commits into
Draft
Conversation
zonuexe
marked this pull request as draft
July 16, 2026 12:14
zonuexe
added a commit
that referenced
this pull request
Jul 16, 2026
Extracted from the cc-mode independent php-mode work (#812) so that it can be reviewed and land independently of the Emacs 28.1 requirement that blocks that branch until the August release (#811). The file holds the PHP vocabulary (control structures, declarations and modifiers, statements, primitive types, constants, magic constants and magic methods) as plain defconst tables plus a symbol-bounded regexp-opt for each. The provenance of every entry is documented in the commentary: they were derived from the c-lang-defconst tables of the CC Mode based implementation, supplemented where PHP has gained keywords since. This is purely additive: nothing requires php-keywords.el yet, so the tables have no effect on php-mode until the cc-mode independent mode lands.
zonuexe
added a commit
that referenced
this pull request
Jul 16, 2026
Extracted from the cc-mode independent php-mode work (#812) so that it can be reviewed and land independently of the Emacs 28.1 requirement that blocks that branch until the August release (#811). The engine is ported from the syntax-ppss based indenter of js.el in GNU Emacs (attributed in the file header); the JavaScript specific handling (JSX, C preprocessor macros, array comprehensions and regexp literals) is removed, and PHP specifics are added: -> / ?-> / :: method and property chains, the . concatenation operator aligned to the assignment, statement continuations, switch/case, the alternative syntax (endif and friends) and heredoc/nowdoc bodies. Its entry points are php-indent-line and php-indent-region. The file is self-contained: it only requires php.el for the php customization group, and defines no symbol that collides with the current php-mode. This is purely additive: nothing sets php-indent-line as indent-line-function yet, so the engine is inert -- the current CC Mode based php-mode keeps indenting through cc-engine. Registering both new files in Eask so they are byte-compiled by CI.
zonuexe
added a commit
that referenced
this pull request
Jul 16, 2026
Add php-indent.el and php-keywords.el (extracted from #812)
zonuexe
force-pushed
the
rewrite/cc-free-php-mode
branch
3 times, most recently
from
July 16, 2026 17:44
c7e2cbf to
f6cb997
Compare
…ation Move the current CC Mode based implementation aside so the cc-mode independent rewrite can claim the php-mode name. The legacy mode is now php-cc-mode (mode line "PHP/cc") and is in frozen maintenance. - git mv lisp/php-mode.el lisp/php-cc-mode.el - Rename the major mode, keymap (php-cc-mode-map), style command (php-cc-set-style), syntax table and font-lock keyword lists. - Retarget the CC Mode language system to php-cc-mode: c-add-language, the c-lang-defconst language symbol (php -> php-cc), c-init-language-vars and c-common-init. - Retarget derived-mode-p advice guards to php-cc-mode. - Drop auto-mode-alist / interpreter-mode-alist registration; these will be owned by the new php-mode. - Run php-mode-hook at the end of php-cc-mode for backward compatibility. - Point php-mode-debug and Eask at php-cc-mode.
php.el no longer requires cc-mode / cc-engine.
- Replace the c-populate-syntax-table based analysis table with a
hand-written php--base-syntax-table (shared, to be reused by the new
php-mode) and derive php--analysis-syntax-table from it.
- Reimplement the token scanner previously borrowed from cc-engine
(c-end-of-token, c-beginning-of-current-token, c-backward-token-2) as
self-contained php--end-of-token / php--beginning-of-current-token /
php--backward-token, using php-re-token-symbols as the token regexp.
php-leading-tokens output is unchanged.
- Move the shared defcustoms php-mode-coding-style (now defaulting to
`per', symfony2 removed from the choices), php-mode-template-compatibility
and php-mode-{pear,drupal,wordpress,psr2}-hook from php-cc-mode.el into
php.el. php-cc-mode keeps its Symfony2 hook/style and gains a "per"
cc-style alias so it still initializes under the new default.
The hand-written PHP base syntax table placed the `b' comment-style flag on `*' instead of `/' and the newline, so `//' line comments were classified as `a'-style (block) comments. `syntax-ppss' element 7 was therefore nil for `//', causing php-indent--find-newline-backward to skip past line comments and mis-indent any continuation line whose predecessor ended with a trailing `// comment'. Move the flag onto `/' to mirror the standard C/JavaScript tables. Also skip a leading block comment in php-indent--continued-expression-p so that a chained call whose operator is preceded by `/* ... */' (e.g. `/* c */ ->foo()') is still recognized as a continuation line.
Implement php-mode as a php-base-mode derivative with no dependency on CC Mode. Font-lock is built from a single php-font-lock-keywords list (the cc-independent matchers of the legacy php-cc-font-lock-keywords-3 plus keyword matchers driven by php-keywords.el and a self-contained php--fontlock-doc-comments PHPDoc matcher). Indentation delegates to php-indent.el, coding style to php-style.el. The heredoc/nowdoc, attribute and comment-quote syntax-propertize helpers are duplicated verbatim so php-mode and php-cc-mode can be loaded simultaneously. Move php-beginning/end-of-defun, imenu, page-delimiter, newcomment and navigation setup here, and register the auto-mode-alist / interpreter-mode-alist associations that php-cc-mode.el gave up. Point the Eask package-file back at lisp/php-mode.el and list the new files.
Introduce a base "php" style holding the indentation defaults (php-indent-offset, indent-tabs-mode, tab-width, php-indent-switch-case-offset, php-indent-chain-indent) and have per, pear, drupal and wordpress inherit it via php-style-parent. Previously pear/drupal/wordpress omitted indent-tabs-mode and tab-width, letting global defaults leak into the buffer.
While porting the test harness to the cc-mode independent php-mode,
several implementation bugs surfaced. Fix them at the source:
lisp/php.el (syntax table):
- Mark single-quote (') as a string delimiter; PHP single-quoted
strings were not fontified or parsed as strings at all.
- Mark `=' as punctuation instead of the Emacs default symbol
constituent, so `\_<...\_>' token matchers no longer swallow it.
lisp/php-mode.el (font-lock):
- Make the "all upper-cased symbols are constants" matcher
case-sensitive; font-lock runs with case-fold-search enabled for
PHP's case-insensitive keywords, which made [A-Z_] match everything.
- Fix php--fontlock-doc-comments to probe syntax-ppss *inside* the
comment (the opening slash reports being outside it), to apply the
doc face to the whole comment body, and to ignore a `/**' that
appears inside another comment.
- Add matchers for class names in type position: type hints written
before a $-variable, new/instanceof/insteadof/extends/implements,
namespace and use declarations, enum cases and type casts.
- Fontify self/parent/static as keywords rather than type names.
lisp/php-style.el:
- Reset show-trailing-whitespace / php-style-delete-trailing-whitespace
in the pear and wordpress styles so switching away from drupal clears
them, matching the previous behaviour.
tests/*.faces:
- Regenerate errorcontrol and doc-comment/comments fixtures where the
new output is more correct (`@new' as operator+keyword; a plain
block comment's closing `*/' is not a delimiter face).
Remove the CC Mode assumptions from the test harness so it exercises the new php-mode: - Drop the (c-after-font-lock-init) workaround and the php-mode-debug require (php-mode-debug is now specific to php-cc-mode). - Replace c-guess-basic-syntax with syntax-ppss based checks and debug output. - Replace c-basic-offset with php-indent-offset in assertions, and add a dedicated regression test that a legacy buffer-local c-basic-offset is honoured via php-style--honor-legacy-c-basic-offset. The magic directive evaluator binds c-basic-offset to php-indent-offset so the shared PHP fixtures keep working. - Drop the removed symfony2 style and switch php-lineup-cascaded-calls to php-indent-chain-indent, :style handling to php-enable-*-coding -style / php-set-style, and php-cautious-indent-line to php-indent-line. - Split php-mode-test-indentation-issues and php-mode-test-php81 so the passing fixtures stay green. Mark tests that depend on not-yet-ported features (chained-call and operator continuation alignment, stacked multi-line declarations, the delete-indentation whitespace fixup, pseudo-tag colouring) as :expected-result :failed with an explanation, instead of deleting them.
Add tests/php-cc-mode-test.el, a self-contained ert suite that drives the frozen CC Mode based php-cc-mode through a representative subset of the indentation and font-lock fixtures (using php-cc-set-style and the .faces files that still describe CC Mode's output), plus the CC Mode specific php-mode-debug command. It is run in its own Eask invocation so the two major modes, which still share several public symbol names, do not clobber each other in one Emacs session.
- Bump the minimum Emacs to 28.1 in Eask and in php-cc-mode.el's Package-Requires (php-mode.el already declares it). - Drop Emacs 27.2 from the CI matrix (28.2 is now the minimum tested). - Add a `test-cc' Makefile target and a CI step that runs the php-cc-mode regression suite in a separate Eask invocation.
The legacy `php-cc-mode' and the new `php-mode' are designed to be loadable in the same Emacs session. Several public symbols were still defined by both files, so loading `php-cc-mode' after `php-mode' silently redefined the new implementation's functions with the CC Mode based ones (e.g. `php-enable-default-coding-style' was replaced by a `c-set-style' wrapper, and `php-beginning-of-defun' by the CC Mode navigation variant). Rename every colliding `php-cc-mode' definition (and all of its in-file references, keymap remaps, advice, hooks and style-variable keys) under a `php-cc-'/`php-cc-mode-' prefix: php-enable-default-coding-style -> php-cc-enable-default-coding-style php-enable-per-coding-style -> php-cc-enable-per-coding-style php-enable-pear-coding-style -> php-cc-enable-pear-coding-style php-enable-drupal-coding-style -> php-cc-enable-drupal-coding-style php-enable-wordpress-coding-style -> php-cc-enable-wordpress-coding-style php-enable-psr2-coding-style -> php-cc-enable-psr2-coding-style php-enable-symfony2-coding-style -> php-cc-enable-symfony2-coding-style php-beginning-of-defun -> php-cc-beginning-of-defun php-end-of-defun -> php-cc-end-of-defun php-heredoc-start-re -> php-cc-heredoc-start-re php-heredoc-end-re -> php-cc-heredoc-end-re php-syntax-propertize-rules -> php-cc-syntax-propertize-rules php-syntax-propertize-function -> php-cc-syntax-propertize-function php-syntax-propertize-extend-region -> php-cc-syntax-propertize-extend-region php--syntax-propertize-heredoc -> php-cc--syntax-propertize-heredoc php--syntax-propertize-quotes-in-comment -> php-cc--syntax-propertize-quotes-in-comment php--syntax-propertize-attributes -> php-cc--syntax-propertize-attributes php-string-interpolated-variable-regexp -> php-cc-string-interpolated-variable-regexp php-phpdoc-type-names -> php-cc-phpdoc-type-names php-phpdoc-type-tags -> php-cc-phpdoc-type-tags php-phpdoc-font-lock-doc-comments -> php-cc-phpdoc-font-lock-doc-comments php-phpdoc-font-lock-keywords -> php-cc-phpdoc-font-lock-keywords php-mode-lineup-cascaded-calls -> php-cc-mode-lineup-cascaded-calls php-mode-enable-backup-style-variables -> php-cc-mode-enable-backup-style-variables php-mode--error-control-op-font-lock-find -> php-cc-mode--error-control-op-font-lock-find php-mode--string-interpolated-variable-font-lock-find -> php-cc-mode--string-interpolated-variable-font-lock-find php-mode--propertize-extend-region-current -> php-cc-mode--propertize-extend-region-current The public user-facing customization variables that both modes share by design (`php-mode-hook', `php-mode-page-delimiter', `php-mode-force-pear', `php-mode-warn-if-html-template', `php-mode-replace-flymake-diag-function', `php-mode-enable-project-coding-style') and the shared style variable `php-style-delete-trailing-whitespace' are plain `defcustom's whose second definition does not override an existing value, so they are intentionally left shared. `php-beginning-of-defun-regexp' likewise stays in php.el. The user-callable `php-enable-*-coding-style' names keep working because php-style.el defines the same names for the new `php-mode'. Stop php-mode-debug.el from requiring `php-cc-mode': the require dragged the whole legacy implementation into any session that autoloaded the debugger. Load CC Mode only at compile time, guard the CC Mode specific diagnostics on `(derived-mode-p 'php-cc-mode)', and let `php-mode-debug' run in any PHP Mode buffer.
The `:faces t' keyword was placed outside the `with-php-mode-test'
argument list --- `(with-php-mode-test ("issue-136.php") :faces t)' ---
so it was parsed as part of BODY and the macro's `faces' argument stayed
nil. The test therefore never compared any faces and passed vacuously.
Move `:faces t' inside the argument list so the face assertion runs. The
committed issue-136.php.faces was stale (it held the golden faces of an
unrelated fixture), so regenerate it from issue-136.php under the same
setup the test uses. The regenerated data confirms the cc-mode
independent `php-mode' highlights every interpolated-variable syntax
(`$name', `${name}', `{$name}', `{$user->name}', `{$users[$i]->name}',
...) with `php-variable-name', which is exactly what this test guards.
Resolve the indentation behaviours that were deferred when the cc-mode independent engine was first ported from js.el: * php-indent--skip-term-backward now skips comments and newlines between chain links, so a `->' chain aligns to the first `->' even when comment lines are interleaved (issues #115, #135). * A chain rooted in a `::' static access is no longer aligned to the operator; like CC Mode it is treated as a plain statement continuation, and inside argument lists it is indented as an ordinary element without the continued-expression bonus (php-indent--static-chain-in-list-p). * New php-indent--statement-continuation-indentation indents lines that continue an unterminated statement (stacked member modifiers, a lone `extends', a broken `return'/`yield', a return-type `):') one php-indent-offset beyond the statement's first line. * New php-indent--concat-continuation-indentation aligns a leading `.' string-concatenation line to the `=' of the assignment it continues (issue #211). * When a brace is preceded by a parameter/condition list, the bracket indentation now anchors to the start of the whole statement, which may lie on an earlier line than the `(' (namespace-block fixture). * php-indent-chain-indent is no longer set buffer-locally by the coding styles; like the old php-mode-lineup-cascaded-calls it is a plain global user option (issues #237, #623).
* The open-tag matcher now also matches `<?' followed by a word, so pseudo-tags that are not authentic PHP open tags (`<?xml', `<?hh') are fontified as tags, matching the accepted false positives of the CC Mode implementation (issue #443). * Add the declare-directive pseudo-keywords `strict_types', `encoding' and `ticks' to php-keywords--declarations; they were present in php-cc-mode's c-other-kwds but had not been carried over. * Port the fixup-whitespace advice so that delete-indentation removes whitespace around `->'/`::' and before `;'/`,' (issue #73). The function is named php-mode--fixup-whitespace so it does not clash with php-cc-mode's php-mode--fixup-whitespace-after.
All ten tests that were marked `:expected-result :failed' when the cc-mode independent php-mode landed now pass, so drop the markers and the DEFERRED notes. Only php-mode-test-issue-9 (skipped on macOS) remains deferred. Since the minimum Emacs version is 28.1, the version-conditional fixture variants are unnecessary: * issue-443.php.27.faces is removed; the plain .faces file already matches the new engine's output (comment ends are fontified with the comment face, like the other regenerated fixtures). * type-hints.php.29.faces is removed; the plain .faces file expects the `stdClass' parameter of a method named `object' to get the type face, which the new font-lock produces on every version (the .29.faces variant encoded a CC Mode regression on Emacs 29+). * 8.1/readonly.php.faces no longer expects faces on the deliberately invalid `claas Err' declaration; the regexp-based font-lock leaves malformed code unfontified.
PHP 8.5 adds two constructs that reach the indentation engine:
$slug = $title
|> trim(...)
|> strtolower(...);
return clone($this, [
'alpha' => $alpha,
]);
The pipe operator starts a continuation line with an operator, and
"clone with" gives `clone' -- until now a bare prefix keyword -- an
argument list. Both already indent correctly: `|>' matches
php-indent--indent-operator-re, so a line starting with it is a
statement continuation, and `clone(...)' is just a call as far as the
bracket handling is concerned. Pin that down before it regresses.
The fixtures cover the pipe at the top level, on the same line as the
assignment, inside a function body, inside an argument list, and piping
into static and instance methods; and clone-with inline, with a broken
argument list, and next to a plain `clone $obj'.
Verified the tests have teeth: flattening every line's leading
whitespace and reindenting restores all 35 checked lines.
See https://www.php.net/releases/8.5/en.php
php-pipe-op and its rule arrived on master (#821) in the CC Mode based php-mode.el, which this branch renames to php-cc-mode.el; the new mode body needs the same rule for the same reason. Without it the comparison-operator matcher claims the `>' of `|>' and leaves the `|' plain.
Two shapes that put a brace block somewhere the engine does not
otherwise expect one.
PHP 8.5 allows static closures and first-class callables in constant
expressions, so an attribute argument can now hold a closure body:
#[AccessControl(static function (
Request $request,
Post $post,
): bool {
return $request->user === $post->getAuthor();
})]
public function update(...)
The `#[' has to stay a token rather than a comment across all of that,
and the declaration after the `)]' has to land back at the class body's
level. The pre-8.5 spelling, which wraps the same logic in an
Expression object, is covered next to it, along with a first-class
callable, an arrow function, and an attribute holding a nested array.
PHP 8.4 property hooks attach a brace block to a property declaration
rather than to a function signature. tests/8.4/property-hooks.php
already existed but only checks faces; adding magic comments there would
force its .faces fixture to be regenerated, so the indentation shapes
get a fixture of their own: both short forms, both block forms, the two
hooks on one property, an abstract hook, a typed `set' parameter, and
hooks on a constructor-promoted property.
Both already indent correctly. Verified the tests have teeth by
flattening every line's leading whitespace and reindenting: all 68
checked lines come back.
zonuexe
force-pushed
the
rewrite/cc-free-php-mode
branch
from
July 16, 2026 18:13
34f66d5 to
c8c5b04
Compare
An arrow function whose body starts on the next line fell back to
column zero:
$f = fn($x) =>
$x + 1;
`=>' is deliberately excluded from php-indent--indent-operator-re --- in
PHP it is overwhelmingly the array key operator, and treating every
`'key' => 'value'' as a continuation would be wrong --- so nothing at
all picked up a trailing one, and the body was indented as if it began
a fresh statement.
Property hooks showed the same gap, and worse, an inconsistent one: the
first line after `get =>' stayed put while a following `.' line was
indented by php-indent--statement-continuation-indentation, which does
fire after an identifier:
get =>
$this->a
. ' ';
Treat a trailing `=>' as leaving the statement open, alongside the
identifier / `)' / `):' cases already there. The three shapes now
agree:
$f = fn($x) =>
$x + 1;
$config = [
'key' =>
'a value long enough to wrap',
];
get =>
$this->a
. ' ';
Only `=>' is matched, so `>=', `->' and `<=>' keep the handling they
had. The shape had no fixture at all, which is why the gap went
unnoticed; tests/indent/trailing-arrow.php now covers arrow functions,
array values, the bracket-on-the-same-line case, property hooks, match
arms, and a `>=' left at the end of a line.
Redefine php-complete.el as a small collection of dependency-light, offline completion-at-point functions usable both as `M-x` commands and as building blocks for `cape-capf-super'. * lisp/php.el (php-dot-context): New dependency-free primitive that classifies the context before point (`string-or-comment', `next-to-string', `code') for context-sensitive `.' insertion, without cc-engine. Meant to drive smartchr/key-combo setups. * lisp/php-complete.el: State the file's mission in the Commentary. (php-complete-complete-function): Redefine as the offline built-in function-name source (behavior unchanged). (php-complete-path-dir-constants): New option; directory-valued subset of `php-magical-constants'. (php-complete-complete-path): New capf completing a filesystem path in the `__DIR__ . '/...'' idiom, one component at a time, rooted at the directory of the current file. Robust to the unterminated string that is normal while typing. * tests/php-mode-test.el: Cover `php-dot-context' and `php-complete-complete-path'. * README.md, README.ja.md: Document the completion capfs, `cape-capf-super' composition, and a `php-dot-context'-based smartchr recipe for `.'.
Follow the "Personal Settings" pattern in the README: add the capfs from a named setup function inside `with-eval-after-load 'php-mode', rather than adding an anonymous closure to the hook, which is hard to remove or redefine later. Also fix "補完" that had been garbled to "補completion" in README.ja.md.
Add `__DIR__` path completion capf and `php-dot-context` primitive
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This branch replaces the CC Mode based implementation of
php-modewith a self-contained one, while keeping the old implementation available:lisp/php-cc-mode.el— the previous CC Mode based mode, renamed tophp-cc-modeand frozen for backward compatibility. Both modes can be loaded at the same time, andphp-mode-hookusers keep working.lisp/php-mode.el— the new mode body: nocc-moderequirement, own font-lock keywords (including PHPDoc highlighting),syntax-propertizebased heredoc/nowdoc and PHP 8 attribute handling.lisp/php-indent.el— an indentation engine ported from thesyntax-ppssbased indenter of js.el in GNU Emacs (GPL attribution in the header), extended with PHP specifics:->/?->/::chains,.concatenation alignment, statement continuations (stacked modifiers, brokenreturn,extendson its own line),switch/case, alternative syntax (endif…), and heredoc handling.lisp/php-style.el— a plain variable-alist coding style system replacingc-add-style. The default style is PER (per),psr2/pear/drupal/wordpressremain,symfony2is removed. A migration layer honors a buffer-localc-basic-offsetset via file/dir-local variables and maps it tophp-indent-offsetwith a warning.lisp/php-keywords.el— the PHP keyword vocabulary extracted from the oldc-lang-defconsttables, with provenance notes.Compatibility notes
php-mode-lineup-cascaded-calls→php-indent-chain-indent(global user option, not style-bound).c-basic-offset→php-indent-offset(with the migration layer above).?> … <?phpHTML region is left untouched by the indenter.Tests
tests/php-mode-test.elwas rewritten for the new mode; all previously deferred behaviours (chained-call alignment, operator-anchored continuations, multi-line modifiers,fixup-whitespaceintegration, pseudo open-tag fontification, and the version-specific faces fixtures) are now implemented and un-deferred. The only remaining:expected-result :failedtest isphp-mode-test-issue-9, which is skipped on macOS.tests/php-cc-mode-test.elis a new regression suite that keeps the frozenphp-cc-modehonest; it runs in CI in a separate Eask process..27.faces/.29.faces) were consolidated into plain.facesfiles now that Emacs ≥ 28.1 is required.Locally:
php-mode-test.el60/60 as expected (4 environment-dependent skips),php-cc-mode-test.el16/16, byte-compilation clean.