diff --git a/CHANGELOG.md b/CHANGELOG.md index 833b5b16..5e9a41d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ All notable changes of the PHP Mode 1.19.1 release series are documented in this * Shows whether it is on, what is configured, and what is available * Add `php-ide-eglot-activate`, which registers `php-ide-eglot-executable` into `eglot-server-programs` * Buffer-local, so only buffers that set the variable are affected; previously the variable had no effect on Eglot at all + * Add `php-cc-mode` as a forward-compatible alias for the CC Mode based `php-mode` + * Lets configuration and third-party code refer to the CC Mode implementation by the name it will keep once `php-mode` becomes cc-mode independent; loading it has no effect on `php-mode` itself + * `php-cc-mode-hook`, `php-cc-mode-lineup-cascaded-calls` and `php-cc-mode-enable-backup-style-variables` are provided as aliases of their current `php-mode-*` counterparts ### Changed diff --git a/Eask b/Eask index deef9331..70a2566f 100644 --- a/Eask +++ b/Eask @@ -14,6 +14,7 @@ "lisp/php-complete.el" "lisp/php-defs.el" "lisp/php-indent.el" + "lisp/php-style.el" "lisp/php-keywords.el" "lisp/php-face.el" "lisp/php-flymake.el" @@ -23,6 +24,7 @@ "lisp/php-ide-phpactor.el" "lisp/php-ide.el" "lisp/php-align.el" + "lisp/php-cc-mode.el" "lisp/php-mode-debug.el") (script "test" "echo \"Error: no test specified\" && exit 1") diff --git a/lisp/php-cc-mode.el b/lisp/php-cc-mode.el new file mode 100644 index 00000000..13924176 --- /dev/null +++ b/lisp/php-cc-mode.el @@ -0,0 +1,80 @@ +;;; php-cc-mode.el --- Compatibility alias for the CC Mode based php-mode -*- lexical-binding: t; -*- + +;; Copyright (C) 2026 Friends of Emacs-PHP development + +;; Author: USAMI Kenta +;; Maintainer: USAMI Kenta +;; URL: https://github.com/emacs-php/php-mode +;; Keywords: languages php +;; License: GPL-3.0-or-later + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Commentary: + +;; This file is a forward-compatibility shim introduced while PHP Mode is +;; being reworked to no longer depend on CC Mode. +;; +;; In the target design, the CC Mode based implementation is renamed to +;; `php-cc-mode' and the name `php-mode' is reused for a cc-mode +;; independent major mode. During the transition `php-mode' (in +;; php-mode.el) is still the CC Mode based implementation, so this file +;; only establishes the *names* that code will migrate to, without +;; changing any behavior: +;; +;; - `php-cc-mode' is provided as an alias for `php-mode', so that +;; configuration or third-party code written against the new name +;; works now and keeps working after the eventual swap. +;; - Variable names that will belong to `php-cc-mode' are provided as +;; aliases of their current `php-mode-*' counterparts. +;; +;; Loading this file has no effect on `php-mode' itself: it does not +;; register any `auto-mode-alist' / `interpreter-mode-alist' entry, and +;; it does not redefine `php-mode'. It is intentionally kept out of the +;; `php-mode' require graph and is loaded only when something asks for +;; `php-cc-mode'. + +;;; Code: + +(require 'php-mode) + +;;;###autoload +(defalias 'php-cc-mode 'php-mode + "Major mode for editing PHP code, based on CC Mode. + +This is currently an alias for `php-mode'. It exists so that code can +already refer to the CC Mode based implementation by the name it will +keep (`php-cc-mode') once the default `php-mode' becomes the cc-mode +independent implementation.") + +;; Hook: configuration hung on `php-cc-mode-hook' should run for the CC +;; Mode based mode. While `php-cc-mode' is an alias for `php-mode', the +;; hook is an alias too, so setting either one has the same effect. +(defvaralias 'php-cc-mode-hook 'php-mode-hook + "Hook run when entering `php-cc-mode'. +Currently an alias for `php-mode-hook'.") + +;; Variables that will be owned by `php-cc-mode' in the target design. +;; They are aliased to the current `php-mode-*' names so that either +;; spelling works during the transition. +(defvaralias 'php-cc-mode-lineup-cascaded-calls 'php-mode-lineup-cascaded-calls + "Indent chained method calls to the previous line. +Currently an alias for `php-mode-lineup-cascaded-calls'.") + +(defvaralias 'php-cc-mode-enable-backup-style-variables 'php-mode-enable-backup-style-variables + "When non-nil, back up values set by hook and buffer local variables. +Currently an alias for `php-mode-enable-backup-style-variables'.") + +(provide 'php-cc-mode) +;;; php-cc-mode.el ends here diff --git a/lisp/php-mode.el b/lisp/php-mode.el index 4fd23b29..37d4297d 100644 --- a/lisp/php-mode.el +++ b/lisp/php-mode.el @@ -165,11 +165,7 @@ Turning this on will open it whenever `php-mode' is loaded." (when val (speedbar 1)))) -(define-obsolete-variable-alias 'php-template-compatibility 'php-mode-template-compatibility "1.20.0") -(defcustom php-mode-template-compatibility t - "Should detect presence of html tags." - :tag "PHP Mode Template Compatibility" - :type 'boolean) +;; `php-mode-template-compatibility' is defined in php.el (shared). (define-obsolete-variable-alias 'php-lineup-cascaded-calls 'php-mode-lineup-cascaded-calls "1.20.0") (defcustom php-mode-lineup-cascaded-calls nil @@ -216,31 +212,14 @@ enabled." :tag "PHP Mode Hook" :type 'hook) -(defcustom php-mode-pear-hook nil - "Hook called when a PHP PEAR file is opened with `php-mode'." - :tag "PHP Mode Pear Hook" - :type 'hook) - -(defcustom php-mode-drupal-hook nil - "Hook called when a Drupal file is opened with `php-mode'." - :tag "PHP Mode Drupal Hook" - :type 'hook) - -(defcustom php-mode-wordpress-hook nil - "Hook called when a WordPress file is opened with `php-mode'." - :tag "PHP Mode WordPress Hook" - :type 'hook) +;; `php-mode-pear-hook', `php-mode-drupal-hook', `php-mode-wordpress-hook' +;; and `php-mode-psr2-hook' are defined in php.el (shared). (defcustom php-mode-symfony2-hook nil "Hook called when a Symfony2 file is opened with `php-mode'." :tag "PHP Mode Symfony2 Hook" :type 'hook) -(defcustom php-mode-psr2-hook nil - "Hook called when a PSR-2 file is opened with `php-mode'." - :tag "PHP Mode PSR-2 Hook" - :type 'hook) - (defcustom php-mode-force-pear nil "Normally PEAR coding rules are enforced only when the filename contains \"PEAR\". Turning this on will force PEAR rules on all PHP files." @@ -254,24 +233,7 @@ Turning this on will force PEAR rules on all PHP files." :type '(choice (const :tag "Warn" t) (const :tag "Don't warn" nil))) (make-obsolete-variable 'php-mode-warn-if-mumamo-off 'php-mode-warn-if-html-template "2.0.0") -(defcustom php-mode-coding-style 'pear - "Select default coding style to use with `php-mode'. -This variable can take one of the following symbol values: - -`Default' - use a reasonable default style for PHP. -`PSR-2' - use PSR standards (PSR-2, PSR-12). -`PEAR' - use coding styles preferred for PEAR code and modules. -`Drupal' - use coding styles preferred for working with Drupal projects. -`WordPress' - use coding styles preferred for working with WordPress projects. -`Symfony2' - use coding styles preferred for working with Symfony2 projects." - :tag "PHP Mode Coding Style" - :type '(choice (const :tag "Default" php) - (const :tag "PEAR" pear) - (const :tag "Drupal" drupal) - (const :tag "WordPress" wordpress) - (const :tag "Symfony2" symfony2) - (const :tag "PSR-2" psr2)) - :initialize #'custom-initialize-default) +;; `php-mode-coding-style' is defined in php.el (shared). ;; Since this function has a bad influence on the environment of many users, ;; temporarily disable it diff --git a/lisp/php-style.el b/lisp/php-style.el new file mode 100644 index 00000000..24a8a1ce --- /dev/null +++ b/lisp/php-style.el @@ -0,0 +1,271 @@ +;;; php-style.el --- Coding style management for PHP Mode -*- lexical-binding: t; -*- + +;; Copyright (C) 2026 Friends of Emacs-PHP development + +;; Author: USAMI Kenta +;; Keywords: languages, php +;; Homepage: https://github.com/emacs-php/php-mode +;; License: GPL-3.0-or-later + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Commentary: + +;; This file provides a self-contained, variable-based coding style +;; mechanism for the cc-mode independent `php-mode'. Unlike the legacy +;; `php-cc-mode', styles here are plain alists of (VARIABLE . VALUE) pairs +;; applied as buffer-local variables; there is no dependency on CC Mode's +;; `c-add-style' / `c-set-style' machinery. +;; +;; The indentation variables referenced by the styles below +;; (`php-indent-offset' and `php-indent-switch-case-offset') are +;; defined in php-indent.el, which is +;; implemented in a parallel task. They are forward-declared with +;; `defvar' here so that this file byte-compiles cleanly whether or not +;; php-indent.el has been loaded yet. + +;;; Code: + +(require 'php) + +;; Forward declarations for variables owned by php-indent.el (implemented +;; separately). These `defvar' forms exist only to keep the byte +;; compiler quiet; php-indent.el is the canonical definition site. +(defvar php-indent-offset) +(defvar php-indent-switch-case-offset) + +;; Forward declaration for the shared coding-style option. In the target +;; design this lives in php.el; while php-style.el is dormant scaffolding +;; it is only referenced, never defined, here. +(defvar php-mode-coding-style) + +;;; Style definitions + +(defvar php-style-alist + '(("php" . ((php-indent-offset . 4) + (indent-tabs-mode . nil) + (tab-width . 4) + (php-indent-switch-case-offset . 4))) + ("per" . ((php-style-parent . "php") + (fill-column . 120) + (show-trailing-whitespace . t) + (php-style-delete-trailing-whitespace . t))) + ("psr2" . ((php-style-parent . "per") + (fill-column . 78))) + ("pear" . ((php-style-parent . "php") + (php-indent-switch-case-offset . 0) + (show-trailing-whitespace . nil) + (php-style-delete-trailing-whitespace . nil))) + ("drupal" . ((php-style-parent . "php") + (php-indent-offset . 2) + (tab-width . 2) + (fill-column . 78) + (show-trailing-whitespace . t) + (php-style-delete-trailing-whitespace . t))) + ("wordpress" . ((php-style-parent . "php") + (indent-tabs-mode . t) + (fill-column . 78) + (show-trailing-whitespace . nil) + (php-style-delete-trailing-whitespace . nil)))) + "Alist of PHP Mode coding styles. + +Each element is (STYLE-NAME . VARIABLE-ALIST) where STYLE-NAME is a +string and VARIABLE-ALIST is an alist of (VARIABLE . VALUE) pairs to be +set buffer-locally by `php-set-style'. + +A style may inherit from another style with a single level of +inheritance by including a (php-style-parent . PARENT-NAME) entry; +`php-style--resolve' merges the parent's variables underneath the +child's own, with the child's values taking precedence.") + +(defun php-style--resolve (stylename) + "Resolve STYLENAME in `php-style-alist' into a flat variable alist. + +Follows a single `php-style-parent' inheritance link (if present), +merging the parent's resolved alist underneath STYLENAME's own +entries so that the child's values win. The pseudo-variable +`php-style-parent' itself is stripped from the result. + +Signal an error if STYLENAME is not present in `php-style-alist'." + (let* ((entry (or (cdr (assoc stylename php-style-alist)) + (error "Undefined PHP coding style: %s" stylename))) + (parent (cdr (assq 'php-style-parent entry))) + (own (assq-delete-all 'php-style-parent (copy-alist entry))) + (parent-alist (if (stringp parent) (php-style--resolve parent) nil))) + (append own + (seq-remove (lambda (pair) (assq (car pair) own)) parent-alist)))) + +;;; php-style-delete-trailing-whitespace + +(defcustom php-style-delete-trailing-whitespace nil + "When non-nil, delete trailing whitespace in this buffer before saving. + +This is set buffer-locally by `php-set-style' according to the current +coding style, and controls whether `delete-trailing-whitespace' is +added to `before-save-hook' buffer-locally." + :group 'php + :tag "PHP Style Delete Trailing Whitespace" + :type 'boolean + :safe #'booleanp) +(make-variable-buffer-local 'php-style-delete-trailing-whitespace) + +;;; php-set-style + +(defvar php-style-set-style-history nil + "History of style names passed to `php-set-style'.") + +(defvar-local php-style--current-style nil + "Name of the coding style last applied by `php-set-style' in this buffer.") + +(defun php-set-style (stylename &optional dont-override) + "Set the current `php-mode' buffer to use the coding style STYLENAME. + +STYLENAME is one of the names in `php-style-alist' (customarily one +of the values accepted by `php-mode-coding-style'). + +If DONT-OVERRIDE is non-nil, variables that already have a buffer-local +value in this buffer are left untouched; only variables without an +existing buffer-local value are set. + +After applying the style's variables, this function manages +`before-save-hook' according to `php-style-delete-trailing-whitespace' +and finally runs the hook `php-mode-STYLENAME-hook' (e.g. +`php-mode-per-hook' for STYLENAME \"per\")." + (interactive + (list (completing-read "Which PHP coding style? " + (mapcar #'car php-style-alist) + nil t nil + 'php-style-set-style-history))) + (let ((resolved (php-style--resolve stylename))) + (dolist (pair resolved) + (let ((var (car pair)) + (val (cdr pair))) + (unless (and dont-override (local-variable-p var)) + (set (make-local-variable var) val))))) + (if (eq php-style-delete-trailing-whitespace t) + (add-hook 'before-save-hook #'delete-trailing-whitespace nil t) + (remove-hook 'before-save-hook #'delete-trailing-whitespace t)) + (setq php-style--current-style stylename) + (run-hooks (intern (format "php-mode-%s-hook" stylename))) + stylename) + +;;; Style wrapper commands + +(defun php-enable-per-coding-style () + "Set PHP Mode to use the PHP-FIG PER coding style." + (interactive) + (php-set-style "per")) + +(defun php-enable-default-coding-style () + "Set PHP Mode to use reasonable default formatting (the PER style)." + (interactive) + (php-set-style "per")) + +(defun php-enable-pear-coding-style () + "Set up `php-mode' to use the coding styles preferred for PEAR code and modules." + (interactive) + (php-set-style "pear")) + +(defun php-enable-drupal-coding-style () + "Make `php-mode' use coding styles that are preferable for working with Drupal." + (interactive) + (php-set-style "drupal")) + +(defun php-enable-wordpress-coding-style () + "Make `php-mode' use coding styles preferable for working with WordPress." + (interactive) + (php-set-style "wordpress")) + +(defun php-enable-psr2-coding-style () + "Make `php-mode' comply with the PSR-2 / PSR-12 coding style." + (interactive) + (php-set-style "psr2")) + +(define-obsolete-function-alias 'php-enable-symfony2-coding-style + #'php-enable-per-coding-style "1.27.0" + "The Symfony2 coding style has been removed; use the PER coding style +instead, which supersedes it.") + +;;; c-basic-offset migration layer + +(defcustom php-mode-enable-project-coding-style nil + "When non-nil, override `php-mode-coding-style' with `php-project-coding-style'. + +If you want to suppress styles from being overwritten by directory / +file local variables, set this to nil." + :group 'php + :tag "PHP Mode Enable Project Coding Style" + :type 'boolean) + +(defun php-style--honor-legacy-c-basic-offset () + "Honor a legacy buffer-local `c-basic-offset' by mapping it to +`php-indent-offset'. + +Some projects still set `c-basic-offset' (a CC Mode variable) via +file-local or directory-local variables to control indentation. The +cc-mode independent `php-mode' does not consult `c-basic-offset' for +indentation; this function bridges the gap for backward compatibility +by copying a buffer-local integer value of `c-basic-offset' into +`php-indent-offset' and warning that `c-basic-offset' is obsolete in +this context." + (when (and (local-variable-p 'c-basic-offset) + (boundp 'c-basic-offset) + (integerp (symbol-value 'c-basic-offset))) + (set (make-local-variable 'php-indent-offset) (symbol-value 'c-basic-offset)) + (display-warning 'php-mode + "`c-basic-offset' is obsolete here; use `php-indent-offset'" + :warning))) + +(defun php-style--apply-project-or-default-style () + "Apply the project's coding style, or fall back to `php-mode-coding-style'. + +Intended to run from `hack-local-variables-hook' once directory/file +local variables (including `php-project-coding-style') are known. +Removes itself from the buffer-local hook after running." + (let ((style (or (and (bound-and-true-p php-project-coding-style) + (symbol-name php-project-coding-style)) + (and php-mode-coding-style (symbol-name php-mode-coding-style))))) + (when style + (php-set-style style))) + (remove-hook 'hack-local-variables-hook #'php-style--apply-project-or-default-style t)) + +;;;###autoload +(defun php-style-setup-buffer () + "Set up buffer-local coding style state for `php-mode'. + +This is the entry point the new cc-mode independent `php-mode' calls +during major-mode initialization. It performs three things: + +1. Applies the coding style named by `php-mode-coding-style' (or, when + `php-mode-enable-project-coding-style' is non-nil, delays this until + directory/file local variables --- in particular + `php-project-coding-style' --- have been processed, so that the + project's style can take priority). +2. Buffer-locally hooks `php-style--honor-legacy-c-basic-offset' onto + `hack-local-variables-hook' so that a `c-basic-offset' set via + file/directory local variables is still honored. +3. Runs `php-style--honor-legacy-c-basic-offset' once immediately, in + case `c-basic-offset' was already made buffer-local before this + function was called." + (if php-mode-enable-project-coding-style + (add-hook 'hack-local-variables-hook + #'php-style--apply-project-or-default-style t t) + (when php-mode-coding-style + (php-set-style (symbol-name php-mode-coding-style)))) + (add-hook 'hack-local-variables-hook + #'php-style--honor-legacy-c-basic-offset t t) + (php-style--honor-legacy-c-basic-offset)) + +(provide 'php-style) +;;; php-style.el ends here diff --git a/lisp/php.el b/lisp/php.el index 26bbd4b7..1a0bb8a0 100644 --- a/lisp/php.el +++ b/lisp/php.el @@ -243,6 +243,64 @@ out and get `php-default-major-mode' instead." :group 'php :tag "PHP Static Method Call" :type 'face) + +;;; Shared PHP Mode coding-style options +;; +;; These options are consulted by more than one major mode implementation +;; (the CC Mode based `php-mode' and, in the future, the cc-mode +;; independent one), so they live here in the shared php.el rather than in +;; php-mode.el. + +(define-obsolete-variable-alias 'php-template-compatibility 'php-mode-template-compatibility "1.20.0") +(defcustom php-mode-template-compatibility t + "Should detect presence of html tags." + :group 'php + :tag "PHP Mode Template Compatibility" + :type 'boolean) + +(defcustom php-mode-coding-style 'pear + "Select default coding style to use with `php-mode'. +This variable can take one of the following symbol values: + +`Default' - use a reasonable default style for PHP. +`PSR-2' - use PSR standards (PSR-2, PSR-12). +`PEAR' - use coding styles preferred for PEAR code and modules. +`Drupal' - use coding styles preferred for working with Drupal projects. +`WordPress' - use coding styles preferred for working with WordPress projects. +`Symfony2' - use coding styles preferred for working with Symfony2 projects." + :group 'php + :tag "PHP Mode Coding Style" + :type '(choice (const :tag "Default" php) + (const :tag "PEAR" pear) + (const :tag "Drupal" drupal) + (const :tag "WordPress" wordpress) + (const :tag "Symfony2" symfony2) + (const :tag "PSR-2" psr2)) + :initialize #'custom-initialize-default) + +(defcustom php-mode-pear-hook nil + "Hook called when a PHP PEAR file is opened with `php-mode'." + :group 'php + :tag "PHP Mode Pear Hook" + :type 'hook) + +(defcustom php-mode-drupal-hook nil + "Hook called when a Drupal file is opened with `php-mode'." + :group 'php + :tag "PHP Mode Drupal Hook" + :type 'hook) + +(defcustom php-mode-wordpress-hook nil + "Hook called when a WordPress file is opened with `php-mode'." + :group 'php + :tag "PHP Mode WordPress Hook" + :type 'hook) + +(defcustom php-mode-psr2-hook nil + "Hook called when a PSR-2 file is opened with `php-mode'." + :group 'php + :tag "PHP Mode PSR-2 Hook" + :type 'hook) ;;; PHP Keywords (defconst php-re-token-symbols diff --git a/tests/php-mode-test.el b/tests/php-mode-test.el index 454ebdf6..f86214ca 100644 --- a/tests/php-mode-test.el +++ b/tests/php-mode-test.el @@ -34,6 +34,7 @@ (require 'php-complete) (require 'php-defs) (require 'php-mode) +(require 'php-cc-mode) (require 'php-mode-debug) (require 'php-project) (require 'php-ide) @@ -863,6 +864,19 @@ half-fontified: the `|' plain and the `>' as `php-comparison-op'." (with-php-mode-test ("indent/issue-227.php" :indent t :magic t :style pear)) (with-php-mode-test ("indent/issue-774.php" :indent t :magic t :style pear))) +(ert-deftest php-cc-mode-test-transition-aliases () + "The transitional `php-cc-mode' names alias the current CC Mode `php-mode'." + (should (eq (symbol-function 'php-cc-mode) 'php-mode)) + (let ((php-mode-hook nil)) + (add-hook 'php-cc-mode-hook #'ignore) + (should (memq #'ignore php-mode-hook))) + (let ((php-mode-lineup-cascaded-calls nil) + (php-mode-enable-backup-style-variables t)) + (setq php-cc-mode-lineup-cascaded-calls t) + (setq php-cc-mode-enable-backup-style-variables nil) + (should php-mode-lineup-cascaded-calls) + (should-not php-mode-enable-backup-style-variables))) + (ert-deftest php-complete-test-function-module-names-match-alist () "`php-defs-function-module-names' must list every module of the alist.