diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7aeaf5c4..50cce894 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,7 +20,6 @@ jobs: matrix: os: [ubuntu-latest, macos-latest, windows-latest] emacs-version: - - "27.2" - "28.2" - "29.4" - "30.2" @@ -35,9 +34,6 @@ jobs: - os: windows-latest emacs-version: snapshot experimental: true - exclude: - - os: macos-latest - emacs-version: "27.2" steps: - uses: actions/setup-python@v6 with: @@ -52,5 +48,7 @@ jobs: - uses: actions/checkout@v7 - name: Run tests run: 'make .eask test' + - name: Run php-cc-mode regression tests + run: 'make test-cc' - name: Run tests (allow failure) run: 'make .eask test || true' diff --git a/CHANGELOG.md b/CHANGELOG.md index 115ef78d..c5ee756c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,15 +4,44 @@ All notable changes of the PHP Mode 1.19.1 release series are documented in this ## Unreleased +### Added + + * **Rewrite `php-mode` as a CC Mode independent major mode** + * Indentation is handled by `php-indent.el`, a `syntax-ppss` based engine ported from `js.el` bundled with GNU Emacs + * Coding styles are handled by `php-style.el`, a variable-based style system that replaces CC Mode's `c-set-style` + * PHP language keyword tables are provided by `php-keywords.el` + * Preserve the legacy CC Mode based implementation as `php-cc-mode` + * `php-cc-mode` is in frozen maintenance: only regression fixes are accepted, and new development happens in the CC Mode independent `php-mode` + * For backward compatibility, `php-cc-mode` runs `php-mode-hook` in addition to its own `php-cc-mode-hook` + * Settings described in [the CC Mode manual] now apply only to `php-cc-mode`; see the commentary at the top of `lisp/php-cc-mode.el` + ### Changed + * Change the default of `php-mode-coding-style` from `pear` to `per` ([PER Coding Style 2.0]) * Add `readonly` class modifier to [Imenu] ([#802]) * Add `enum` support to `php-current-class` ([#802]) * Remove hardcoding of implicit paths in `php` that are not guaranteed to exist ([#803]) +### Deprecated + + * `c-basic-offset` is obsolete in `php-mode`; use `php-indent-offset` instead + * A migration layer still honors a buffer-local `c-basic-offset` (for example set via `.dir-locals.el`) by copying its value into `php-indent-offset` with a warning + * `php-mode-lineup-cascaded-calls` is obsolete; use `php-indent-chain-indent` instead + * `php-mode-enable-backup-style-variables` is obsolete; use `php-style-delete-trailing-whitespace` instead + +### Removed + + * **Drop support for Emacs 27; PHP Mode now requires Emacs 28.1 or later** ([#811]) + * Remove the `symfony2` coding style from `php-mode`, which is superseded by `per` + * The style remains available in `php-cc-mode` + * `php-enable-symfony2-coding-style` is now an obsolete alias for `php-enable-per-coding-style` + [Imenu]: https://www.gnu.org/software/emacs/manual/html_node/emacs/Imenu.html +[PER Coding Style 2.0]: https://www.php-fig.org/per/coding-style/ +[the CC Mode manual]: https://www.gnu.org/software/emacs/manual/html_mono/ccmode.html [#802]: https://github.com/emacs-php/php-mode/pull/802 [#803]: https://github.com/emacs-php/php-mode/pull/803 +[#811]: https://github.com/emacs-php/php-mode/issues/811 ## [1.27.0] - 2024-12-20 diff --git a/Eask b/Eask index deef9331..904fdbf2 100644 --- a/Eask +++ b/Eask @@ -10,11 +10,14 @@ (package-file "lisp/php-mode.el") (files "lisp/php.el" + "lisp/php-mode.el" "lisp/php-core.el" - "lisp/php-complete.el" - "lisp/php-defs.el" + "lisp/php-cc-mode.el" "lisp/php-indent.el" + "lisp/php-style.el" "lisp/php-keywords.el" + "lisp/php-complete.el" + "lisp/php-defs.el" "lisp/php-face.el" "lisp/php-flymake.el" "lisp/php-format.el" @@ -30,7 +33,7 @@ (source 'melpa) (source 'gnu) -(depends-on "emacs" "27.1") +(depends-on "emacs" "28.1") (development (depends-on "phpactor") diff --git a/Makefile b/Makefile index ff55d907..d912dc8d 100644 --- a/Makefile +++ b/Makefile @@ -52,4 +52,11 @@ test: clean all $(EASK) install polymode web-mode $(EASK) test ert ./tests/php-mode-test.el -.PHONY: all authors autoloads clean test +# Regression tests for the frozen CC Mode based `php-cc-mode'. Run in a +# separate Eask invocation (process) from the `php-mode' tests so that +# the two major modes, which still share several public symbol names, do +# not clobber each other in a single Emacs session. +test-cc: clean all + $(EASK) test ert ./tests/php-cc-mode-test.el + +.PHONY: all authors autoloads clean test test-cc diff --git a/README.ja.md b/README.ja.md index 2222cbb3..bdeec955 100644 --- a/README.ja.md +++ b/README.ja.md @@ -19,6 +19,9 @@ A powerful and flexible Emacs major mode for editing PHP scripts > [!NOTE] > [最新版][releases]のPHP ModeはEmacs 30をサポートしています。
アップグレードに伴うトラブルは[Discussions][discussions-emacs30]に気軽に書き込んでください。 +> [!IMPORTANT] +> PHP Modeは書き直され、CC Modeに依存しなくなりました。インデントはGNU Emacsに同梱されている`js.el`に由来するエンジンで処理されます。従来のCC Modeベースの実装は`php-cc-mode`として引き続き利用できます。詳しくは[レガシーなCC Mode実装](#レガシーなcc-mode実装-php-cc-mode)をお読みください。 + > [!WARNING] > Emacsをアップグレードした直後に初めてPHPファイルを開いたときに、CC Mode関連のエラーが発生する可能性があります。これは以前のバージョンのEmacsでバイトコンパイルされたPHP Modeがディスクにキャッシュされているために起こるので、PHP Modeの再インストールによって解決します。 > @@ -29,7 +32,7 @@ A powerful and flexible Emacs major mode for editing PHP scripts ## インストール -**PHP ModeはEmacs 27.1以降で動作します**。対応バージョンの詳細は[Supported Version]をお読みください。Emacs 28以降では単に以下のコマンドを実行するだけでインストールできます。 +**PHP ModeはEmacs 28.1以降で動作します**([#811])。対応バージョンの詳細は[Supported Version]をお読みください。Emacs 28以降では単に以下のコマンドを実行するだけでインストールできます。 ``` M-x package-install php-mode @@ -75,6 +78,67 @@ M-x package-install php-mode (php-project-coding-style . psr2))) ``` +### コーディングスタイル + +`php-mode`は`php-mode-coding-style`に従ってインデントや編集関連の変数を設定します。既定のスタイルは`per`([PER Coding Style 2.0][per-cs])です。以前のバージョンの既定は`pear`でした。利用できるスタイルは`per`、`psr2`、`pear`、`drupal`、`wordpress`です。 + +`symfony2`スタイルは削除されました。現代のSymfonyが採用するコーディングスタイルはPERに置き換えられているためです。`php-cc-mode`では引き続き利用できます。 + +インデントエンジンはもはやCC Modeを使わないため、`php-mode`では`c-basic-offset`はobsoleteです。かわりに`php-indent-offset`をカスタマイズしてください。後方互換のため、プロジェクトが`c-basic-offset`をバッファローカルに設定している場合(`.dir-locals.el`やファイルローカル変数など)は、`php-mode`がその値を`php-indent-offset`に引き継ぎ、警告を表示します。 + +## 補完 + +`php-complete.el`は、言語サーバーなしでも使える依存の軽い小さな`completion-at-point`関数(capf)をいくつか提供します。いずれも`M-x`コマンドとしても、[`cape`][cape]の`cape-capf-super`で合成する部品としても使えます。 + +- `php-complete-complete-function` — 組み込み関数名。 +- `php-complete-complete-path` — `__DIR__ . '/...'`イディオムの中でパスを補完します。1階層ずつ辿り、起点は現在のファイルのディレクトリ(実行時に`__DIR__`が解決する先)です。 + +名前付き関数から登録すると、あとから設定を変更しやすくなります。無名クロージャをフックに追加すると、削除や再定義が困難になります。 + +```elisp +(defun my-php-mode-setup-completion () + "現在のバッファで `php-mode' の補完ソースを有効にする。" + (add-hook 'completion-at-point-functions + #'php-complete-complete-path nil t)) + +(with-eval-after-load 'php-mode + (add-hook 'php-mode-hook #'my-php-mode-setup-completion)) +``` + +`cape`で複数のオフラインソースを1つの super-capf に合成するには、同じ関数の中で`cape-capf-super`を登録します。 + +```elisp +(defun my-php-mode-setup-completion () + "現在のバッファで `php-mode' の補完ソースを有効にする。" + (add-hook 'completion-at-point-functions + (cape-capf-super #'php-complete-complete-function + #'php-complete-complete-path) + nil t)) +``` + +### 文脈依存の `.` キー + +`__DIR__`とパス補完を橋渡しする`. '/'`の挿入は、あえてcapfの仕事にはしていません。これは編集環境側に委ねます。そのための primitive が`php-dot-context`です。ポイントが文字列/コメント内か(`string-or-comment`)、文字列リテラルや`__DIR__`のようなマジック定数の直後か(`next-to-string`)、素のコードか(`code`)を返します。capf とこの primitive は「文字列」と「マジック定数」の定義を共有するため、キー挿入と補完の挙動が食い違いません。 + +たとえば[smartchr][smartchr]では、`.`キーをコード中では`->` / `.` / `. `で循環させ、文字列内ではリテラルの`.`を挿入し、`__DIR__`の直後では`. `を優先(そのまま`php-complete-complete-path`につながる)といった設定が書けます。 + +```elisp +(defun my-php-smartchr-dot (code within-string next-to-string) + "`php-dot-context' を使って `.' キーの smartchr を作る。" + (let ((select (lambda () + (pcase (php-dot-context) + ('string-or-comment within-string) + ('next-to-string next-to-string) + (_ code))))) + (smartchr-make-struct + :cleanup-fn (lambda () (delete-char (- (length (funcall select))))) + :insert-fn (lambda () (insert (funcall select)))))) + +;; (smartchr (my-php-smartchr-dot "->" "." ". ") +;; (my-php-smartchr-dot ". " ".." "..") +;; "...") +``` + ## HTMLとPHPが混在するファイルの編集 `php-mode`は純粋なPHPスクリプトのためのメジャーモードです。テンプレートのようにHTMLの中にPHPを埋め込んだファイルは、両方の言語を理解するメジャーモードで編集するほうが適しています。特にインデントは、HTML部分を素の`php-mode`で編集すると正しく動作しません。 @@ -107,6 +171,12 @@ M-x package-install php-mode すでに`php-mode`でHTMLタグを含むファイルをインデントしようとすると、PHP Modeは警告し、`php-html-template-major-mode`への切り替えを尋ねます。このプロンプトを無効にするには`php-mode-warn-if-html-template`を`nil`に設定してください。 +## レガシーなCC Mode実装 (php-cc-mode) + +これまでのPHP ModeはEmacs組み込みのCC Modeの上に実装されていました。この実装は`php-cc-mode`として保存されており、凍結メンテナンス状態です。リグレッション修正のみ受け付け、新規の開発はCC Mode非依存の`php-mode`で行われます。レガシー実装でファイルを開くには`M-x php-cc-mode`を実行するか、自分で`auto-mode-alist`にエントリを追加してください。 + +既存の設定が動き続けるように、`php-cc-mode`は自身の`php-cc-mode-hook`に加えて`php-mode-hook`も実行します。[CC Modeマニュアル][cc-mode-manual]に記載されている設定が適用されるのは`php-cc-mode`だけです。詳細は`lisp/php-cc-mode.el`冒頭のコメントをお読みください。 + ## 不具合を報告する バグ報告の際には `M-x php-mode-debug` の出力を含めてください。この情報は問題の再現に役立ちます。 @@ -120,7 +190,7 @@ M-x package-install php-mode PHP Modeは[GNU General Public License Version 3][gpl-v3] (GPLv3) でライセンスされています。 -このプロジェクトは1999年に[Turadg Aleahmad][@turadg]が書いた`php-mode.el`に起源を持ちます。2013年に[Daniel Hackney][@haxney]がEmacs組み込みのCC Modeをもとに書き直し始めました。PHPモードの改善に協力した貢献者のリストは[Authors]と[Contributors]に掲載されています。 +このプロジェクトは1999年に[Turadg Aleahmad][@turadg]が書いた`php-mode.el`に起源を持ちます。2013年に[Daniel Hackney][@haxney]がEmacs組み込みのCC Modeをもとに書き直し始めました。2026年にCC Modeへの依存を取り除くための書き直しが行われました。PHPモードの改善に協力した貢献者のリストは[Authors]と[Contributors]に掲載されています。 このプロジェクトは2017年まで[Eric James Michael Ritz][@ejmr]によりメンテナンスされていました。現在は[Friends of Emacs-PHP Development][@emacs-php]コミュニティが引き継いで開発しています。 @@ -151,7 +221,12 @@ PHP Modeは[GNU General Public License Version 3][gpl-v3] (GPLv3) でライセ [Authors]: https://github.com/emacs-php/php-mode/wiki/Authors [Contributors]: https://github.com/emacs-php/php-mode/graphs/contributors [Supported Version]: https://github.com/emacs-php/php-mode/wiki/Supported-Version +[cape]: https://github.com/minad/cape +[cc-mode-manual]: https://www.gnu.org/software/emacs/manual/html_mono/ccmode.html [gpl-v3]: https://www.gnu.org/licenses/gpl-3.0 +[per-cs]: https://www.php-fig.org/per/coding-style/ +[smartchr]: https://github.com/imakado/emacs-smartchr +[#811]: https://github.com/emacs-php/php-mode/issues/811 [nongnu-elpa-badge]: https://elpa.nongnu.org/nongnu/php-mode.svg [nongnu-elpa]: https://elpa.nongnu.org/nongnu/php-mode.html [melpa-badge]: http://melpa.org/packages/php-mode-badge.svg diff --git a/README.md b/README.md index 94421fb8..00451445 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,9 @@ Please submit any bug reports or feature requests by creating issues on [the Git > The [latest version][releases] of PHP Mode supports Emacs 30. > Please feel free to [open a discussion][discussions-emacs30] if you have any issues upgrading to Emacs 30. +> [!IMPORTANT] +> PHP Mode has been rewritten and no longer depends on CC Mode. Indentation is now handled by an engine derived from `js.el`, which is bundled with GNU Emacs. The previous CC Mode based implementation is still available as `php-cc-mode`; see [Legacy CC Mode implementation](#legacy-cc-mode-implementation-php-cc-mode). + > [!WARNING] > After upgrading Emacs, when you open a PHP file for the first time, you may encounter errors related to CC Mode. These errors occur because a previously byte-compiled version of PHP Mode, cached on your disk, differs from the newly installed one. Reinstalling PHP Mode should resolve the issue. > @@ -30,7 +33,7 @@ Please submit any bug reports or feature requests by creating issues on [the Git ## Installation -**PHP Mode works with Emacs 27.1 or later.** For details on supported versions, see [Supported Version]. +**PHP Mode works with Emacs 28.1 or later** ([#811]). For details on supported versions, see [Supported Version]. On Emacs 28 or later, you can install it simply by running: ``` @@ -77,6 +80,67 @@ You can add project-specific settings by creating a `.dir-locals.el` or `.dir-lo (php-project-coding-style . psr2))) ``` +### Coding Styles + +`php-mode` sets indentation and related editing variables according to `php-mode-coding-style`. The default style is `per` ([PER Coding Style 2.0][per-cs]); previous versions defaulted to `pear`. The available styles are `per`, `psr2`, `pear`, `drupal` and `wordpress`. + +The `symfony2` style has been removed, since PER supersedes the coding style used by modern Symfony. It remains available in `php-cc-mode`. + +Because the indentation engine no longer uses CC Mode, `c-basic-offset` is obsolete in `php-mode`; customize `php-indent-offset` instead. For backward compatibility, when a project sets `c-basic-offset` buffer-locally (for example via `.dir-locals.el` or file-local variables), `php-mode` copies that value into `php-indent-offset` and displays a warning. + +## Completion + +`php-complete.el` provides a few small, dependency-light `completion-at-point` functions (capfs) for use without a language server. Each is usable both as an `M-x` command and as a building block for [`cape`][cape]'s `cape-capf-super`: + +- `php-complete-complete-function` — built-in function names. +- `php-complete-complete-path` — a filesystem path inside the `__DIR__ . '/...'` idiom, completed one component at a time and rooted at the directory of the current file (what `__DIR__` resolves to at runtime). + +Register them from a named function so the setup stays easy to change later; adding an anonymous closure to the hook makes it hard to remove or redefine. + +```elisp +(defun my-php-mode-setup-completion () + "Enable `php-mode' completion sources for the current buffer." + (add-hook 'completion-at-point-functions + #'php-complete-complete-path nil t)) + +(with-eval-after-load 'php-mode + (add-hook 'php-mode-hook #'my-php-mode-setup-completion)) +``` + +To compose several offline sources into one super-capf with `cape`, register `cape-capf-super` inside the same function instead: + +```elisp +(defun my-php-mode-setup-completion () + "Enable `php-mode' completion sources for the current buffer." + (add-hook 'completion-at-point-functions + (cape-capf-super #'php-complete-complete-function + #'php-complete-complete-path) + nil t)) +``` + +### A context-sensitive `.` key + +Inserting the `. '/'` that bridges `__DIR__` into path completion is deliberately *not* the capf's job; it is left to your editing setup. `php-dot-context` is the primitive for that: it reports whether point is inside a string or comment (`string-or-comment`), directly after a string literal or a magic constant such as `__DIR__` (`next-to-string`), or in plain code (`code`). Because the capf and this primitive share the same notion of "string" and "magic constant", key-driven insertion and completion stay consistent. + +For example, with [smartchr][smartchr] the `.` key can cycle `->` / `.` / `. ` in code, insert a literal `.` inside strings, and prefer `. ` right after `__DIR__` (which then flows straight into `php-complete-complete-path`): + +```elisp +(defun my-php-smartchr-dot (code within-string next-to-string) + "Build a smartchr for the `.' key using `php-dot-context'." + (let ((select (lambda () + (pcase (php-dot-context) + ('string-or-comment within-string) + ('next-to-string next-to-string) + (_ code))))) + (smartchr-make-struct + :cleanup-fn (lambda () (delete-char (- (length (funcall select))))) + :insert-fn (lambda () (insert (funcall select)))))) + +;; (smartchr (my-php-smartchr-dot "->" "." ". ") +;; (my-php-smartchr-dot ". " ".." "..") +;; "...") +``` + ## Editing files that mix HTML and PHP `php-mode` is designed for pure PHP scripts. Files that embed PHP inside HTML, such as templates, are better edited in a major mode that understands both languages. Indentation in particular is unreliable when the HTML part of a file is edited in plain `php-mode`. @@ -109,6 +173,12 @@ Set `php-project-php-file-as-template` per project in `.dir-locals.el`: If you are already in `php-mode` and indent a file that contains HTML tags, PHP Mode warns you and offers to switch to `php-html-template-major-mode`. Set `php-mode-warn-if-html-template` to `nil` to turn off this prompt. +## Legacy CC Mode implementation (php-cc-mode) + +Until now, PHP Mode was implemented on top of Emacs' built-in CC Mode. That implementation is preserved as `php-cc-mode` and is in frozen maintenance: only regression fixes are accepted, and new development happens in the CC Mode independent `php-mode`. To open a file with the legacy implementation, run `M-x php-cc-mode` or add an entry to `auto-mode-alist` yourself. + +So that existing configurations keep working, `php-cc-mode` runs `php-mode-hook` in addition to its own `php-cc-mode-hook`. Settings described in [the CC Mode manual][cc-mode-manual] apply only to `php-cc-mode`; see the commentary at the top of `lisp/php-cc-mode.el` for details. + ## Reporting Bugs When reporting a bug, please run `M-x php-mode-debug` and include its output in your bug report. This helps us reproduce any issues you may be experiencing. @@ -121,7 +191,7 @@ Please see [CONTRIBUTING.md](CONTRIBUTING.md#english). PHP Mode is licensed under [GNU General Public License Version 3][gpl-v3] (GPLv3). -This project originated in `php-mode.el` written by [Turadg Aleahmad][@turadg] in 1999. In 2013 [Daniel Hackney][@haxney] began rewriting parts of PHP Mode in terms of Emacs' built-in CC Mode. Other contributors are listed in [Authors] and [Contributors]. +This project originated in `php-mode.el` written by [Turadg Aleahmad][@turadg] in 1999. In 2013 [Daniel Hackney][@haxney] began rewriting parts of PHP Mode in terms of Emacs' built-in CC Mode. In 2026 the mode was rewritten again to remove the CC Mode dependency. Other contributors are listed in [Authors] and [Contributors]. This project was maintained by [Eric James Michael Ritz][@ejmr] until 2017. Currently, the [Friends of Emacs-PHP Development][@emacs-php] community inherits PHP Mode. @@ -152,7 +222,12 @@ This project was maintained by [Eric James Michael Ritz][@ejmr] until 2017. Curr [Authors]: https://github.com/emacs-php/php-mode/wiki/Authors [Contributors]: https://github.com/emacs-php/php-mode/graphs/contributors [Supported Version]: https://github.com/emacs-php/php-mode/wiki/Supported-Version +[cape]: https://github.com/minad/cape +[cc-mode-manual]: https://www.gnu.org/software/emacs/manual/html_mono/ccmode.html [gpl-v3]: https://www.gnu.org/licenses/gpl-3.0 +[smartchr]: https://github.com/imakado/emacs-smartchr +[per-cs]: https://www.php-fig.org/per/coding-style/ +[#811]: https://github.com/emacs-php/php-mode/issues/811 [nongnu-elpa-badge]: https://elpa.nongnu.org/nongnu/php-mode.svg [nongnu-elpa]: https://elpa.nongnu.org/nongnu/php-mode.html [melpa-badge]: http://melpa.org/packages/php-mode-badge.svg diff --git a/lisp/php-cc-mode.el b/lisp/php-cc-mode.el new file mode 100644 index 00000000..dbf083a0 --- /dev/null +++ b/lisp/php-cc-mode.el @@ -0,0 +1,1620 @@ +;;; php-cc-mode.el --- CC Mode based major mode for editing PHP code -*- lexical-binding: t; -*- + +;; Copyright (C) 2024 Friends of Emacs-PHP development +;; Copyright (C) 1999, 2000, 2001, 2003, 2004 Turadg Aleahmad +;; 2008 Aaron S. Hawley +;; 2011, 2012, 2013, 2014, 2015, 2016, 2017 Eric James Michael Ritz + +;; Author: Eric James Michael Ritz +;; Maintainer: USAMI Kenta +;; URL: https://github.com/emacs-php/php-mode +;; Keywords: languages php +;; Version: 1.26.1 +;; Package-Requires: ((emacs "28.1")) +;; License: GPL-3.0-or-later + +(eval-and-compile + (make-obsolete-variable + (defconst php-mode-version-number "1.26.1" "PHP Mode version number.") + "Please call (php-mode-version :as-number t) for compatibility." "1.24.2")) + +;; 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: + +;; `php-cc-mode' is the legacy CC Mode based implementation of PHP Mode. +;; It is kept for backward compatibility and is in frozen maintenance: +;; only regression fixes are accepted here, new development happens in the +;; cc-mode independent `php-mode' (lisp/php-mode.el). +;; +;; PHP Mode is a major mode for editing PHP script. It's an extension +;; of CC mode; thus it inherits all C mode's navigation functionality. +;; But it colors according to the PHP syntax and indents according to the +;; PSR-2 coding guidelines. It also includes a couple handy IDE-type +;; features such as documentation search and a source and class browser. +;; +;; For backward compatibility this mode runs `php-mode-hook' in addition +;; to `php-cc-mode-hook' at the end of its initialization. + +;; Please read the manual for setting items compatible with CC Mode. +;; https://www.gnu.org/software/emacs/manual/html_mono/ccmode.html + +;; This mode is designed for PHP scripts consisting of a single ") + (prefix "\\" "::"))) + +(c-lang-defconst c-operators + php-cc `((prefix "new" "clone") + ,@(c-lang-const c-identifier-ops) + (postfix "->") + (postfix "++" "--" "[" "]" "(" ")") + (right-assoc "**") + (prefix "++" "--" "+" "-" "~" "(" ")" "@") + (prefix "instanceof") + (prefix "!") + (left-assoc "*" "/" "%") + (left-assoc "+" "-" ".") + (left-assoc "<<" ">>") + (left-assoc "<" ">" "<=" ">=") + (left-assoc "==" "!=" "===" "!==" "<>" "<=>") + (left-assoc "&") + (left-assoc "^") + (left-assoc "|") + (left-assoc "&&") + (left-assoc "||") + (right-assoc "??") + (left-assoc "?:") + (right-assoc-sequence "?" ":") + (right-assoc ,@(c-lang-const c-assignment-operators)) + (left-assoc "and") + (left-assoc "xor") + (left-assoc "or") + (left-assoc ",") + (left-assoc "=>"))) + +;; Allow '\' when scanning from open brace back to defining +;; construct like class +(c-lang-defconst c-block-prefix-disallowed-chars + php-cc (cl-set-difference (c-lang-const c-block-prefix-disallowed-chars) + '(?\\))) + +;; Allow $ so variables are recognized in cc-mode and remove @. This +;; makes cc-mode highlight variables and their type hints in arglists. +(c-lang-defconst c-symbol-start + php-cc (concat "[" c-alpha "_$]")) + +;; All string literals can possibly span multiple lines +(c-lang-defconst c-multiline-string-start-char + php-cc t) + +(c-lang-defconst c-assignment-operators + php-cc '("=" "*=" "/=" "%=" "+=" "-=" ">>=" "<<=" "&=" "^=" "|=" ".=" "??=")) + +(c-lang-defconst beginning-of-defun-function + php-cc 'php-cc-beginning-of-defun) + +(c-lang-defconst end-of-defun-function + php-cc 'php-cc-end-of-defun) + +(c-lang-defconst c-primitive-type-kwds + php-cc '("int" "integer" "bool" "boolean" "float" "double" "real" + "string" "object" "void" "mixed" "never")) + +(c-lang-defconst c-class-decl-kwds + "Keywords introducing declarations where the following block (if any) +contains another declaration level that should be considered a class." + php-cc '("class" "trait" "interface" "enum")) + +(c-lang-defconst c-brace-list-decl-kwds + "Keywords introducing declarations where the following block (if +any) is a brace list. + +PHP does not have an C-like \"enum\" keyword." + php-cc nil) + +;; cc-mode renamed `c-after-brace-list-decl-kwds' to `c-after-enum-list-kwds'; +;; keep the old name defined so cc-langs' transitional lookup stays quiet where +;; the rename landed (see `c-after-enum-list-key' in cc-langs.el). +(c-lang-defconst c-after-brace-list-decl-kwds + php-cc nil) + +(c-lang-defconst c-typeless-decl-kwds + php-cc (append (c-lang-const c-class-decl-kwds php-cc) '("function" "const"))) + +(c-lang-defconst c-modifier-kwds + php-cc '("abstract" "final" "static" "case" "readonly")) + +(c-lang-defconst c-protection-kwds + "Access protection label keywords in classes." + php-cc '("private" "protected" "public")) + +(c-lang-defconst c-postfix-decl-spec-kwds + php-cc '("implements" "extends")) + +(c-lang-defconst c-type-list-kwds + php-cc '("@new" ;; @new is *NOT* language construct, it's workaround for coloring. + "new" "use" "implements" "extends" "namespace" "instanceof" "insteadof")) + +(c-lang-defconst c-ref-list-kwds + php-cc nil) + +(c-lang-defconst c-block-stmt-2-kwds + php-cc '("catch" "declare" "elseif" "for" "foreach" "if" "switch" "while")) + +(c-lang-defconst c-simple-stmt-kwds + php-cc '("break" "continue" "die" "echo" "exit" "goto" "return" "throw" + "include" "include_once" "print" "require" "require_once")) + +(c-lang-defconst c-constant-kwds + php-cc '("true" "false" "null")) + +(c-lang-defconst c-lambda-kwds + php-cc '("function" "use")) + +(c-lang-defconst c-inexpr-block-kwds + php-cc '("match")) + +(c-lang-defconst c-other-block-decl-kwds + php-cc '("namespace")) + +(c-lang-defconst c-other-kwds + "Keywords not accounted for by any other `*-kwds' language constant." + php-cc + '("__halt_compiler" + "and" + "array" + "as" + "break" + "catch" + "clone" + "default" + "empty" + "enddeclare" + "endfor" + "endforeach" + "endif" + "endswitch" + "endwhile" + "eval" + "fn" ;; NOT c-lambda-kwds + "global" + "isset" + "list" + "or" + "parent" + "static" + "unset" + "var" + "xor" + "yield" + "yield from" + + ;; Below keywords are technically not reserved keywords, but + ;; threated no differently by php-mode from actual reserved + ;; keywords + ;; + ;;; declare directives: + "encoding" + "ticks" + "strict_types" + + ;;; self for static references: + "self")) + +;; PHP does not have <> templates/generics +(c-lang-defconst c-recognize-<>-arglists + php-cc nil) + +(c-lang-defconst c-<>-type-kwds + php-cc nil) + +(c-lang-defconst c-inside-<>-type-kwds + php-cc nil) + +(c-lang-defconst c-enums-contain-decls + php-cc nil) + +(c-lang-defconst c-nonlabel-token-key + "Regexp matching things that can't occur in generic colon labels. + +This overrides cc-mode `c-nonlabel-token-key' to support switching on +double quoted strings and true/false/null. + +Note: this regexp is also applied to goto-labels, a future improvement +might be to handle switch and goto labels differently." + php-cc (concat + ;; All keywords except `c-label-kwds' and `c-constant-kwds'. + (c-make-keywords-re t + (cl-set-difference (c-lang-const c-keywords) + (append (c-lang-const c-label-kwds) + (c-lang-const c-constant-kwds)) + :test 'string-equal)))) + +(c-lang-defconst c-basic-matchers-before + php-cc (cl-remove-if (lambda (elm) (and (listp elm) (equal (car elm) "\\s|"))) + (c-lang-const c-basic-matchers-before php-cc))) + +(c-lang-defconst c-basic-matchers-after + php-cc (cl-remove-if (lambda (elm) (and (listp elm) (memq 'c-annotation-face elm))) + (c-lang-const c-basic-matchers-after php-cc))) + +(defconst php-mode--re-return-typed-closure + (eval-when-compile + (rx symbol-start "function" symbol-end + (* (syntax whitespace)) + "(" (* (not (any "("))) ")" + (* (syntax whitespace)) + (? symbol-start "use" symbol-end + (* (syntax whitespace)) + "(" (* (not (any "("))) ")" + (* (syntax whitespace))) + ":" (+ (not (any "{}"))) + (group "{")))) + +(defun php-c-lineup-arglist (langelem) + "Line up the current argument line under the first argument using +`c-lineup-arglist' LANGELEM." + (let (in-return-typed-closure) + (when (and (consp langelem) + (eq 'arglist-cont-nonempty (car langelem))) + (save-excursion + (save-match-data + (when (re-search-backward php-mode--re-return-typed-closure (cdr langelem) t) + (goto-char (match-beginning 1)) + (when (not (php-in-string-or-comment-p)) + (setq in-return-typed-closure t)))))) + (unless in-return-typed-closure + (c-lineup-arglist langelem)))) + +(defun php-lineup-cascaded-calls (langelem) + "Line up chained methods using `c-lineup-cascaded-calls', +but only if the setting is enabled." + (cond + (php-cc-mode-lineup-cascaded-calls (c-lineup-cascaded-calls langelem)) + ((assq 'arglist-cont-nonempty c-syntactic-context) nil) + ((assq 'defun-block-intro c-syntactic-context) nil) + ((assq 'defun-close c-syntactic-context) nil) + ((assq 'statement-cont c-syntactic-context) nil) + ((save-excursion + (beginning-of-line) + (let ((beginning-of-langelem (cdr langelem)) + (beginning-of-current-line (point)) + start) + (skip-chars-forward " ") + (cond + ((looking-at-p "->") '+) + ((looking-at-p "[:?]") '+) + ((looking-at-p "[,;]") nil) + ((looking-at-p "//") nil) + ;; Is the previous line terminated with `,' ? + ((progn + (forward-line -1) + (end-of-line) + (skip-chars-backward " ") + (backward-char 1) + (while (and (< beginning-of-langelem (point)) + (setq start (php-in-string-or-comment-p))) + (goto-char start) + (skip-chars-backward " \r\n") + (backward-char 1)) + (and (not (eq (point) beginning-of-current-line)) + (not (php-in-string-or-comment-p)) + (not (looking-at-p ",")) + (save-excursion + (backward-char) (not (looking-at-p ","))))) + '+) + (t nil))))))) + +(defun php-c-looking-at-or-maybe-in-bracelist (orig-fun &optional containing-sexp lim &rest args) + "Replace `c-looking-at-or-maybe-in-bracelist'. + +CONTAINING-SEXP is the position of the brace/paren/bracket enclosing +POINT, or nil if there is no such position, or we do not know it. LIM is +a backward search limit." + (cond + ((not (derived-mode-p 'php-cc-mode)) (apply orig-fun containing-sexp lim args)) + ((looking-at-p "{") + (save-excursion + (c-backward-token-2 2 t lim) + ;; PHP 8.0 match expression + ;; echo match ($var) |{ + ;; ↑ matches ↑ initial position + (when (looking-at-p (eval-when-compile (rx symbol-start "match" symbol-end))) + (cons (point) t)))) + (t nil))) + +(defun php-mode--anonymous-function-brace-p (pos) + "Return non-nil when the opening brace at POS starts a closure body. + +POS must be the position of a `{'. A closure is an anonymous +`function (...) [use (...)] [: type] {' expression; named function +declarations and other constructs (e.g. `match', `if') return nil." + (save-excursion + (goto-char pos) + (c-backward-syntactic-ws) + ;; Step back over an optional return type declaration `): Type {' so + ;; that point lands just after the parameter (or `use') list's `)'. + (unless (eq (char-before) ?\)) + (let ((colon (save-excursion + (skip-chars-backward "^):;{}(" (point-min)) + (and (eq (char-before) ?:) (1- (point)))))) + (when colon + (goto-char colon) + (c-backward-syntactic-ws)))) + ;; Walk backwards over the `)...(' list(s): the parameter list, plus an + ;; optional `use (...)' clause, until we reach `function' or give up. + (let ((result nil) (continue t)) + (while (and continue (eq (char-before) ?\))) + (condition-case nil + (backward-list) + (error (setq continue nil))) + (when continue + (c-backward-syntactic-ws) + (if (zerop (c-backward-token-2)) + (cond + ((looking-at-p "\\_") (setq result t continue nil)) + ((looking-at-p "\\_") (c-backward-syntactic-ws)) + (t (setq continue nil))) + (setq continue nil)))) + result))) + +(defun php-c-looking-at-statement-block (orig-fun &rest args) + "Treat a closure body as a statement block around `c-looking-at-statement-block'. + +Emacs' CC Mode >= 31 (the change for bug#19867) fails to recognize an +anonymous function body whose first line is a comment as a statement +block, and reports it as a brace list, mis-indenting both the body and +its closing brace. When point is at the opening brace of such a closure, +return non-nil; otherwise fall back to ORIG-FUN with ARGS. + +A closure body is always a statement block, so this is also correct on +Emacs versions unaffected by the bug." + (or (and (derived-mode-p 'php-cc-mode) + (eq (char-after) ?\{) + (php-mode--anonymous-function-brace-p (point))) + (apply orig-fun args))) + +(c-add-style + "php" + `((c-basic-offset . 4) + (c-offsets-alist . ((arglist-close . php-lineup-arglist-close) + (arglist-cont . (first php-lineup-cascaded-calls 0)) + (arglist-cont-nonempty . (first php-lineup-cascaded-calls php-c-lineup-arglist)) + (arglist-intro . php-lineup-arglist-intro) + (case-label . +) + (class-open . 0) + (comment-intro . 0) + (inexpr-class . 0) + (inlambda . 0) + (inline-open . 0) + (namespace-open . 0) + (lambda-intro-cont . +) + (label . +) + (statement-cont . (first php-lineup-cascaded-calls php-lineup-string-cont +)) + (substatement-open . 0) + (topmost-intro-cont . (first php-lineup-cascaded-calls +)))) + (indent-tabs-mode . nil) + (tab-width . ,(default-value 'tab-width)) + (fill-column . ,(default-value 'fill-column)) + (show-trailing-whitespace . ,(default-value 'show-trailing-whitespace)) + (php-cc-mode-lineup-cascaded-calls . t) + (php-style-delete-trailing-whitespace . nil))) + +(defun php-cc-enable-default-coding-style () + "Set PHP Mode to use reasonable default formatting." + (interactive) + (php-cc-set-style "php")) + +;; The shared `php-mode-coding-style' now defaults to `per'. Register a +;; "per" cc-style (an alias of the base "php" style) so that `php-cc-mode' +;; keeps initializing when that default is in effect. +(c-add-style "per" '("php")) + +(defun php-cc-enable-per-coding-style () + "Set PHP Mode to use the PHP-FIG PER coding style." + (interactive) + (php-cc-set-style "per")) + +(c-add-style + "pear" + '("php" + (c-basic-offset . 4) + (c-offsets-alist . ((case-label . 0) (statement-cont . +))) + (tab-width . 4) + (php-cc-mode-lineup-cascaded-calls . nil))) + +(defun php-cc-enable-pear-coding-style () + "Set up `php-mode' to use the coding styles preferred for PEAR code and modules." + (interactive) + (php-cc-set-style "pear")) + +(c-add-style + "drupal" + '("php" + (c-basic-offset . 2) + (tab-width . 2) + (fill-column . 78) + (show-trailing-whitespace . t) + (php-cc-mode-lineup-cascaded-calls . nil) + (php-style-delete-trailing-whitespace . t))) + +(defun php-cc-enable-drupal-coding-style () + "Make `php-mode' use coding styles that are preferable for working with Drupal." + (interactive) + (php-cc-set-style "drupal")) + +(c-add-style + "wordpress" + '("php" + (c-basic-offset . 4) + (c-indent-comments-syntactically-p t) + (indent-tabs-mode . t) + (tab-width . 4) + (fill-column . 78))) + +(defun php-cc-enable-wordpress-coding-style () + "Make `php-mode' use coding styles that are preferable for working with +Wordpress." + (interactive) + (php-cc-set-style "wordpress")) + +(c-add-style + "symfony2" + '("php" + (c-offsets-alist . ((statement-cont . php-lineup-hanging-semicolon))) + (c-indent-comments-syntactically-p . t) + (php-cc-mode-lineup-cascaded-calls . nil) + (fill-column . 78))) + +(defun php-cc-enable-symfony2-coding-style () + "Make `php-mode' use coding styles that are preferable for working with Symfony2." + (interactive) + (php-cc-set-style "symfony2")) + +(c-add-style + "psr2" ; PSR-2 / PSR-12 + '("php" + (c-offsets-alist . ((statement-cont . +))) + (c-indent-comments-syntactically-p . t) + (fill-column . 78) + (show-trailing-whitespace . t) + (php-cc-mode-lineup-cascaded-calls . nil) + (php-style-delete-trailing-whitespace . t))) + +(defun php-cc-enable-psr2-coding-style () + "Make `php-mode' comply to the PSR-2 coding style." + (interactive) + (php-cc-set-style "psr2")) + +(defun php-cc-beginning-of-defun (&optional arg) + "Move to the beginning of the ARGth PHP function from point. +Implements PHP version of `beginning-of-defun-function'." + (interactive "p") + (let (found-p (arg (or arg 1))) + (while (> arg 0) + (setq found-p (re-search-backward php-beginning-of-defun-regexp + nil 'noerror)) + (setq arg (1- arg))) + (while (< arg 0) + (end-of-line 1) + (let ((opoint (point))) + (beginning-of-defun 1) + (forward-list 2) + (forward-line 1) + (if (eq opoint (point)) + (setq found-p (re-search-forward php-beginning-of-defun-regexp + nil 'noerror))) + (setq arg (1+ arg)))) + (not (null found-p)))) + +(defun php-cc-end-of-defun (&optional arg) + "Move the end of the ARGth PHP function from point. +Implements PHP version of `end-of-defun-function' + +See `php-cc-beginning-of-defun'." + (interactive "p") + (php-cc-beginning-of-defun (- (or arg 1)))) + + +(defvar-local php-warned-bad-indent nil + "Non-nil once the user has been warned about indenting this buffer. +Buffer-local so a warning in one buffer does not suppress it in others.") + +(defun php-check-html-for-indentation () + "Return non-nil when the current buffer may be indented as PHP. +Warn and offer to switch to `php-html-template-major-mode' when the +buffer looks like an HTML template edited in plain `php-mode'." + (cond + ((not php-mode-warn-if-html-template) t) + ;; In a polymode buffer php-mode only ever indents its own PHP chunks, + ;; so indent normally and never warn about the surrounding HTML. + ((php-in-poly-php-html-mode) t) + ((not (php-buffer-has-html-tag)) t) + (php-warned-bad-indent nil) + ((fboundp php-html-template-major-mode) + (if (y-or-n-p (format "This file seems to contain an HTML tag. Switch to %s? " + php-html-template-major-mode)) + (funcall php-html-template-major-mode) + (prog1 nil + (setq-local php-warned-bad-indent t)))) + (t ;; Suppress warnings in Emacs session + (setq-local php-warned-bad-indent t) + (lwarn 'php-mode + :warning "Indentation fails badly with mixed HTML/PHP in the HTML part in plain `php-mode'. +It is highly recommended to install a major mode that supports PHP and HTML templates, such as Web Mode. + +Set `php-html-template-major-mode' variable to use a mode other than `web-mode'. +Set `php-mode-warn-if-html-template' variable to nil to suppress the warning. +") + nil))) + +(defun php-cautious-indent-region (start end &optional quiet) + "Carefully indent region START to END in contexts other than HTML templates. + +If the optional argument QUIET is non-nil then no syntactic errors are +reported, even if `c-report-syntactic-errors' is non-nil." + (when (php-check-html-for-indentation) + (c-indent-region start end quiet))) + +(defun php-cautious-indent-line () + "Carefully indent lines in contexts other than HTML templates." + (when (and (php-check-html-for-indentation) + (save-excursion + (beginning-of-line) + ;; Don't indent heredoc end mark + (not (and (looking-at-p "[a-zA-Z0-9_]+;\n") (php-in-string-p))))) + (c-indent-line))) + +(defun php-c-at-vsemi-p (&optional pos) + "Return T on HTML lines (including php tag) or PHP8 Attribute, otherwise NIL. +POS is a position on the line in question. + +This is was done due to the problem reported here: + + URL `https://answers.launchpad.net/nxhtml/+question/43320'" + ;; If this function could call c-beginning-of-statement-1, change php-c-vsemi-status-unknown-p. + (save-excursion + (if pos + (goto-char pos) + (setq pos (point))) + (cond + ;; Detect PHP8 attribute: #[Attribute()] + ((and (< 1 pos) (< 1 (- pos (c-point 'bol)))) + (backward-char 1) + (looking-at-p (eval-when-compile (rx "]" (* (syntax whitespace)) (or "#[" line-end))))) + ;; Detect HTML/XML tag and PHP tag () + (php-mode-template-compatibility + (beginning-of-line) + (looking-at-p + (eval-when-compile + (rx (or (: bol (0+ space) "<" (in "?a-z")) + (: (0+ not-newline) (in "?a-z") ">" (0+ space) eol))))))))) + +(defun php-c-vsemi-status-unknown-p () + "Always return NIL. See `c-vsemi-status-unknown-p'." + ;; Current implementation of php-c-at-vsemi-p never calls c-beginning-of-statement-1 + nil) + +(defun php-lineup-string-cont (langelem) + "Line up string toward equal sign or dot. +e.g. +$str = \\='some' + . \\='string'; +this ^ lineup" + (save-excursion + (goto-char (cdr langelem)) + (let (ret finish) + (while (and (not finish) (re-search-forward "[=.]" (line-end-position) t)) + (unless (php-in-string-or-comment-p) + (setq finish t + ret (vector (1- (current-column)))))) + ret))) + +(defun php-lineup-arglist-intro (langelem) + (save-excursion + (goto-char (cdr langelem)) + (vector (+ (current-column) c-basic-offset)))) + +(defun php-lineup-arglist-close (langelem) + (save-excursion + (goto-char (cdr langelem)) + (vector (current-column)))) + +(defun php-lineup-arglist (_langelem) + (save-excursion + (beginning-of-line) + (if (looking-at-p "\\s-*->") '+ 0))) + +(defun php-lineup-hanging-semicolon (_langelem) + (save-excursion + (beginning-of-line) + (if (looking-at-p "\\s-*;\\s-*$") 0 '+))) + +(eval-and-compile + (defconst php-cc-heredoc-start-re + (rx "<<<" + (* (syntax whitespace)) + (or (group (+ (or (syntax word) (syntax symbol)))) + (: "\"" (group (+ (or (syntax word) (syntax symbol)))) "\"") + (: "'" (group (+ (or (syntax word) (syntax symbol)))) "'")) + line-end) + "Regular expression for the start of a PHP heredoc.")) + +(defun php-cc-heredoc-end-re (heredoc-start) + "Build a regular expression for the end of a heredoc started by the string +HEREDOC-START." + ;; Extract just the identifier without <<< and quotes. + (string-match "\\_<.+?\\_>" heredoc-start) + (concat "^\\s-*\\(" (match-string 0 heredoc-start) "\\)\\W")) + +(eval-and-compile + (defconst php-cc-syntax-propertize-rules + (syntax-propertize-precompile-rules + (php-cc-heredoc-start-re + (0 (ignore (php-cc--syntax-propertize-heredoc + (match-beginning 0) + (or (match-string 1) (match-string 2) (match-string 3)) + (null (match-string 3)))))) + ((rx "#[") + (0 (ignore (php-cc--syntax-propertize-attributes (match-beginning 0))))) + ((rx (or "'" "\"")) + (0 (ignore (php-cc--syntax-propertize-quotes-in-comment (match-beginning 0)))))))) + +(defalias 'php-cc-syntax-propertize-function + (syntax-propertize-rules php-cc-syntax-propertize-rules)) + +(defun php-cc--syntax-propertize-heredoc (start id _is-heredoc) + "Apply propertize Heredoc and Nowdoc from START, with ID and IS-HEREDOC." + (let ((terminator (rx-to-string `(: line-start (* (syntax whitespace)) ,id word-boundary)))) + (put-text-property start (1+ start) 'syntax-table (string-to-syntax "|")) + (re-search-forward terminator nil t) + (when (match-string 0) + (put-text-property (1- (point)) (point) 'syntax-table (string-to-syntax "|"))))) + +(defun php-cc--syntax-propertize-quotes-in-comment (pos) + "Apply propertize quotes (' and \") from POS." + (when (php-in-comment-p) + (put-text-property pos (1+ pos) 'syntax-table (string-to-syntax "_")))) + +(defun php-cc--syntax-propertize-attributes (start) + "Apply propertize PHP8 #[Attributes] (without # comment) from START." + (unless (php-in-string-p) + (put-text-property start (1+ start) 'syntax-table (string-to-syntax ".")))) + +(defvar-local php-cc-mode--propertize-extend-region-current nil + "Prevent undesirable recursion in PHP-SYNTAX-PROPERTIZE-EXTEND-REGION.") + +(defun php-cc-syntax-propertize-extend-region (start end) + "Extend the propertize region if START or END falls inside a PHP heredoc." + (let ((pair (cons start end))) + (when (not (member pair php-cc-mode--propertize-extend-region-current)) + ;; re-search functions may trigger + ;; syntax-propertize-extend-region-functions to be called again, which in + ;; turn call this to be called again. + (push pair php-cc-mode--propertize-extend-region-current) + (unwind-protect + (let (new-start new-end) + (goto-char start) + ;; Consider bounding this backwards search by `beginning-of-defun'. + ;; (Benchmarking for a wide range of cases may be needed to decide + ;; whether that's an improvement, as `php-cc-beginning-of-defun' also + ;; uses `re-search-backward'.) + (when (re-search-backward php-cc-heredoc-start-re nil t) + (let ((maybe (point))) + (when (and (re-search-forward (php-cc-heredoc-end-re (match-string 0)) nil t) + (> (point) start)) + (setq new-start maybe) + (when (> (point) end) + (setq new-end (point)))))) + (unless new-end + (goto-char end) + (when (re-search-backward php-cc-heredoc-start-re start t) + (if (re-search-forward (php-cc-heredoc-end-re (match-string 0)) nil t) + (when (> (point) end) + (setq new-end (point))) + (setq new-end (point-max))))) + (when (or new-start new-end) + (cons (or new-start start) (or new-end end)))) + ;; Cleanup + (setq php-cc-mode--propertize-extend-region-current + (delete pair php-cc-mode--propertize-extend-region-current)))))) + +(easy-menu-define php-mode-menu php-cc-mode-map "PHP Mode Commands." + (cons "PHP" (c-lang-const c-mode-menu php-cc))) + +(defun php-mode-get-style-alist () + "Return an alist consisting of `php' style and styles that inherit it." + (cl-loop for l in c-style-alist + if (or (string= (car l) "php") + (equal (cadr l) "php")) + collect l)) + +(defvar php-mode-set-style-history nil) +(defvar-local php-mode--delayed-set-style nil) +(defvar-local php-style-delete-trailing-whitespace nil) + +(defun php-cc-set-style (stylename &optional dont-override) + "Set the current `php-mode' buffer to use the style STYLENAME. +STYLENAME is one of the names selectable in `php-mode-coding-style'. + +Borrow the `interactive-form' from `c-set-style' and use the original +`c-set-style' function to set all declared stylevars. +For compatibility with `c-set-style' pass DONT-OVERRIDE to it. + +After setting the stylevars run hook `php-mode-STYLENAME-hook'." + (interactive + (list (let ((completion-ignore-case t) + (prompt (format "Which %s indentation style? " + mode-name))) + (completing-read prompt + (php-mode-get-style-alist) + nil t nil + 'php-mode-set-style-history + c-indentation-style)))) + (php-mode--disable-delay-set-style) + + ;; Back up manually set variables + (let* (value + (backup-vars + (and php-cc-mode-enable-backup-style-variables + (cl-loop for name in c-style-variables + do (setq value (symbol-value name)) + if (and value (not (eq 'set-from-style value))) + collect (cons name value))))) + (c-set-style stylename dont-override) + ;; Restore variables + (cl-loop for (name . value) in backup-vars + do (set (make-local-variable name) value))) + (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)) + (run-hooks (intern (format "php-mode-%s-hook" stylename)))) + +(defun php-mode--disable-delay-set-style (&rest _args) + "Disable `php-mode-set-style-delay' on after hook. ARGS be ignore." + (setq php-mode--delayed-set-style nil) + (advice-remove #'php-mode--disable-delay-set-style #'c-set-style)) + +(defun php-mode-set-style-delay () + "Set the current `php-mode' buffer to use the style by custom or local variables." + (when php-mode--delayed-set-style + (let ((coding-style (or (and (boundp 'php-project-coding-style) php-project-coding-style) + php-mode-coding-style))) + (prog1 (when coding-style + (php-cc-set-style (symbol-name coding-style))) + (remove-hook 'hack-local-variables-hook #'php-mode-set-style-delay))))) + +(defun php-mode-set-local-variable-delay () + "Set local variable from php-project." + (php-project-apply-local-variables) + (remove-hook 'hack-local-variables-hook #'php-mode-set-local-variable-delay)) + +(defvar php-cc-mode-syntax-table + (let ((table (make-syntax-table))) + (c-populate-syntax-table table) + (modify-syntax-entry ?_ "_" table) + (modify-syntax-entry ?` "\"" table) + (modify-syntax-entry ?\" "\"" table) + (modify-syntax-entry ?# "< b" table) + (modify-syntax-entry ?\n "> b" table) + (modify-syntax-entry ?$ "_" table) + table)) + +;;;###autoload +(define-derived-mode php-cc-mode php-base-mode "PHP/cc" + "CC Mode based major mode for editing PHP code. + +This is the legacy implementation kept for backward compatibility. + +\\{php-cc-mode-map}" + :syntax-table php-cc-mode-syntax-table + :after-hook (progn (c-make-noise-macro-regexps) + (c-make-macro-with-semi-re) + (c-update-modeline)) + (unless (string= php-mode-cc-version c-version) + (php-mode-debug-reinstall nil)) + + (c-initialize-cc-mode t) + (setq abbrev-mode t) + + ;; Must be called once as c-mode to enable font-lock for Heredoc. + ;; TODO: This call may be removed in the future. + (c-common-init 'c-mode) + + (c-init-language-vars php-cc-mode) + (c-common-init 'php-cc-mode) + (cc-imenu-init cc-imenu-c-generic-expression) + + (setq-local c-auto-align-backslashes nil) + + (setq-local comment-start "// ") + (setq-local comment-start-skip + (eval-when-compile + (rx (group (or (: "#" (not (any "["))) + (: "/" (+ "/")) + (: "/*"))) + (* (syntax whitespace))))) + (setq-local comment-end "") + (setq-local page-delimiter php-mode-page-delimiter) + + (with-suppressed-warnings ((obsolete font-lock-string-face + font-lock-keyword-face + font-lock-builtin-face + font-lock-function-name-face + font-lock-variable-name-face + font-lock-constant-face)) + (setq-local font-lock-string-face 'php-string) + (setq-local font-lock-keyword-face 'php-keyword) + (setq-local font-lock-builtin-face 'php-builtin) + (setq-local c-preprocessor-face-name 'php-php-tag) + (setq-local font-lock-function-name-face 'php-function-name) + (setq-local font-lock-variable-name-face 'php-variable-name) + (setq-local font-lock-constant-face 'php-constant)) + + (setq-local syntax-propertize-function #'php-cc-syntax-propertize-function) + (add-hook 'syntax-propertize-extend-region-functions + #'php-cc-syntax-propertize-extend-region t t) + + (setq imenu-generic-expression (if (symbolp php-imenu-generic-expression) + (symbol-value php-imenu-generic-expression) + php-imenu-generic-expression)) + + ;; PHP vars are case-sensitive + (setq case-fold-search t) + + (when php-mode-enable-project-local-variable + (add-hook 'hack-local-variables-hook #'php-mode-set-local-variable-delay t t)) + + ;; When php-mode-enable-project-coding-style is set, it is delayed by hook. + ;; Since it depends on the timing at which the file local variable is set. + ;; File local variables are set after initialization of major mode except `run-hook' is complete. + (if php-mode-enable-project-coding-style + (progn + (add-hook 'hack-local-variables-hook #'php-mode-set-style-delay t t) + (setq php-mode--delayed-set-style t) + (advice-add 'c-set-style :after #'php-mode--disable-delay-set-style)) + (let ((php-cc-mode-enable-backup-style-variables nil)) + (php-cc-set-style (symbol-name php-mode-coding-style)))) + + (when (or php-mode-force-pear + (and (stringp buffer-file-name) + (string-match "PEAR\\|pear" buffer-file-name) + (string-match "\\.php\\'" buffer-file-name))) + (php-cc-set-style "pear")) + + (setq indent-line-function #'php-cautious-indent-line) + (setq indent-region-function #'php-cautious-indent-region) + (setq c-at-vsemi-p-fn #'php-c-at-vsemi-p) + (setq c-vsemi-status-unknown-p-fn #'php-c-vsemi-status-unknown-p) + + ;; We map the php-{beginning,end}-of-defun functions so that they + ;; replace the similar commands that we inherit from CC Mode. + ;; Because of our remapping we may not actually need to keep the + ;; following two local variables, but we keep them for now until we + ;; are completely sure their removal will not break any current + ;; behavior or backwards compatibility. + (setq-local beginning-of-defun-function #'php-cc-beginning-of-defun) + (setq-local end-of-defun-function #'php-cc-end-of-defun) + + (setq-local open-paren-in-column-0-is-defun-start nil) + (setq-local defun-prompt-regexp + "^\\s-*function\\s-+&?\\s-*\\(\\(\\sw\\|\\s_\\)+\\)\\s-*") + (setq-local add-log-current-defun-function nil) + (setq-local add-log-current-defun-header-regexp php-beginning-of-defun-regexp) + + (when (and (eval-when-compile (boundp 'flymake-diagnostic-functions)) + php-mode-replace-flymake-diag-function) + (add-hook 'flymake-diagnostic-functions php-mode-replace-flymake-diag-function nil t)) + + (advice-add 'c-looking-at-or-maybe-in-bracelist + :around 'php-c-looking-at-or-maybe-in-bracelist) + (when (fboundp 'c-looking-at-statement-block) + (advice-add 'c-looking-at-statement-block + :around 'php-c-looking-at-statement-block)) + (advice-add 'fixup-whitespace :after #'php-mode--fixup-whitespace-after) + + (advice-add 'acm-backend-tabnine-candidate-expand + :filter-args #'php-acm-backend-tabnine-candidate-expand-filter-args) + + (when (eval-when-compile (>= emacs-major-version 25)) + (syntax-ppss-flush-cache (point-min))) + + ;; Backward compatibility: historically this mode was named `php-mode' + ;; and users hung their configuration on `php-mode-hook'. Run it here so + ;; existing setups keep working under the renamed `php-cc-mode'. + (run-hooks 'php-mode-hook)) + +(declare-function semantic-create-imenu-index "semantic/imenu" (&optional stream)) + +(defvar-mode-local php-cc-mode imenu-create-index-function + (if php-do-not-use-semantic-imenu + #'imenu-default-create-index-function + (require 'semantic/imenu) + #'semantic-create-imenu-index) + "Imenu index function for PHP.") + +(when (bound-and-true-p consult-imenu-config) + (add-to-list 'consult-imenu-config '(php-cc-mode :toplevel "Namespace" + :types ((?n "Namespace" font-lock-function-name-face) + (?p "Properties" font-lock-type-face) + (?o "Constants" font-lock-type-face) + (?c "Classes" font-lock-type-face) + (?f "Functions" font-lock-function-name-face) + (?i "Imports" font-lock-type-face) + (?m "Methods" font-lock-function-name-face))))) + +(autoload 'php-local-manual-complete-function "php-local-manual") + +(defun php-complete-function () + "Perform function completion on the text around point. +Completes to the set of names listed in the current tags table +and the standard php functions. +The string to complete is chosen in the same way as the default +for \\[find-tag] (which see)." + (interactive) + (php-local-manual-complete-function)) + +(defun php-show-arglist () + "Show function arguments at cursor position." + (interactive) + (let* ((tagname (php-get-pattern)) + (buf (find-tag-noselect tagname nil nil)) + arglist) + (with-current-buffer buf + (save-excursion + (goto-char (point-min)) + (when (re-search-forward + (format "function\\s-+%s\\s-*(\\([^{]*\\))" tagname) + nil t) + (setq arglist (buffer-substring-no-properties + (match-beginning 1) (match-end 1)))))) + (if arglist + (message "Arglist for %s: %s" tagname arglist) + (message "Unknown function: %s" tagname)))) + +;; Font Lock +(defconst php-cc-phpdoc-type-names + '(;; PHPStan and Psalm types + "__stringandstringable" "array" "array-key" "associative-array" "bool" "boolean" + "callable" "callable-array" "callable-object" "callable-string" "class-string" + "closed-resource" "double" "empty" "empty-scalar" "enum-string" "false" "float" + "int" "integer" "interface-string" "iterable" "list" "literal-string" "lowercase-string" + "mixed" "negative-int" "never" "never-return" "never-returns" "no-return" "non-empty-array" + "non-empty-list" "non-empty-literal-string" "non-empty-lowercase-string" "non-empty-mixed" + "non-empty-scalar" "non-empty-string" "non-empty-uppercase-string" "non-falsy-string" + "non-negative-int" "non-positive-int" "non-zero-int" "noreturn" "null" "number" "numeric" + "numeric-string" "object" "open-resource" "parent" "positive-int" "pure-callable" + "pure-closure" "resource" "scalar" "self" "static" "string" "trait-string" "true" + "truthy-string" "uppercase-string" "void" + ;; PHPStan Generic Types + "key-of" "value-of" "int-mask-of" "int-mask" "__benevolent" "template-type" "new") + "A list of type and pseudotype names that can be used in PHPDoc.") + +(make-obsolete-variable 'php-phpdoc-type-keywords 'php-cc-phpdoc-type-names "1.24.2") + +(defconst php-cc-phpdoc-type-tags + (list "package" "param" "property" "property-read" "property-write" + "return" "throws" "var" "self-out" "this-out" "param-out" + "type" "extends" "require-extends" "implemtents" "require-implements" + "template" "template-covariant" "template-extends" "template-implements" + "require-extends" "require-implements" + "assert" "assert-if-true" "assert-if-false" "if-this-is") + "A list of tags specifying type names.") + +(defconst php-cc-phpdoc-font-lock-doc-comments + `(("{@[-[:alpha:]]+\\s-*\\([^}]*\\)}" ; "{@foo ...}" markup. + (0 'php-doc-annotation-tag prepend nil) + (1 'php-string prepend nil)) + (,(rx (group "$") (group (in "A-Za-z_") (* (in "0-9A-Za-z_")))) + (1 'php-doc-variable-sigil prepend nil) + (2 'php-variable-name prepend nil)) + ("\\(\\$\\)\\(this\\)\\>" (1 'php-doc-$this-sigil prepend nil) (2 'php-doc-$this prepend nil)) + (,(concat "\\s-@" (rx (? (or "phan" "phpstan" "psalm") "-")) (regexp-opt php-cc-phpdoc-type-tags) "\\s-+" + "\\(" (rx (+ (? "?") (? "\\") (+ (in "0-9A-Z_a-z")) (? "[]") (? "|"))) "\\)+") + 1 'php-string prepend nil) + (,(concat "\\(?:|\\|\\?\\|\\s-\\)\\(" + (regexp-opt php-cc-phpdoc-type-names 'words) + "\\)") + 1 font-lock-type-face prepend nil) + ("^\\(?:/\\*\\)?\\(?:\\s \\|\\*\\)*\\(@[[:alpha:]][-[:alpha:]\\]*\\)" ; "@foo ..." markup. + 1 'php-doc-annotation-tag prepend nil))) + +(defvar php-cc-phpdoc-font-lock-keywords + `((,(lambda (limit) + (c-font-lock-doc-comments "/\\*\\*" limit + php-cc-phpdoc-font-lock-doc-comments))))) + +(defconst php-cc-font-lock-keywords-1 (c-lang-const c-matchers-1 php-cc) + "Basic highlighting for PHP Mode.") + +(defconst php-cc-font-lock-keywords-2 (c-lang-const c-matchers-2 php-cc) + "Medium level highlighting for PHP Mode.") + +(defconst php-cc-font-lock-keywords-3 + (append + php-cc-phpdoc-font-lock-keywords + ;; php-mode patterns *before* cc-mode: + ;; only add patterns here if you want to prevent cc-mode from applying + ;; a different face. + `( + ;; Class declaration specification keywords (implements, extends) + ("\\_<\\(?:implements\\|extends\\)\\_>" . 'php-class-declaration-spec) + ;; Namespace declaration + ("\\_" . 'php-namespace-declaration) + ;; import statement + ("\\_" . 'php-import-declaration) + ;; Class modifiers (abstract, final) + ("\\_<\\(abstract\\|final\\)\\_>\\s-+\\_" 1 'php-class-modifier) + + ;; Highlight variables, e.g. 'var' in '$var' and '$obj->var', but + ;; not in $obj->var() + ("\\(->\\)\\(\\sw+\\)\\s-*(" (1 'php-object-op) (2 php-method-call)) + ("\\<\\(const\\)\\s-+\\(\\_<.+?\\_>\\)" (1 'php-keyword) (2 'php-constant-assign)) + + ;; Logical operator (!) + ("\\(!\\)[^=]" 1 'php-logical-op) + + ;; Highlight special variables + ("\\(\\$\\)\\(this\\)\\>" (1 'php-this-sigil) (2 'php-this)) + ("\\(\\$+\\)\\(\\sw+\\)" (1 'php-variable-sigil) (2 'php-variable-name)) + ("\\(->\\)\\([a-zA-Z0-9_]+\\)" (1 'php-object-op) (2 'php-property-name)) + + ;; Highlight function/method names + ("\\" . 'php-class-declaration) + + ;; Highlight static method calls as such. This is necessary for method + ;; names which are identical to keywords to be highlighted correctly. + ("\\sw+::\\(\\sw+\\)(" 1 php-static-method-call) + ;; Multiple catch (FooException | BarException $e) + (,(rx symbol-start "catch" symbol-end + (* (syntax whitespace)) "(" (* (syntax whitespace)) + (group (+ (or (syntax word) (syntax symbol))))) + (1 font-lock-type-face) + (,(rx (* (syntax whitespace)) "|" (* (syntax whitespace)) + (group (+ (or (syntax word) (syntax symbol))) symbol-end)) + nil nil (1 font-lock-type-face))) + ;; While c-opt-cpp-* highlights the " + "" ;; obsolete ASP tag + ;; Obsoleted tags were deleted in PHP 7. + ;; @see http://php.net/manual/language.basic-syntax.phptags.php + )) + 0 'php-php-tag)) + + ;; cc-mode patterns + (c-lang-const c-matchers-3 php-cc) + + ;; php-mode patterns *after* cc-mode: + ;; most patterns should go here, faces will only be applied if not + ;; already fontified by another pattern. Note that using OVERRIDE + ;; is usually overkill. + `( + (php-cc-mode--error-control-op-font-lock-find 0 'php-errorcontrol-op t) + ;; import function statement + (,(rx symbol-start (group "use" (+ (syntax whitespace)) "function") + (+ (syntax whitespace))) + (1 'php-import-declaration) + (,(rx (group (+ (or (syntax word) (syntax symbol) "\\" "{" "}")))) nil nil (1 'php-function-name t))) + ;; import constant statement + (,(rx symbol-start (group "use" (+ (syntax whitespace)) "const") + (+ (syntax whitespace))) + (1 'php-import-declaration) + (,(rx (group (+ (or (syntax word) (syntax symbol) "\\" "{" "}")))) nil nil (1 'php-constant-assign t))) + ;; Highlight function calls + ("\\(\\_<\\(?:\\sw\\|\\s_\\)+?\\_>\\)\\s-*(" 1 php-function-call) + ;; Highlight all upper-cased symbols as constant + ("\\<\\([A-Z_][A-Z0-9_]+\\)\\>" 1 'php-constant) + + ;; Highlight all statically accessed class names as constant, + ;; another valid option would be using type-face, but using + ;; constant-face because this is how it works in c++-mode. + ("\\(\\sw+\\)\\(::\\)" (1 'php-constant) (2 'php-paamayim-nekudotayim)) + + ;; Highlight class name after "use .. as" + ("\\]+?\\([\-+./%]?=\\)[^=) --- PHP 8.5. Must precede the comparison + ;; operators, whose `[<>]=?' alternative would otherwise claim the + ;; `>' and leave the `|' unfontified. + ("\\(|>\\)" 1 'php-pipe-op) + + ;; Comparison operators (==, ===, >=, ...) + ("\\([!=]=\\{1,2\\}[>]?\\|[<>]=?\\)" 1 'php-comparison-op) + + ;; Arithmetic operators (+, -, *, **, /, %) + ("\\(?:[A-Za-z0-9[:blank:]]\\)\\([\-+*/%]\\*?\\)\\(?:[A-Za-z0-9[:blank:]]\\)" 1 'php-arithmetic-op) + + ;; Increment and Decrement operators (++, --) + ("\\(\-\-\\|\+\+\\)\$\\w+" 1 'php-inc-dec-op) ;; pre inc/dec + ("\$\\w+\\(\-\-\\|\+\+\\)" 1 'php-inc-dec-op) ;; post inc/dec + + ;; Logical operators (and, or, &&, ...) + ;; Not operator (!) is defined in "before cc-mode" section above. + ("\\(&&\\|||\\)" 1 'php-logical-op) + ;; string interpolation ("$var, ${var}, {$var}") + (php-cc-mode--string-interpolated-variable-font-lock-find 0 nil) + (,(rx symbol-start (group (or "get" "set")) (+ (syntax whitespace)) (or "{" "=>")) + 1 'php-builtin))) + "Detailed highlighting for PHP Mode.") + +(defvar php-cc-font-lock-keywords php-cc-font-lock-keywords-3 + "Default expressions to highlight in PHP Mode.") + +(eval-when-compile + (unless (boundp 'flymake-proc-allowed-file-name-masks) + (add-to-list 'flymake-allowed-file-name-masks + '("\\.php[345s]?\\'" php-flymake-php-init)))) + + +(defun php-send-region (start end) + "Send the region between `START' and `END' to PHP for execution. +The output will appear in the buffer *PHP*." + (interactive "r") + (let ((php-buffer (get-buffer-create "*PHP*")) + (code (buffer-substring start end))) + ;; Calling 'php -r' will fail if we send it code that starts with + ;; '= 31 fontifies a bare `@' as a +keyword because `@new' is registered in `c-type-list-kwds', so this +matcher is set to override that face." + (let (found) + (while (and (not found) + (re-search-forward "@" limit t)) + ;; `php-in-string-or-comment-p' calls `syntax-ppss', which may run + ;; `syntax-propertize' and clobber the match data, so guard it to + ;; keep the `@' match for both the `@new' check below and font-lock. + (unless (or (save-match-data (php-in-string-or-comment-p)) + (save-excursion + (goto-char (match-beginning 0)) + (looking-at-p "@new\\_>"))) + (setq found t))) + found)) + +(defun php-cc-mode--string-interpolated-variable-font-lock-find (limit) + "Apply text-property to LIMIT for string interpolation by font-lock." + (let (quoted-stuff) + (while (re-search-forward php-cc-string-interpolated-variable-regexp limit t) + (setq quoted-stuff (php-in-string-p)) + (when (or (eq ?\" quoted-stuff) (eq ?` quoted-stuff)) + (put-text-property (match-beginning 0) (match-end 0) 'face 'php-variable-name)))) + nil) + +;;; Correct the behavior of `delete-indentation' by modifying the +;;; logic of `fixup-whitespace'. +(defun php-mode--fixup-whitespace-after () + "Remove whitespace before certain characters in PHP Mode." + (when (and (derived-mode-p 'php-cc-mode) + (or (looking-at-p " \\(?:;\\|,\\|->\\|::\\)") + (save-excursion + (forward-char -2) + (looking-at-p "->\\|::")))) + (delete-char 1))) + +;; Advice for lsp-bridge' acm-backend-tabnine +;; see https://github.com/manateelazycat/lsp-bridge/issues/402#issuecomment-1305653058 +(defun php-acm-backend-tabnine-candidate-expand-filter-args (args) + "Adjust to replace bound-start ARGS for Tabnine in PHP." + (if (not (derived-mode-p 'php-cc-mode)) + args + (cl-multiple-value-bind (candidate-info bound-start) args + (save-excursion + (goto-char bound-start) + (when (looking-at-p (eval-when-compile (regexp-quote "$"))) + (setq bound-start (1+ bound-start)))) + (list candidate-info bound-start)))) + +;; NOTE: `auto-mode-alist' registration is intentionally omitted; the +;; cc-mode independent `php-mode' owns the file associations. + +(provide 'php-cc-mode) +;;; php-cc-mode.el ends here diff --git a/lisp/php-complete.el b/lisp/php-complete.el index fe3050cd..3dc1f1f8 100644 --- a/lisp/php-complete.el +++ b/lisp/php-complete.el @@ -24,9 +24,23 @@ ;;; Commentary: -;; Provide auto-compiletion functions. +;; php-complete.el provides a small collection of dependency-light, +;; offline completion-at-point functions (capfs) for PHP. It is not an +;; LSP replacement; it targets the cases an LSP server cannot or will not +;; handle well. Each capf is a small, independent unit usable both as an +;; `M-x' command and as a building block composed with `cape-capf-super': +;; +;; - `php-complete-complete-function' -- built-in function name source. +;; - `php-complete-complete-path' -- filesystem path inside the +;; `__DIR__ . '/...'' idiom. +;; +;; Key-driven insertion (e.g. a context-sensitive "." key via smartchr) +;; is intentionally kept out of this file; it belongs to the orthogonal +;; primitive `php-dot-context'. Both share the same notion of "string" +;; and "magic constant", so insertion and completion stay consistent. -;; These functions are copied function from GNU ELPA. +;; The following helpers are copied from cape.el on GNU ELPA; thanks to +;; the original author Daniel Mendler (@minad). ;; ;; - cape--table-with-properties (cape.el) ;; - cape--bounds (cape.el) @@ -52,6 +66,17 @@ php-defs-functions-alist))) :safe (lambda (value) (and (listp value) (cl-loop for v in values always (assq v php-defs-functions-alist))))) + +;;;###autoload +(defcustom php-complete-path-dir-constants '("__DIR__") + "Magic constants treated as the current file directory for path completion. + +This is the directory-valued subset of `php-magical-constants' used by +`php-complete-complete-path'. Only constants that resolve to a directory +belong here: \"__DIR__\" qualifies, whereas \"__FILE__\" (a file) does not." + :tag "PHP Complete Path Dir Constants" + :type '(repeat string) + :group 'php-complete) ;;; Cape functions: @@ -101,7 +126,13 @@ SORT should be nil to disable sorting." ;;;###autoload (defun php-complete-complete-function (&optional interactive) - "Complete PHP keyword at point. + "Complete a PHP built-in function name at point. + +This is the offline built-in function-name source: it offers names from +the modules listed in `php-complete-function-modules', and is meant for +environments without an LSP server. It does not fire after `->' or `::', +nor after a variable, so it only suggests where a bare function call makes +sense. If INTERACTIVE is nil the function acts like a capf." (interactive (list t)) @@ -119,5 +150,77 @@ If INTERACTIVE is nil the function acts like a capf." :company-kind (lambda (_) 'keyword) :exclusive 'no)))) +;;; Path completion rooted at `__DIR__': + +(defun php-complete--path-directory () + "Return the directory that `__DIR__' resolves to for the current buffer." + (or (and buffer-file-name (file-name-directory buffer-file-name)) + default-directory)) + +(defun php-complete--path-string-bounds () + "Return (CONTENT-BEG . STR-END) when point is inside a `__DIR__ . STRING'. + +CONTENT-BEG is placed after the opening quote and a single leading slash, +so the path is completed relative to the directory of the current file. +Return nil when point is not inside such a string. + +The recognized directory-valued constants are held in +`php-complete-path-dir-constants', a subset of `php-magical-constants', +so this shares its notion of \"magic constant\" with `php-dot-context'." + (when (php-in-string-p) + ;; Take the string start from `syntax-ppss' (nth 8): it is reliable even + ;; for the unterminated string that is normal while typing + ;; ("__DIR__ . '/" before the closing quote exists). Match the preceding + ;; "CONST ." with `looking-back' rather than the token scanner, which is + ;; not meant to be entered from the opening-quote position. + (let ((str-beg (nth 8 (syntax-ppss)))) + (when (save-excursion + (goto-char str-beg) + (looking-back + (concat (regexp-opt php-complete-path-dir-constants 'symbols) + "[ \t\r\n]*\\.[ \t\r\n]*") + (max (point-min) (- str-beg 120)))) + (let ((content-beg (1+ str-beg))) + ;; Keep a single leading "/" fixed so it stays a separator and the + ;; path resolves relative to `__DIR__' instead of the filesystem root. + (when (eq (char-after content-beg) ?/) + (setq content-beg (1+ content-beg))) + (cons content-beg + (or (ignore-errors + (save-excursion (goto-char str-beg) (forward-sexp) (point))) + (point)))))))) + +(defun php-complete--path-table () + "Return a file-name completion table rooted at the current file directory. +The directory is what `__DIR__' resolves to at runtime, bound when the +table is called rather than captured from the buffer `default-directory'." + (let ((dir (php-complete--path-directory))) + (lambda (string pred action) + (let ((default-directory dir) + (non-essential t)) + (read-file-name-internal string pred action))))) + +;;;###autoload +(defun php-complete-complete-path (&optional interactive) + "Complete a filesystem path written as `__DIR__ . \\='/...\\='.' + +Inside the string of `__DIR__ . \\='/PATH\\='', complete PATH from the +directory of the current file, one path component at a time. This is the +completion half of the `__DIR__' path idiom; inserting the leading +`. \\='/\\='' is left to the editor (see `php-dot-context' and the smartchr +recipe in the README), keeping key-driven insertion and +completion-at-point orthogonal but consistent. + +If INTERACTIVE is nil the function acts like a capf." + (interactive (list t)) + (if interactive + (php-complete--cape-interactive #'php-complete-complete-path) + (when-let* ((bounds (php-complete--path-string-bounds))) + `(,(min (car bounds) (point)) ,(point) + ,(php-complete--path-table) + :annotation-function ,(lambda (_) " __DIR__ path") + :company-kind ,(lambda (_) 'file) + :exclusive no)))) + (provide 'php-complete) ;;; php-complete.el ends here diff --git a/lisp/php-indent.el b/lisp/php-indent.el index 1a61d2ec..c8a017bb 100644 --- a/lisp/php-indent.el +++ b/lisp/php-indent.el @@ -464,9 +464,10 @@ until a statement terminator (`;', `{', `}', an opening `(' or `[', a A continuation line is one whose previous code token leaves the statement unterminated: an identifier or keyword (as in stacked member modifiers, `extends' on its own line or a broken `return'), a closing -parenthesis, or a return-type `):'. Such lines are indented one -`php-indent-offset' beyond the first line of the statement. Return nil -when the current line does not continue a statement this way." +parenthesis, a return-type `):', or a trailing `=>'. Such lines are +indented one `php-indent-offset' beyond the first line of the +statement. Return nil when the current line does not continue a +statement this way." (save-excursion (back-to-indentation) (unless (or (memq (char-after) '(?\{ ?\} ?\) ?\])) @@ -480,7 +481,15 @@ when the current line does not continue a statement this way." ;; A `):' return-type (or alternative-syntax `):') ;; also leaves the statement open. (and (eq (char-before) ?:) - (eq (char-before (1- (point))) ?\))))) + (eq (char-before (1- (point))) ?\))) + ;; So does a `=>' left dangling at the end of a + ;; line: an arrow function's body, an array value + ;; or a property hook's expression follows it. + ;; `=>' is excluded from + ;; `php-indent--indent-operator-re', so nothing + ;; else picks these up. + (and (eq (char-before) ?>) + (eq (char-before (1- (point))) ?=)))) (goto-char pos) (php-indent--goto-statement-start) (unless (php-indent--same-line pos) diff --git a/lisp/php-mode-debug.el b/lisp/php-mode-debug.el index efe6e48e..d0245692 100644 --- a/lisp/php-mode-debug.el +++ b/lisp/php-mode-debug.el @@ -26,14 +26,24 @@ ;; Provides functions to debugging php-mode work. ;;; Code: -(require 'cc-mode) +(require 'cl-lib) (require 'cus-edit) -(require 'php-mode) (require 'package) (require 'pkg-info nil t) (require 'el-get nil t) +;; The CC Mode based `php-cc-mode' is intentionally NOT required here. The +;; CC Mode diagnostics below are the only part that depends on it, and they +;; are guarded so that invoking `php-mode-debug' in a cc-mode independent +;; `php-mode' session does not drag in (and thus get polluted by) the legacy +;; implementation. Symbols owned by CC Mode / `php-cc-mode' are only ever +;; touched from inside a `php-cc-mode' buffer, where both are already loaded. +(eval-when-compile + (require 'cc-mode)) + (declare-function pkg-info-version-info "ext:pkg-info" (library &optional package show)) +(declare-function php-mode-version "php-cc-mode" (&rest args)) +(defvar php-mode-cc-version) (defun php-mode-debug-reinstall (force &optional called-interactive) "Reinstall PHP Mode to solve Cc Mode version mismatch. @@ -96,8 +106,8 @@ When CALLED-INTERACTIVE then message the result." (defun php-mode-debug () "Display informations useful for debugging PHP Mode." (interactive) - (unless (eq major-mode 'php-mode) - (user-error "Invoke this command only in php-mode buffer")) + (unless (derived-mode-p 'php-base-mode) + (user-error "Invoke this command only in a PHP Mode buffer")) (php-mode-debug--buffer 'init) (php-mode-debug--message "Feel free to report on GitHub what you noticed!") (php-mode-debug--message "https://github.com/emacs-php/php-mode/issues/new") @@ -105,12 +115,18 @@ When CALLED-INTERACTIVE then message the result." (php-mode-debug--message "Pasting the following information on the issue will help us to investigate the cause.") (php-mode-debug--message "```") (php-mode-debug--message "--- PHP-MODE DEBUG BEGIN ---") - (php-mode-debug--message "versions: %s; %s; Cc Mode %s)" - (emacs-version) - (php-mode-version) - (if (string= php-mode-cc-version c-version) - c-version - (format "%s (php-mode-cc-version: %s *mismatched*)" c-version php-mode-cc-version))) + ;; The version line reports the CC Mode based `php-cc-mode' build and + ;; compares its baked-in CC Mode version against the running one. Only + ;; the legacy mode carries `php-mode-version' / `php-mode-cc-version', so + ;; skip it (rather than loading `php-cc-mode') outside a cc buffer. + (if (derived-mode-p 'php-cc-mode) + (php-mode-debug--message "versions: %s; %s; Cc Mode %s)" + (emacs-version) + (php-mode-version) + (if (string= php-mode-cc-version c-version) + c-version + (format "%s (php-mode-cc-version: %s *mismatched*)" c-version php-mode-cc-version))) + (php-mode-debug--message "versions: %s" (emacs-version))) (php-mode-debug--message "package-version: %s" (if (fboundp 'pkg-info) (pkg-info-version-info 'php-mode) @@ -132,13 +148,18 @@ When CALLED-INTERACTIVE then message the result." (cl-loop for (v type) in (custom-group-members 'php nil) if (eq type 'custom-variable) collect (list v (symbol-value v)))) - (php-mode-debug--message "c-indentation-style: %s" c-indentation-style) - (php-mode-debug--message "c-style-variables: %s" - (cl-loop for v in c-style-variables - unless (memq v '(c-doc-comment-style c-offsets-alist)) - collect (list v (symbol-value v)))) - (php-mode-debug--message "c-doc-comment-style: %s" c-doc-comment-style) - (php-mode-debug--message "c-offsets-alist: %s" c-offsets-alist) + ;; CC Mode specific state. These variables only exist and are only + ;; meaningful in a `php-cc-mode' buffer; guarding on the buffer's mode + ;; keeps `php-mode-debug' usable in the cc-mode independent `php-mode' + ;; without touching CC Mode. + (when (derived-mode-p 'php-cc-mode) + (php-mode-debug--message "c-indentation-style: %s" c-indentation-style) + (php-mode-debug--message "c-style-variables: %s" + (cl-loop for v in c-style-variables + unless (memq v '(c-doc-comment-style c-offsets-alist)) + collect (list v (symbol-value v)))) + (php-mode-debug--message "c-doc-comment-style: %s" c-doc-comment-style) + (php-mode-debug--message "c-offsets-alist: %s" c-offsets-alist)) (php-mode-debug--message "buffer: %s" (list :length (save-excursion (goto-char (point-max)) (point)))) (php-mode-debug--message "--- PHP-MODE DEBUG END ---") (php-mode-debug--message "```\n") diff --git a/lisp/php-mode.el b/lisp/php-mode.el index 4fd23b29..673996d3 100644 --- a/lisp/php-mode.el +++ b/lisp/php-mode.el @@ -1,23 +1,19 @@ ;;; php-mode.el --- Major mode for editing PHP code -*- lexical-binding: t; -*- -;; Copyright (C) 2024 Friends of Emacs-PHP development +;; Copyright (C) 2026 Friends of Emacs-PHP development +;; Copyright (C) 1999-2026 Free Software Foundation, Inc. ;; Copyright (C) 1999, 2000, 2001, 2003, 2004 Turadg Aleahmad ;; 2008 Aaron S. Hawley -;; 2011, 2012, 2013, 2014, 2015, 2016, 2017 Eric James Michael Ritz +;; 2011-2017 Eric James Michael Ritz -;; Author: Eric James Michael Ritz +;; Author: USAMI Kenta ;; Maintainer: USAMI Kenta ;; URL: https://github.com/emacs-php/php-mode ;; Keywords: languages php ;; Version: 1.26.1 -;; Package-Requires: ((emacs "27.1")) +;; Package-Requires: ((emacs "28.1")) ;; License: GPL-3.0-or-later -(eval-and-compile - (make-obsolete-variable - (defconst php-mode-version-number "1.26.1" "PHP Mode version number.") - "Please call (php-mode-version :as-number t) for compatibility." "1.24.2")) - ;; 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 @@ -33,100 +29,37 @@ ;;; Commentary: -;; PHP Mode is a major mode for editing PHP script. It's an extension -;; of CC mode; thus it inherits all C mode's navigation functionality. -;; But it colors according to the PHP syntax and indents according to the -;; PSR-2 coding guidelines. It also includes a couple handy IDE-type -;; features such as documentation search and a source and class browser. - -;; Please read the manual for setting items compatible with CC Mode. -;; https://www.gnu.org/software/emacs/manual/html_mono/ccmode.html - -;; This mode is designed for PHP scripts consisting of a single ") - (prefix "\\" "::"))) - -(c-lang-defconst c-operators - php `((prefix "new" "clone") - ,@(c-lang-const c-identifier-ops) - (postfix "->") - (postfix "++" "--" "[" "]" "(" ")") - (right-assoc "**") - (prefix "++" "--" "+" "-" "~" "(" ")" "@") - (prefix "instanceof") - (prefix "!") - (left-assoc "*" "/" "%") - (left-assoc "+" "-" ".") - (left-assoc "<<" ">>") - (left-assoc "<" ">" "<=" ">=") - (left-assoc "==" "!=" "===" "!==" "<>" "<=>") - (left-assoc "&") - (left-assoc "^") - (left-assoc "|") - (left-assoc "&&") - (left-assoc "||") - (right-assoc "??") - (left-assoc "?:") - (right-assoc-sequence "?" ":") - (right-assoc ,@(c-lang-const c-assignment-operators)) - (left-assoc "and") - (left-assoc "xor") - (left-assoc "or") - (left-assoc ",") - (left-assoc "=>"))) - -;; Allow '\' when scanning from open brace back to defining -;; construct like class -(c-lang-defconst c-block-prefix-disallowed-chars - php (cl-set-difference (c-lang-const c-block-prefix-disallowed-chars) - '(?\\))) - -;; Allow $ so variables are recognized in cc-mode and remove @. This -;; makes cc-mode highlight variables and their type hints in arglists. -(c-lang-defconst c-symbol-start - php (concat "[" c-alpha "_$]")) - -;; All string literals can possibly span multiple lines -(c-lang-defconst c-multiline-string-start-char - php t) - -(c-lang-defconst c-assignment-operators - php '("=" "*=" "/=" "%=" "+=" "-=" ">>=" "<<=" "&=" "^=" "|=" ".=" "??=")) - -(c-lang-defconst beginning-of-defun-function - php 'php-beginning-of-defun) - -(c-lang-defconst end-of-defun-function - php 'php-end-of-defun) - -(c-lang-defconst c-primitive-type-kwds - php '("int" "integer" "bool" "boolean" "float" "double" "real" - "string" "object" "void" "mixed" "never")) - -(c-lang-defconst c-class-decl-kwds - "Keywords introducing declarations where the following block (if any) -contains another declaration level that should be considered a class." - php '("class" "trait" "interface" "enum")) - -(c-lang-defconst c-brace-list-decl-kwds - "Keywords introducing declarations where the following block (if -any) is a brace list. - -PHP does not have an C-like \"enum\" keyword." - php nil) - -;; cc-mode renamed `c-after-brace-list-decl-kwds' to `c-after-enum-list-kwds'; -;; keep the old name defined so cc-langs' transitional lookup stays quiet where -;; the rename landed (see `c-after-enum-list-key' in cc-langs.el). -(c-lang-defconst c-after-brace-list-decl-kwds - php nil) - -(c-lang-defconst c-typeless-decl-kwds - php (append (c-lang-const c-class-decl-kwds php) '("function" "const"))) - -(c-lang-defconst c-modifier-kwds - php '("abstract" "final" "static" "case" "readonly")) - -(c-lang-defconst c-protection-kwds - "Access protection label keywords in classes." - php '("private" "protected" "public")) - -(c-lang-defconst c-postfix-decl-spec-kwds - php '("implements" "extends")) - -(c-lang-defconst c-type-list-kwds - php '("@new" ;; @new is *NOT* language construct, it's workaround for coloring. - "new" "use" "implements" "extends" "namespace" "instanceof" "insteadof")) - -(c-lang-defconst c-ref-list-kwds - php nil) - -(c-lang-defconst c-block-stmt-2-kwds - php '("catch" "declare" "elseif" "for" "foreach" "if" "switch" "while")) - -(c-lang-defconst c-simple-stmt-kwds - php '("break" "continue" "die" "echo" "exit" "goto" "return" "throw" - "include" "include_once" "print" "require" "require_once")) - -(c-lang-defconst c-constant-kwds - php '("true" "false" "null")) - -(c-lang-defconst c-lambda-kwds - php '("function" "use")) - -(c-lang-defconst c-inexpr-block-kwds - php '("match")) - -(c-lang-defconst c-other-block-decl-kwds - php '("namespace")) - -(c-lang-defconst c-other-kwds - "Keywords not accounted for by any other `*-kwds' language constant." - php - '("__halt_compiler" - "and" - "array" - "as" - "break" - "catch" - "clone" - "default" - "empty" - "enddeclare" - "endfor" - "endforeach" - "endif" - "endswitch" - "endwhile" - "eval" - "fn" ;; NOT c-lambda-kwds - "global" - "isset" - "list" - "or" - "parent" - "static" - "unset" - "var" - "xor" - "yield" - "yield from" - - ;; Below keywords are technically not reserved keywords, but - ;; threated no differently by php-mode from actual reserved - ;; keywords - ;; - ;;; declare directives: - "encoding" - "ticks" - "strict_types" - - ;;; self for static references: - "self")) - -;; PHP does not have <> templates/generics -(c-lang-defconst c-recognize-<>-arglists - php nil) - -(c-lang-defconst c-<>-type-kwds - php nil) - -(c-lang-defconst c-inside-<>-type-kwds - php nil) - -(c-lang-defconst c-enums-contain-decls - php nil) - -(c-lang-defconst c-nonlabel-token-key - "Regexp matching things that can't occur in generic colon labels. - -This overrides cc-mode `c-nonlabel-token-key' to support switching on -double quoted strings and true/false/null. - -Note: this regexp is also applied to goto-labels, a future improvement -might be to handle switch and goto labels differently." - php (concat - ;; All keywords except `c-label-kwds' and `c-constant-kwds'. - (c-make-keywords-re t - (cl-set-difference (c-lang-const c-keywords) - (append (c-lang-const c-label-kwds) - (c-lang-const c-constant-kwds)) - :test 'string-equal)))) - -(c-lang-defconst c-basic-matchers-before - php (cl-remove-if (lambda (elm) (and (listp elm) (equal (car elm) "\\s|"))) - (c-lang-const c-basic-matchers-before php))) - -(c-lang-defconst c-basic-matchers-after - php (cl-remove-if (lambda (elm) (and (listp elm) (memq 'c-annotation-face elm))) - (c-lang-const c-basic-matchers-after php))) - -(defconst php-mode--re-return-typed-closure - (eval-when-compile - (rx symbol-start "function" symbol-end - (* (syntax whitespace)) - "(" (* (not (any "("))) ")" - (* (syntax whitespace)) - (? symbol-start "use" symbol-end - (* (syntax whitespace)) - "(" (* (not (any "("))) ")" - (* (syntax whitespace))) - ":" (+ (not (any "{}"))) - (group "{")))) - -(defun php-c-lineup-arglist (langelem) - "Line up the current argument line under the first argument using -`c-lineup-arglist' LANGELEM." - (let (in-return-typed-closure) - (when (and (consp langelem) - (eq 'arglist-cont-nonempty (car langelem))) - (save-excursion - (save-match-data - (when (re-search-backward php-mode--re-return-typed-closure (cdr langelem) t) - (goto-char (match-beginning 1)) - (when (not (php-in-string-or-comment-p)) - (setq in-return-typed-closure t)))))) - (unless in-return-typed-closure - (c-lineup-arglist langelem)))) - -(defun php-lineup-cascaded-calls (langelem) - "Line up chained methods using `c-lineup-cascaded-calls', -but only if the setting is enabled." - (cond - (php-mode-lineup-cascaded-calls (c-lineup-cascaded-calls langelem)) - ((assq 'arglist-cont-nonempty c-syntactic-context) nil) - ((assq 'defun-block-intro c-syntactic-context) nil) - ((assq 'defun-close c-syntactic-context) nil) - ((assq 'statement-cont c-syntactic-context) nil) - ((save-excursion - (beginning-of-line) - (let ((beginning-of-langelem (cdr langelem)) - (beginning-of-current-line (point)) - start) - (skip-chars-forward " ") - (cond - ((looking-at-p "->") '+) - ((looking-at-p "[:?]") '+) - ((looking-at-p "[,;]") nil) - ((looking-at-p "//") nil) - ;; Is the previous line terminated with `,' ? - ((progn - (forward-line -1) - (end-of-line) - (skip-chars-backward " ") - (backward-char 1) - (while (and (< beginning-of-langelem (point)) - (setq start (php-in-string-or-comment-p))) - (goto-char start) - (skip-chars-backward " \r\n") - (backward-char 1)) - (and (not (eq (point) beginning-of-current-line)) - (not (php-in-string-or-comment-p)) - (not (looking-at-p ",")) - (save-excursion - (backward-char) (not (looking-at-p ","))))) - '+) - (t nil))))))) - -(defun php-c-looking-at-or-maybe-in-bracelist (orig-fun &optional containing-sexp lim &rest args) - "Replace `c-looking-at-or-maybe-in-bracelist'. - -CONTAINING-SEXP is the position of the brace/paren/bracket enclosing -POINT, or nil if there is no such position, or we do not know it. LIM is -a backward search limit." - (cond - ((not (derived-mode-p 'php-mode)) (apply orig-fun containing-sexp lim args)) - ((looking-at-p "{") - (save-excursion - (c-backward-token-2 2 t lim) - ;; PHP 8.0 match expression - ;; echo match ($var) |{ - ;; ↑ matches ↑ initial position - (when (looking-at-p (eval-when-compile (rx symbol-start "match" symbol-end))) - (cons (point) t)))) - (t nil))) - -(defun php-mode--anonymous-function-brace-p (pos) - "Return non-nil when the opening brace at POS starts a closure body. - -POS must be the position of a `{'. A closure is an anonymous -`function (...) [use (...)] [: type] {' expression; named function -declarations and other constructs (e.g. `match', `if') return nil." - (save-excursion - (goto-char pos) - (c-backward-syntactic-ws) - ;; Step back over an optional return type declaration `): Type {' so - ;; that point lands just after the parameter (or `use') list's `)'. - (unless (eq (char-before) ?\)) - (let ((colon (save-excursion - (skip-chars-backward "^):;{}(" (point-min)) - (and (eq (char-before) ?:) (1- (point)))))) - (when colon - (goto-char colon) - (c-backward-syntactic-ws)))) - ;; Walk backwards over the `)...(' list(s): the parameter list, plus an - ;; optional `use (...)' clause, until we reach `function' or give up. - (let ((result nil) (continue t)) - (while (and continue (eq (char-before) ?\))) - (condition-case nil - (backward-list) - (error (setq continue nil))) - (when continue - (c-backward-syntactic-ws) - (if (zerop (c-backward-token-2)) - (cond - ((looking-at-p "\\_") (setq result t continue nil)) - ((looking-at-p "\\_") (c-backward-syntactic-ws)) - (t (setq continue nil))) - (setq continue nil)))) - result))) - -(defun php-c-looking-at-statement-block (orig-fun &rest args) - "Treat a closure body as a statement block around `c-looking-at-statement-block'. - -Emacs' CC Mode >= 31 (the change for bug#19867) fails to recognize an -anonymous function body whose first line is a comment as a statement -block, and reports it as a brace list, mis-indenting both the body and -its closing brace. When point is at the opening brace of such a closure, -return non-nil; otherwise fall back to ORIG-FUN with ARGS. - -A closure body is always a statement block, so this is also correct on -Emacs versions unaffected by the bug." - (or (and (derived-mode-p 'php-mode) - (eq (char-after) ?\{) - (php-mode--anonymous-function-brace-p (point))) - (apply orig-fun args))) - -(c-add-style - "php" - `((c-basic-offset . 4) - (c-offsets-alist . ((arglist-close . php-lineup-arglist-close) - (arglist-cont . (first php-lineup-cascaded-calls 0)) - (arglist-cont-nonempty . (first php-lineup-cascaded-calls php-c-lineup-arglist)) - (arglist-intro . php-lineup-arglist-intro) - (case-label . +) - (class-open . 0) - (comment-intro . 0) - (inexpr-class . 0) - (inlambda . 0) - (inline-open . 0) - (namespace-open . 0) - (lambda-intro-cont . +) - (label . +) - (statement-cont . (first php-lineup-cascaded-calls php-lineup-string-cont +)) - (substatement-open . 0) - (topmost-intro-cont . (first php-lineup-cascaded-calls +)))) - (indent-tabs-mode . nil) - (tab-width . ,(default-value 'tab-width)) - (fill-column . ,(default-value 'fill-column)) - (show-trailing-whitespace . ,(default-value 'show-trailing-whitespace)) - (php-mode-lineup-cascaded-calls . t) - (php-style-delete-trailing-whitespace . nil))) - -(defun php-enable-default-coding-style () - "Set PHP Mode to use reasonable default formatting." - (interactive) - (php-set-style "php")) - -(c-add-style - "pear" - '("php" - (c-basic-offset . 4) - (c-offsets-alist . ((case-label . 0) (statement-cont . +))) - (tab-width . 4) - (php-mode-lineup-cascaded-calls . nil))) - -(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")) - -(c-add-style - "drupal" - '("php" - (c-basic-offset . 2) - (tab-width . 2) - (fill-column . 78) - (show-trailing-whitespace . t) - (php-mode-lineup-cascaded-calls . nil) - (php-style-delete-trailing-whitespace . t))) - -(defun php-enable-drupal-coding-style () - "Make `php-mode' use coding styles that are preferable for working with Drupal." - (interactive) - (php-set-style "drupal")) - -(c-add-style - "wordpress" - '("php" - (c-basic-offset . 4) - (c-indent-comments-syntactically-p t) - (indent-tabs-mode . t) - (tab-width . 4) - (fill-column . 78))) - -(defun php-enable-wordpress-coding-style () - "Make `php-mode' use coding styles that are preferable for working with -Wordpress." - (interactive) - (php-set-style "wordpress")) - -(c-add-style - "symfony2" - '("php" - (c-offsets-alist . ((statement-cont . php-lineup-hanging-semicolon))) - (c-indent-comments-syntactically-p . t) - (php-mode-lineup-cascaded-calls . nil) - (fill-column . 78))) - -(defun php-enable-symfony2-coding-style () - "Make `php-mode' use coding styles that are preferable for working with Symfony2." - (interactive) - (php-set-style "symfony2")) - -(c-add-style - "psr2" ; PSR-2 / PSR-12 - '("php" - (c-offsets-alist . ((statement-cont . +))) - (c-indent-comments-syntactically-p . t) - (fill-column . 78) - (show-trailing-whitespace . t) - (php-mode-lineup-cascaded-calls . nil) - (php-style-delete-trailing-whitespace . t))) - -(defun php-enable-psr2-coding-style () - "Make `php-mode' comply to the PSR-2 coding style." - (interactive) - (php-set-style "psr2")) - -(defun php-beginning-of-defun (&optional arg) - "Move to the beginning of the ARGth PHP function from point. -Implements PHP version of `beginning-of-defun-function'." - (interactive "p") - (let (found-p (arg (or arg 1))) - (while (> arg 0) - (setq found-p (re-search-backward php-beginning-of-defun-regexp - nil 'noerror)) - (setq arg (1- arg))) - (while (< arg 0) - (end-of-line 1) - (let ((opoint (point))) - (beginning-of-defun 1) - (forward-list 2) - (forward-line 1) - (if (eq opoint (point)) - (setq found-p (re-search-forward php-beginning-of-defun-regexp - nil 'noerror))) - (setq arg (1+ arg)))) - (not (null found-p)))) - -(defun php-end-of-defun (&optional arg) - "Move the end of the ARGth PHP function from point. -Implements PHP version of `end-of-defun-function' - -See `php-beginning-of-defun'." - (interactive "p") - (php-beginning-of-defun (- (or arg 1)))) - - -(defvar-local php-warned-bad-indent nil - "Non-nil once the user has been warned about indenting this buffer. -Buffer-local so a warning in one buffer does not suppress it in others.") - -(defun php-check-html-for-indentation () - "Return non-nil when the current buffer may be indented as PHP. -Warn and offer to switch to `php-html-template-major-mode' when the -buffer looks like an HTML template edited in plain `php-mode'." - (cond - ((not php-mode-warn-if-html-template) t) - ;; In a polymode buffer php-mode only ever indents its own PHP chunks, - ;; so indent normally and never warn about the surrounding HTML. - ((php-in-poly-php-html-mode) t) - ((not (php-buffer-has-html-tag)) t) - (php-warned-bad-indent nil) - ((fboundp php-html-template-major-mode) - (if (y-or-n-p (format "This file seems to contain an HTML tag. Switch to %s? " - php-html-template-major-mode)) - (funcall php-html-template-major-mode) - (prog1 nil - (setq-local php-warned-bad-indent t)))) - (t ;; Suppress warnings in Emacs session - (setq-local php-warned-bad-indent t) - (lwarn 'php-mode - :warning "Indentation fails badly with mixed HTML/PHP in the HTML part in plain `php-mode'. -It is highly recommended to install a major mode that supports PHP and HTML templates, such as Web Mode. - -Set `php-html-template-major-mode' variable to use a mode other than `web-mode'. -Set `php-mode-warn-if-html-template' variable to nil to suppress the warning. -") - nil))) - -(defun php-cautious-indent-region (start end &optional quiet) - "Carefully indent region START to END in contexts other than HTML templates. - -If the optional argument QUIET is non-nil then no syntactic errors are -reported, even if `c-report-syntactic-errors' is non-nil." - (when (php-check-html-for-indentation) - (c-indent-region start end quiet))) - -(defun php-cautious-indent-line () - "Carefully indent lines in contexts other than HTML templates." - (when (and (php-check-html-for-indentation) - (save-excursion - (beginning-of-line) - ;; Don't indent heredoc end mark - (not (and (looking-at-p "[a-zA-Z0-9_]+;\n") (php-in-string-p))))) - (c-indent-line))) - -(defun php-c-at-vsemi-p (&optional pos) - "Return T on HTML lines (including php tag) or PHP8 Attribute, otherwise NIL. -POS is a position on the line in question. - -This is was done due to the problem reported here: - - URL `https://answers.launchpad.net/nxhtml/+question/43320'" - ;; If this function could call c-beginning-of-statement-1, change php-c-vsemi-status-unknown-p. - (save-excursion - (if pos - (goto-char pos) - (setq pos (point))) - (cond - ;; Detect PHP8 attribute: #[Attribute()] - ((and (< 1 pos) (< 1 (- pos (c-point 'bol)))) - (backward-char 1) - (looking-at-p (eval-when-compile (rx "]" (* (syntax whitespace)) (or "#[" line-end))))) - ;; Detect HTML/XML tag and PHP tag () - (php-mode-template-compatibility - (beginning-of-line) - (looking-at-p - (eval-when-compile - (rx (or (: bol (0+ space) "<" (in "?a-z")) - (: (0+ not-newline) (in "?a-z") ">" (0+ space) eol))))))))) - -(defun php-c-vsemi-status-unknown-p () - "Always return NIL. See `c-vsemi-status-unknown-p'." - ;; Current implementation of php-c-at-vsemi-p never calls c-beginning-of-statement-1 - nil) - -(defun php-lineup-string-cont (langelem) - "Line up string toward equal sign or dot. -e.g. -$str = \\='some' - . \\='string'; -this ^ lineup" - (save-excursion - (goto-char (cdr langelem)) - (let (ret finish) - (while (and (not finish) (re-search-forward "[=.]" (line-end-position) t)) - (unless (php-in-string-or-comment-p) - (setq finish t - ret (vector (1- (current-column)))))) - ret))) - -(defun php-lineup-arglist-intro (langelem) - (save-excursion - (goto-char (cdr langelem)) - (vector (+ (current-column) c-basic-offset)))) - -(defun php-lineup-arglist-close (langelem) - (save-excursion - (goto-char (cdr langelem)) - (vector (current-column)))) - -(defun php-lineup-arglist (_langelem) - (save-excursion - (beginning-of-line) - (if (looking-at-p "\\s-*->") '+ 0))) - -(defun php-lineup-hanging-semicolon (_langelem) - (save-excursion - (beginning-of-line) - (if (looking-at-p "\\s-*;\\s-*$") 0 '+))) +;; The heredoc/nowdoc, PHP 8 attribute and comment-quote propertize +;; helpers below are cc-mode independent. They are duplicated verbatim +;; from lisp/php-cc-mode.el (rather than shared through a common file) so +;; that both major modes can be loaded at the same time: the definitions +;; are identical, so a later redefinition by the other file is harmless. (eval-and-compile (defconst php-heredoc-start-re @@ -1042,10 +191,6 @@ HEREDOC-START." (unwind-protect (let (new-start new-end) (goto-char start) - ;; Consider bounding this backwards search by `beginning-of-defun'. - ;; (Benchmarking for a wide range of cases may be needed to decide - ;; whether that's an improvement, as `php-beginning-of-defun' also - ;; uses `re-search-backward'.) (when (re-search-backward php-heredoc-start-re nil t) (let ((maybe (point))) (when (and (re-search-forward (php-heredoc-end-re (match-string 0)) nil t) @@ -1066,253 +211,8 @@ HEREDOC-START." (setq php-mode--propertize-extend-region-current (delete pair php-mode--propertize-extend-region-current)))))) -(easy-menu-define php-mode-menu php-mode-map "PHP Mode Commands." - (cons "PHP" (c-lang-const c-mode-menu php))) - -(defun php-mode-get-style-alist () - "Return an alist consisting of `php' style and styles that inherit it." - (cl-loop for l in c-style-alist - if (or (string= (car l) "php") - (equal (cadr l) "php")) - collect l)) - -(defvar php-mode-set-style-history nil) -(defvar-local php-mode--delayed-set-style nil) -(defvar-local php-style-delete-trailing-whitespace nil) - -(defun php-set-style (stylename &optional dont-override) - "Set the current `php-mode' buffer to use the style STYLENAME. -STYLENAME is one of the names selectable in `php-mode-coding-style'. - -Borrow the `interactive-form' from `c-set-style' and use the original -`c-set-style' function to set all declared stylevars. -For compatibility with `c-set-style' pass DONT-OVERRIDE to it. - -After setting the stylevars run hook `php-mode-STYLENAME-hook'." - (interactive - (list (let ((completion-ignore-case t) - (prompt (format "Which %s indentation style? " - mode-name))) - (completing-read prompt - (php-mode-get-style-alist) - nil t nil - 'php-mode-set-style-history - c-indentation-style)))) - (php-mode--disable-delay-set-style) - - ;; Back up manually set variables - (let* (value - (backup-vars - (and php-mode-enable-backup-style-variables - (cl-loop for name in c-style-variables - do (setq value (symbol-value name)) - if (and value (not (eq 'set-from-style value))) - collect (cons name value))))) - (c-set-style stylename dont-override) - ;; Restore variables - (cl-loop for (name . value) in backup-vars - do (set (make-local-variable name) value))) - (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)) - (run-hooks (intern (format "php-mode-%s-hook" stylename)))) - -(defun php-mode--disable-delay-set-style (&rest _args) - "Disable `php-mode-set-style-delay' on after hook. ARGS be ignore." - (setq php-mode--delayed-set-style nil) - (advice-remove #'php-mode--disable-delay-set-style #'c-set-style)) - -(defun php-mode-set-style-delay () - "Set the current `php-mode' buffer to use the style by custom or local variables." - (when php-mode--delayed-set-style - (let ((coding-style (or (and (boundp 'php-project-coding-style) php-project-coding-style) - php-mode-coding-style))) - (prog1 (when coding-style - (php-set-style (symbol-name coding-style))) - (remove-hook 'hack-local-variables-hook #'php-mode-set-style-delay))))) - -(defun php-mode-set-local-variable-delay () - "Set local variable from php-project." - (php-project-apply-local-variables) - (remove-hook 'hack-local-variables-hook #'php-mode-set-local-variable-delay)) - -(defvar php-mode-syntax-table - (let ((table (make-syntax-table))) - (c-populate-syntax-table table) - (modify-syntax-entry ?_ "_" table) - (modify-syntax-entry ?` "\"" table) - (modify-syntax-entry ?\" "\"" table) - (modify-syntax-entry ?# "< b" table) - (modify-syntax-entry ?\n "> b" table) - (modify-syntax-entry ?$ "_" table) - table)) - -;;;###autoload -(define-derived-mode php-mode php-base-mode "PHP" - "Major mode for editing PHP code. - -\\{php-mode-map}" - :syntax-table php-mode-syntax-table - :after-hook (progn (c-make-noise-macro-regexps) - (c-make-macro-with-semi-re) - (c-update-modeline)) - (unless (string= php-mode-cc-version c-version) - (php-mode-debug-reinstall nil)) - - (c-initialize-cc-mode t) - (setq abbrev-mode t) - - ;; Must be called once as c-mode to enable font-lock for Heredoc. - ;; TODO: This call may be removed in the future. - (c-common-init 'c-mode) - - (c-init-language-vars php-mode) - (c-common-init 'php-mode) - (cc-imenu-init cc-imenu-c-generic-expression) - - (setq-local c-auto-align-backslashes nil) - - (setq-local comment-start "// ") - (setq-local comment-start-skip - (eval-when-compile - (rx (group (or (: "#" (not (any "["))) - (: "/" (+ "/")) - (: "/*"))) - (* (syntax whitespace))))) - (setq-local comment-end "") - (setq-local page-delimiter php-mode-page-delimiter) +;;; PHPDoc font-lock - (with-suppressed-warnings ((obsolete font-lock-string-face - font-lock-keyword-face - font-lock-builtin-face - font-lock-function-name-face - font-lock-variable-name-face - font-lock-constant-face)) - (setq-local font-lock-string-face 'php-string) - (setq-local font-lock-keyword-face 'php-keyword) - (setq-local font-lock-builtin-face 'php-builtin) - (setq-local c-preprocessor-face-name 'php-php-tag) - (setq-local font-lock-function-name-face 'php-function-name) - (setq-local font-lock-variable-name-face 'php-variable-name) - (setq-local font-lock-constant-face 'php-constant)) - - (setq-local syntax-propertize-function #'php-syntax-propertize-function) - (add-hook 'syntax-propertize-extend-region-functions - #'php-syntax-propertize-extend-region t t) - - (setq imenu-generic-expression (if (symbolp php-imenu-generic-expression) - (symbol-value php-imenu-generic-expression) - php-imenu-generic-expression)) - - ;; PHP vars are case-sensitive - (setq case-fold-search t) - - (when php-mode-enable-project-local-variable - (add-hook 'hack-local-variables-hook #'php-mode-set-local-variable-delay t t)) - - ;; When php-mode-enable-project-coding-style is set, it is delayed by hook. - ;; Since it depends on the timing at which the file local variable is set. - ;; File local variables are set after initialization of major mode except `run-hook' is complete. - (if php-mode-enable-project-coding-style - (progn - (add-hook 'hack-local-variables-hook #'php-mode-set-style-delay t t) - (setq php-mode--delayed-set-style t) - (advice-add 'c-set-style :after #'php-mode--disable-delay-set-style)) - (let ((php-mode-enable-backup-style-variables nil)) - (php-set-style (symbol-name php-mode-coding-style)))) - - (when (or php-mode-force-pear - (and (stringp buffer-file-name) - (string-match "PEAR\\|pear" buffer-file-name) - (string-match "\\.php\\'" buffer-file-name))) - (php-set-style "pear")) - - (setq indent-line-function #'php-cautious-indent-line) - (setq indent-region-function #'php-cautious-indent-region) - (setq c-at-vsemi-p-fn #'php-c-at-vsemi-p) - (setq c-vsemi-status-unknown-p-fn #'php-c-vsemi-status-unknown-p) - - ;; We map the php-{beginning,end}-of-defun functions so that they - ;; replace the similar commands that we inherit from CC Mode. - ;; Because of our remapping we may not actually need to keep the - ;; following two local variables, but we keep them for now until we - ;; are completely sure their removal will not break any current - ;; behavior or backwards compatibility. - (setq-local beginning-of-defun-function #'php-beginning-of-defun) - (setq-local end-of-defun-function #'php-end-of-defun) - - (setq-local open-paren-in-column-0-is-defun-start nil) - (setq-local defun-prompt-regexp - "^\\s-*function\\s-+&?\\s-*\\(\\(\\sw\\|\\s_\\)+\\)\\s-*") - (setq-local add-log-current-defun-function nil) - (setq-local add-log-current-defun-header-regexp php-beginning-of-defun-regexp) - - (when (and (eval-when-compile (boundp 'flymake-diagnostic-functions)) - php-mode-replace-flymake-diag-function) - (add-hook 'flymake-diagnostic-functions php-mode-replace-flymake-diag-function nil t)) - - (advice-add 'c-looking-at-or-maybe-in-bracelist - :around 'php-c-looking-at-or-maybe-in-bracelist) - (when (fboundp 'c-looking-at-statement-block) - (advice-add 'c-looking-at-statement-block - :around 'php-c-looking-at-statement-block)) - (advice-add 'fixup-whitespace :after #'php-mode--fixup-whitespace-after) - - (advice-add 'acm-backend-tabnine-candidate-expand - :filter-args #'php-acm-backend-tabnine-candidate-expand-filter-args) - - (when (eval-when-compile (>= emacs-major-version 25)) - (syntax-ppss-flush-cache (point-min)))) - -(declare-function semantic-create-imenu-index "semantic/imenu" (&optional stream)) - -(defvar-mode-local php-mode imenu-create-index-function - (if php-do-not-use-semantic-imenu - #'imenu-default-create-index-function - (require 'semantic/imenu) - #'semantic-create-imenu-index) - "Imenu index function for PHP.") - -(when (bound-and-true-p consult-imenu-config) - (add-to-list 'consult-imenu-config '(php-mode :toplevel "Namespace" - :types ((?n "Namespace" font-lock-function-name-face) - (?p "Properties" font-lock-type-face) - (?o "Constants" font-lock-type-face) - (?c "Classes" font-lock-type-face) - (?f "Functions" font-lock-function-name-face) - (?i "Imports" font-lock-type-face) - (?m "Methods" font-lock-function-name-face))))) - -(autoload 'php-local-manual-complete-function "php-local-manual") - -(defun php-complete-function () - "Perform function completion on the text around point. -Completes to the set of names listed in the current tags table -and the standard php functions. -The string to complete is chosen in the same way as the default -for \\[find-tag] (which see)." - (interactive) - (php-local-manual-complete-function)) - -(defun php-show-arglist () - "Show function arguments at cursor position." - (interactive) - (let* ((tagname (php-get-pattern)) - (buf (find-tag-noselect tagname nil nil)) - arglist) - (with-current-buffer buf - (save-excursion - (goto-char (point-min)) - (when (re-search-forward - (format "function\\s-+%s\\s-*(\\([^{]*\\))" tagname) - nil t) - (setq arglist (buffer-substring-no-properties - (match-beginning 1) (match-end 1)))))) - (if arglist - (message "Arglist for %s: %s" tagname arglist) - (message "Unknown function: %s" tagname)))) - -;; Font Lock (defconst php-phpdoc-type-names '(;; PHPStan and Psalm types "__stringandstringable" "array" "array-key" "associative-array" "bool" "boolean" @@ -1330,8 +230,6 @@ for \\[find-tag] (which see)." "key-of" "value-of" "int-mask-of" "int-mask" "__benevolent" "template-type" "new") "A list of type and pseudotype names that can be used in PHPDoc.") -(make-obsolete-variable 'php-phpdoc-type-keywords 'php-phpdoc-type-names "1.24.2") - (defconst php-phpdoc-type-tags (list "package" "param" "property" "property-read" "property-write" "return" "throws" "var" "self-out" "this-out" "param-out" @@ -1359,23 +257,155 @@ for \\[find-tag] (which see)." ("^\\(?:/\\*\\)?\\(?:\\s \\|\\*\\)*\\(@[[:alpha:]][-[:alpha:]\\]*\\)" ; "@foo ..." markup. 1 'php-doc-annotation-tag prepend nil))) +(defun php--fontlock-doc-apply-highlight (highlight) + "Apply a single font-lock HIGHLIGHT form using the current match data. +HIGHLIGHT is (GROUP FACE OVERRIDE LAXMATCH) as in `font-lock-keywords'." + (let* ((group (nth 0 highlight)) + (face (eval (nth 1 highlight) t)) + (override (nth 2 highlight)) + (beg (match-beginning group)) + (end (match-end group))) + (when (and beg end) + (pcase override + ('prepend (font-lock-prepend-text-property beg end 'face face)) + ('append (font-lock-append-text-property beg end 'face face)) + ('t (put-text-property beg end 'face face)) + (_ (unless (get-text-property beg 'face) + (put-text-property beg end 'face face))))))) + +(defun php--fontlock-doc-comments (start-re limit keywords) + "Fontify PHPDoc comments up to LIMIT, replacing `c-font-lock-doc-comments'. +START-RE matches the beginning of a doc comment (e.g. \"/\\*\\*\"). +KEYWORDS is a list of font-lock matcher entries applied within the body +of each doc comment." + (let ((case-fold-search nil)) + (while (re-search-forward start-re limit t) + (let ((beg (match-beginning 0)) + ;; Probe *inside* the comment (its opener has just been + ;; consumed); `syntax-ppss' at the opening slash would still + ;; report being outside the comment. + (ppss (save-excursion (syntax-ppss (min limit (match-end 0))))) + end) + ;; Only treat this as a doc comment when the matched `/**' is + ;; itself the comment opener; a `/**' appearing *inside* another + ;; comment (e.g. the text of a `//' line comment) must be ignored. + (when (and (nth 4 ppss) (eql (nth 8 ppss) beg)) + (let ((cstart beg)) + (setq end (save-excursion + (goto-char cstart) + (if (re-search-forward "\\*/" limit t) (point) limit))) + ;; Give the whole doc comment the doc face (replacing the + ;; syntactic comment face), as CC Mode's + ;; `c-font-lock-doc-comments' did; the keyword highlights + ;; below then prepend their own faces on top. + (put-text-property cstart end 'face 'font-lock-doc-face) + (save-excursion + (save-restriction + (narrow-to-region cstart end) + (dolist (kw keywords) + (let ((matcher (car kw)) + (rest (cdr kw))) + (goto-char (point-min)) + (while (re-search-forward matcher nil t) + (if (numberp (car rest)) + (php--fontlock-doc-apply-highlight rest) + (dolist (h rest) + (php--fontlock-doc-apply-highlight h)))))))) + (goto-char end))))) + nil)) + (defvar php-phpdoc-font-lock-keywords `((,(lambda (limit) - (c-font-lock-doc-comments "/\\*\\*" limit - php-phpdoc-font-lock-doc-comments))))) + (php--fontlock-doc-comments "/\\*\\*" limit + php-phpdoc-font-lock-doc-comments))))) -(defconst php-font-lock-keywords-1 (c-lang-const c-matchers-1 php) - "Basic highlighting for PHP Mode.") +;;; Font-lock custom matchers -(defconst php-font-lock-keywords-2 (c-lang-const c-matchers-2 php) - "Medium level highlighting for PHP Mode.") +(defconst php-string-interpolated-variable-regexp + "{\\$[^}\n\\\\]*\\(?:\\\\.[^}\n\\\\]*\\)*}\\|\\${\\sw+}\\|\\$\\sw+") -(defconst php-font-lock-keywords-3 +(defun php-mode--error-control-op-font-lock-find (limit) + "Font-lock matcher for the error-control operator `@' up to LIMIT. +Match a single `@' used as the error-control operator, skipping +occurrences inside strings or comments." + (let (found) + (while (and (not found) + (re-search-forward "@" limit t)) + (unless (save-match-data (php-in-string-or-comment-p)) + (setq found t))) + found)) + +(defun php-mode--uppercase-constant-font-lock-find (limit) + "Font-lock matcher for ALL-UPPERCASE constants up to LIMIT. +Because `php-mode' fontifies with `case-fold-search' enabled (PHP +keywords are case-insensitive), a plain \"[A-Z_]\" regexp matcher would +also match lowercase identifiers. This matcher searches with +case-sensitivity forced on so that only genuinely upper-cased symbols +\(e.g. constant names) are matched into group 1." + (let ((case-fold-search nil)) + (re-search-forward "\\_<\\([A-Z_][A-Z0-9_]+\\)\\_>" limit t))) + +(defconst php-mode--non-type-word-re + (regexp-opt (append php-keywords--control-structures + php-keywords--declarations + php-keywords--statements + php-keywords--constants + '("class" "self" "parent" "static")) + 'symbols) + "Words that must never be fontified as a type even in type position.") + +(defun php-mode--type-word-p (str) + "Return non-nil when STR may denote a class/type name (not a keyword)." + (and str + (let ((case-fold-search t)) + (not (string-match-p (concat "\\`\\(?:" php-mode--non-type-word-re "\\)\\'") str))))) + +(defun php-mode--type-before-variable-font-lock-find (limit) + "Font-lock matcher for a class type hint written before a `$'-variable. +Matches an unqualified identifier immediately followed by an optional +reference `&' and a `$'-variable (e.g. \"stdClass $obj\"), skipping +language keywords such as `return' or `global'. The type name is left +in group 1. Namespaced type hints are handled by the namespace matcher." + (let (found) + (while (and (not found) + (re-search-forward + "\\<\\([[:alpha:]_][[:alnum:]_]*\\)[ \t]+&?\\$" limit t)) + (when (php-mode--type-word-p (match-string 1)) + (setq found t))) + found)) + +(defun php-mode--type-after-keyword-font-lock-find (limit) + "Font-lock matcher for an unqualified class name in type position. +Matches the identifier following `new', `instanceof', `extends' or +`implements' (e.g. \"new Foo\", \"extends Bar\"), leaving the class name +in group 1 and skipping constructs such as `new class'. Namespaced +names are handled by the namespace matcher." + (let (found) + (while (and (not found) + (re-search-forward + "\\<\\(?:new\\|instanceof\\|insteadof\\|extends\\|implements\\)[ \t]+\\([[:alpha:]_][[:alnum:]_]*\\)" + limit t)) + (when (php-mode--type-word-p (match-string 1)) + (setq found t))) + found)) + +(defun php-mode--string-interpolated-variable-font-lock-find (limit) + "Apply text-property to LIMIT for string interpolation by font-lock." + (let (quoted-stuff) + (while (re-search-forward php-string-interpolated-variable-regexp limit t) + (setq quoted-stuff (php-in-string-p)) + (when (or (eq ?\" quoted-stuff) (eq ?` quoted-stuff)) + (put-text-property (match-beginning 0) (match-end 0) 'face 'php-variable-name)))) + nil) + +;;; Font-lock keywords + +(defconst php-font-lock-keywords (append + ;; PHPDoc /** ... */ comments. php-phpdoc-font-lock-keywords - ;; php-mode patterns *before* cc-mode: - ;; only add patterns here if you want to prevent cc-mode from applying - ;; a different face. + + ;; Patterns that must win over the generic keyword matchers below. `( ;; Class declaration specification keywords (implements, extends) ("\\_<\\(?:implements\\|extends\\)\\_>" . 'php-class-declaration-spec) @@ -1410,6 +440,13 @@ for \\[find-tag] (which see)." ;; For 'array', there is an additional situation: ;; - when used as cast, so that (int) and (array) look the same ("(\\(array\\))" 1 font-lock-type-face) + ;; Type casts: (int), (binary), (unset), ... => type face. + (,(concat "(\\(" + (regexp-opt '("array" "binary" "bool" "boolean" "double" + "float" "int" "integer" "object" "real" + "string")) + "\\)\\s-*)") + 1 font-lock-type-face) (,(regexp-opt php-magical-constants 'symbols) (1 'php-magical-constant)) ;; namespaces @@ -1439,30 +476,49 @@ for \\[find-tag] (which see)." (,(rx (* (syntax whitespace)) "|" (* (syntax whitespace)) (group (+ (or (syntax word) (syntax symbol))) symbol-end)) nil nil (1 font-lock-type-face))) - ;; While c-opt-cpp-* highlights the " - "" ;; obsolete ASP tag - ;; Obsoleted tags were deleted in PHP 7. - ;; @see http://php.net/manual/language.basic-syntax.phptags.php - )) + ;; PHP open/close tags. `<\\?' followed by a word also matches + ;; pseudo-tags that are not authentic PHP open tags (e.g. `\\|<%\\|%>" 0 'php-php-tag)) - ;; cc-mode patterns - (c-lang-const c-matchers-3 php) - - ;; php-mode patterns *after* cc-mode: - ;; most patterns should go here, faces will only be applied if not - ;; already fontified by another pattern. Note that using OVERRIDE - ;; is usually overkill. + ;; Keyword matchers replacing what CC Mode used to fontify. + `( + ;; `__halt_compiler' is a language construct written with a call-like + ;; `()' suffix; fontify it before the generic function-call matcher. + ("\\_<__halt_compiler\\_>" 0 'php-keyword) + ;; true / false / null literals. + (,php-keywords--constants-re 1 'php-constant) + ;; Control structures, declarations and statements => php-keyword. + (,php-keywords--control-structures-re 1 'php-keyword) + (,php-keywords--declarations-re 1 'php-keyword) + (,php-keywords--statements-re 1 'php-keyword) + ;; `self', `parent' and `static' are class-relative pseudo-types but + ;; PHP Mode fontifies them as keywords; match them before the type + ;; names so they do not get the type face. + ("\\_<\\(self\\|parent\\|static\\)\\_>" 1 'php-keyword) + ;; Primitive / pseudo type names. + (,php-keywords--types-re 1 font-lock-type-face)) + + ;; Patterns applied last: only fill faces not already fontified. `( (php-mode--error-control-op-font-lock-find 0 'php-errorcontrol-op t) + ;; Class names in type position (type hints and new/extends/... ). + (php-mode--type-after-keyword-font-lock-find 1 'font-lock-type-face) + (php-mode--type-before-variable-font-lock-find 1 'font-lock-type-face) + ;; `namespace Foo;' / `namespace Foo\Bar;' declaration name. + ("\\_\\)\\s-*(" 1 php-function-call) - ;; Highlight all upper-cased symbols as constant - ("\\<\\([A-Z_][A-Z0-9_]+\\)\\>" 1 'php-constant) + ;; Highlight all upper-cased symbols as constant. Uses a matcher + ;; function so it stays case-sensitive even though font-lock runs + ;; with `case-fold-search' enabled for PHP's case-insensitive keywords. + (php-mode--uppercase-constant-font-lock-find 1 'php-constant) - ;; Highlight all statically accessed class names as constant, - ;; another valid option would be using type-face, but using - ;; constant-face because this is how it works in c++-mode. + ;; Highlight all statically accessed class names as constant. ("\\(\\sw+\\)\\(::\\)" (1 'php-constant) (2 'php-paamayim-nekudotayim)) ;; Highlight class name after "use .. as" ("\\")) 1 'php-builtin))) - "Detailed highlighting for PHP Mode.") + "Font-lock keywords for `php-mode'.") -(defvar php-font-lock-keywords php-font-lock-keywords-3 - "Default expressions to highlight in PHP Mode.") +;;; Navigation -(eval-when-compile - (unless (boundp 'flymake-proc-allowed-file-name-masks) - (add-to-list 'flymake-allowed-file-name-masks - '("\\.php[345s]?\\'" php-flymake-php-init)))) - - -(defun php-send-region (start end) - "Send the region between `START' and `END' to PHP for execution. -The output will appear in the buffer *PHP*." - (interactive "r") - (let ((php-buffer (get-buffer-create "*PHP*")) - (code (buffer-substring start end))) - ;; Calling 'php -r' will fail if we send it code that starts with - ;; ' arg 0) + (setq found-p (re-search-backward php-beginning-of-defun-regexp + nil 'noerror)) + (setq arg (1- arg))) + (while (< arg 0) + (end-of-line 1) + (let ((opoint (point))) + (beginning-of-defun 1) + (forward-list 2) + (forward-line 1) + (if (eq opoint (point)) + (setq found-p (re-search-forward php-beginning-of-defun-regexp + nil 'noerror))) + (setq arg (1+ arg)))) + (not (null found-p)))) -(defun php-mode--error-control-op-font-lock-find (limit) - "Font-lock matcher for the error-control operator `@' up to LIMIT. +(defun php-end-of-defun (&optional arg) + "Move the end of the ARGth PHP function from point. +Implements PHP version of `end-of-defun-function'. -Match a single `@' that is used as the error-control operator, skipping -occurrences inside strings or comments and the `@new' keyword (which CC -Mode fontifies as a keyword). CC Mode >= 31 fontifies a bare `@' as a -keyword because `@new' is registered in `c-type-list-kwds', so this -matcher is set to override that face." - (let (found) - (while (and (not found) - (re-search-forward "@" limit t)) - ;; `php-in-string-or-comment-p' calls `syntax-ppss', which may run - ;; `syntax-propertize' and clobber the match data, so guard it to - ;; keep the `@' match for both the `@new' check below and font-lock. - (unless (or (save-match-data (php-in-string-or-comment-p)) - (save-excursion - (goto-char (match-beginning 0)) - (looking-at-p "@new\\_>"))) - (setq found t))) - found)) +See `php-beginning-of-defun'." + (interactive "p") + (php-beginning-of-defun (- (or arg 1)))) + +;;; HTML template warning + +(defvar-local php-mode--warned-bad-indent nil + "Non-nil once the user has been warned about indenting this buffer.") + +(defun php-mode--check-html-for-indentation () + "Return non-nil when the current buffer may be indented as PHP. +Warn and offer to switch to `php-html-template-major-mode' when the +buffer looks like an HTML template edited in plain `php-mode'." + (cond + ((not php-mode-warn-if-html-template) t) + ((php-in-poly-php-html-mode) t) + ((not (php-buffer-has-html-tag)) t) + (php-mode--warned-bad-indent nil) + ((fboundp php-html-template-major-mode) + (if (y-or-n-p (format "This file seems to contain an HTML tag. Switch to %s? " + php-html-template-major-mode)) + (prog1 nil (funcall php-html-template-major-mode)) + (prog1 nil + (setq-local php-mode--warned-bad-indent t)))) + (t + (setq-local php-mode--warned-bad-indent t) + (lwarn 'php-mode + :warning "Indentation fails badly with mixed HTML/PHP in the HTML part in plain `php-mode'. +It is highly recommended to install a major mode that supports PHP and HTML templates, such as Web Mode. + +Set `php-html-template-major-mode' variable to use a mode other than `web-mode'. +Set `php-mode-warn-if-html-template' variable to nil to suppress the warning. +") + nil))) -(defun php-mode--string-interpolated-variable-font-lock-find (limit) - "Apply text-property to LIMIT for string interpolation by font-lock." - (let (quoted-stuff) - (while (re-search-forward php-string-interpolated-variable-regexp limit t) - (setq quoted-stuff (php-in-string-p)) - (when (or (eq ?\" quoted-stuff) (eq ?` quoted-stuff)) - (put-text-property (match-beginning 0) (match-end 0) 'face 'php-variable-name)))) - nil) - ;;; Correct the behavior of `delete-indentation' by modifying the ;;; logic of `fixup-whitespace'. -(defun php-mode--fixup-whitespace-after () - "Remove whitespace before certain characters in PHP Mode." +;; +;; NOTE: `php-cc-mode' installs an equivalent advice under the name +;; `php-mode--fixup-whitespace-after' guarded on `php-cc-mode'; this one +;; is guarded on `php-mode', so both can be loaded at the same time. +(defun php-mode--fixup-whitespace (&rest _args) + "Remove whitespace before `;', `,', `->' and `::' and after `->' and `::'." (when (and (derived-mode-p 'php-mode) (or (looking-at-p " \\(?:;\\|,\\|->\\|::\\)") (save-excursion @@ -1610,19 +662,116 @@ matcher is set to override that face." (looking-at-p "->\\|::")))) (delete-char 1))) -;; Advice for lsp-bridge' acm-backend-tabnine -;; see https://github.com/manateelazycat/lsp-bridge/issues/402#issuecomment-1305653058 -(defun php-acm-backend-tabnine-candidate-expand-filter-args (args) - "Adjust to replace bound-start ARGS for Tabnine in PHP." - (if (not (derived-mode-p 'php-mode)) - args - (cl-multiple-value-bind (candidate-info bound-start) args - (save-excursion - (goto-char bound-start) - (when (looking-at-p (eval-when-compile (regexp-quote "$"))) - (setq bound-start (1+ bound-start)))) - (list candidate-info bound-start)))) - +(defun php-mode--indent-line () + "Indent the current line, warning about HTML templates first." + (when (php-mode--check-html-for-indentation) + (php-indent-line))) + +(defun php-mode--indent-region (start end) + "Indent region START to END, warning about HTML templates first." + (when (php-mode--check-html-for-indentation) + (php-indent-region start end))) + +;;; Keymap + +(defvar php-mode-map + (let ((map (make-sparse-keymap))) + (define-key map (kbd "C-M-h") #'mark-defun) + (define-key map (kbd "C-c C-f") #'php-search-documentation) + (define-key map (kbd "C-c C-m") #'php-browse-manual) + (define-key map (kbd "C-c C-r") #'php-set-style) + map) + "Keymap for `php-mode'.") + +;;; Major mode + +;;;###autoload +(define-derived-mode php-mode php-base-mode "PHP" + "Major mode for editing PHP code, without a dependency on CC Mode. + +\\{php-mode-map}" + :syntax-table php-mode-syntax-table + ;; PHP keywords are case-insensitive. + (setq case-fold-search t) + + ;; Comments (newcomment.el). + (setq-local comment-start "// ") + (setq-local comment-end "") + (setq-local comment-start-skip + (eval-when-compile + (rx (group (or (: "#" (not (any "["))) + (: "/" (+ "/")) + (: "/*"))) + (* (syntax whitespace))))) + + ;; Paragraphs / page delimiter. + (setq-local page-delimiter php-mode-page-delimiter) + (setq-local parse-sexp-ignore-comments t) + + ;; Syntax propertize (heredoc/nowdoc, attributes, comment quotes). + (setq-local syntax-propertize-function #'php-syntax-propertize-function) + (add-hook 'syntax-propertize-extend-region-functions + #'php-syntax-propertize-extend-region t t) + + ;; Font-lock. + (with-suppressed-warnings ((obsolete font-lock-string-face + font-lock-keyword-face + font-lock-builtin-face + font-lock-function-name-face + font-lock-variable-name-face + font-lock-constant-face)) + (setq-local font-lock-string-face 'php-string) + (setq-local font-lock-keyword-face 'php-keyword) + (setq-local font-lock-builtin-face 'php-builtin) + (setq-local font-lock-function-name-face 'php-function-name) + (setq-local font-lock-variable-name-face 'php-variable-name) + (setq-local font-lock-constant-face 'php-constant)) + (setq font-lock-defaults '(php-font-lock-keywords nil t (("_$" . "w")) nil)) + + ;; Indentation. + (setq-local indent-line-function #'php-mode--indent-line) + (setq-local indent-region-function #'php-mode--indent-region) + (setq-local open-paren-in-column-0-is-defun-start nil) + (setq-local defun-prompt-regexp + "^\\s-*function\\s-+&?\\s-*\\(\\(\\sw\\|\\s_\\)+\\)\\s-*") + + ;; Navigation. + (setq-local beginning-of-defun-function #'php-beginning-of-defun) + (setq-local end-of-defun-function #'php-end-of-defun) + (setq-local add-log-current-defun-function nil) + (setq-local add-log-current-defun-header-regexp php-beginning-of-defun-regexp) + + ;; Imenu. + (setq imenu-generic-expression (if (symbolp php-imenu-generic-expression) + (symbol-value php-imenu-generic-expression) + php-imenu-generic-expression)) + (setq-local imenu-create-index-function #'imenu-default-create-index-function) + + ;; `delete-indentation' whitespace fixup for `->', `::', `;' and `,'. + (advice-add 'fixup-whitespace :after #'php-mode--fixup-whitespace) + + ;; Coding style (php-style.el): applies the style variables, honors a + ;; legacy buffer-local `c-basic-offset', and supports project styles. + (php-style-setup-buffer) + + (when (or php-mode-force-pear + (and (stringp buffer-file-name) + (string-match "PEAR\\|pear" buffer-file-name) + (string-match "\\.php\\'" buffer-file-name))) + (php-set-style "pear")) + + ;; Flymake. + (when (and (eval-when-compile (boundp 'flymake-diagnostic-functions)) + php-mode-replace-flymake-diag-function) + (add-hook 'flymake-diagnostic-functions php-mode-replace-flymake-diag-function nil t))) + +;;; Autoload registration + +;;;###autoload +(add-to-list 'interpreter-mode-alist + ;; Match php, php-3, php5, php7, php5.5, php-7.0.1, etc. + (cons "php\\(?:-?[34578]\\(?:\\.[0-9]+\\)*\\)?" 'php-mode)) + ;;;###autoload (progn (add-to-list 'auto-mode-alist '("/\\.php_cs\\(?:\\.dist\\)?\\'" . php-mode)) diff --git a/lisp/php-style.el b/lisp/php-style.el new file mode 100644 index 00000000..2fef0e2e --- /dev/null +++ b/lisp/php-style.el @@ -0,0 +1,266 @@ +;;; 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) + +;;; 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..76d0d187 100644 --- a/lisp/php.el +++ b/lisp/php.el @@ -27,16 +27,13 @@ ;; This file provides common variable and functions for PHP packages. -;; These functions are copied function from GNU Emacs. -;; -;; - c-end-of-token (cc-engine.el) -;; +;; This file has no dependency on CC Mode; the token scanning helpers +;; below are self-contained reimplementations of the small pieces of +;; cc-engine.el that PHP Mode used to rely on. ;;; Code: (eval-when-compile - (require 'cc-mode) (require 'cl-lib)) -(require 'cc-engine) (require 'flymake) (require 'php-core) (require 'php-keywords) @@ -243,6 +240,60 @@ out and get `php-default-major-mode' instead." :group 'php :tag "PHP Static Method Call" :type 'face) + +;;; Shared coding-style customization +;; +;; These options are shared between the cc-mode based `php-cc-mode' and the +;; cc-mode independent `php-mode', so they live in php.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 'per + "Select default coding style to use with `php-mode'. +This variable can take one of the following symbol values: + +`per' - PHP-FIG PER Coding Style (the modern default). +`psr2' - 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." + :group 'php + :tag "PHP Mode Coding Style" + :type '(choice (const :tag "PER" per) + (const :tag "PEAR" pear) + (const :tag "Drupal" drupal) + (const :tag "WordPress" wordpress) + (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 @@ -461,16 +512,56 @@ can be used to match against definitions for that classlike." (eval-when-compile (php-create-regexp-for-classlike (regexp-opt '("class" "interface" "trait" "enum"))))) +(defun php--make-base-syntax-table () + "Return a fresh syntax table describing PHP tokens. + +This is a hand-written table that does not depend on CC Mode's +`c-populate-syntax-table'. It is shared by `php-mode' (as the buffer +syntax table) and, with minor tweaks, by `php--analysis-syntax-table'." + (let ((table (make-syntax-table))) + ;; Identifier constituents. + (modify-syntax-entry ?_ "_" table) + (modify-syntax-entry ?$ "_" table) + ;; String / escape. PHP has single-quoted, double-quoted and + ;; backtick (shell-exec) string literals; all three are string + ;; delimiters. + (modify-syntax-entry ?` "\"" table) + (modify-syntax-entry ?\" "\"" table) + (modify-syntax-entry ?' "\"" table) + (modify-syntax-entry ?\\ "\\" table) + ;; Operators. `=' defaults to symbol-constituent syntax in Emacs's + ;; standard table, which would make it part of `\\_<...\\_>' tokens; + ;; PHP uses it purely as an operator, so mark it punctuation. + (modify-syntax-entry ?= "." table) + (modify-syntax-entry ?+ "." table) + (modify-syntax-entry ?- "." table) + (modify-syntax-entry ?% "." table) + (modify-syntax-entry ?< "." table) + (modify-syntax-entry ?> "." table) + (modify-syntax-entry ?& "." table) + (modify-syntax-entry ?| "." table) + ;; C-style comments: // and /* ... */. The `b' comment-style flag + ;; marks `//' (and `#') line comments so that they are distinguished + ;; from `/* */' block comments by `syntax-ppss' (element 7); it must + ;; sit on `/' and the newline, not on `*', mirroring the standard + ;; C/JavaScript syntax tables. + (modify-syntax-entry ?/ ". 124b" table) + (modify-syntax-entry ?* ". 23" table) + ;; Shell-style line comments: # ... \n (comment style b). + (modify-syntax-entry ?# "< b" table) + (modify-syntax-entry ?\n "> b" table) + table)) + +(defvar php--base-syntax-table (php--make-base-syntax-table) + "Syntax table describing PHP tokens, independent of CC Mode.") + (defvar php--analysis-syntax-table - (eval-when-compile - (let ((table (make-syntax-table))) - (c-populate-syntax-table table) - (modify-syntax-entry ?_ "w" table) - (modify-syntax-entry ?` "\"" table) - (modify-syntax-entry ?\" "\"" table) - (modify-syntax-entry ?# "< b" table) - (modify-syntax-entry ?\n "> b" table) - table))) + (let ((table (copy-syntax-table php--base-syntax-table))) + ;; For token analysis treat `_' and `$' as word constituents so that + ;; identifiers (and variables) scan as single words. + (modify-syntax-entry ?_ "w" table) + (modify-syntax-entry ?$ "w" table) + table)) (defun php-get-current-element (re-pattern) "Return backward matched element by RE-PATTERN." @@ -501,35 +592,86 @@ Prefer the enclosing string with fallback on sexp at point. (let ((bound (bounds-of-thing-at-point 'sexp))) (and bound (<= (car bound) (point)) (< (point) (cdr bound)) - bound)))))))) - (if (eval-when-compile (fboundp 'c-end-of-token)) - (defalias 'php--c-end-of-token #'c-end-of-token) - ;; Copyright (C) 1985, 1987, 1992-2022 Free Software Foundation, Inc. - ;; Follows function is copied from Emacs 27's cc-engine.el. - ;; https://emba.gnu.org/emacs/emacs/-/commit/95fb826dc58965eac287c0826831352edf2e56f7 - (defun php--c-end-of-token (&optional back-limit) - ;; Move to the end of the token we're just before or in the middle of. - ;; BACK-LIMIT may be used to bound the backward search; if given it's - ;; assumed to be at the boundary between two tokens. Return non-nil if the - ;; point is moved, nil otherwise. - ;; - ;; This function might do hidden buffer changes. - (let ((start (point))) - (cond ;; ((< (skip-syntax-backward "w_" (1- start)) 0) - ;; (skip-syntax-forward "w_")) - ((> (skip-syntax-forward "w_") 0)) - ((< (skip-syntax-backward ".()" back-limit) 0) - (while (< (point) start) - (if (looking-at c-nonsymbol-token-regexp) - (goto-char (match-end 0)) - ;; `c-nonsymbol-token-regexp' should always match since - ;; we've skipped backward over punctuation or paren - ;; syntax, but move forward in case it doesn't so that - ;; we don't leave point earlier than we started with. - (forward-char)))) - (t (if (looking-at c-nonsymbol-token-regexp) - (goto-char (match-end 0))))) - (> (point) start))))) + bound))))))))) + +;; The following three functions are self-contained reimplementations of +;; the token scanning behavior PHP Mode used to borrow from cc-engine.el +;; (`c-end-of-token', `c-beginning-of-current-token' and +;; `c-backward-token-2'). They operate under `php--analysis-syntax-table' +;; and use `php-re-token-symbols' as the operator/punctuation token regexp +;; (the analogue of cc-mode's `c-nonsymbol-token-regexp'). + +(defun php--end-of-token (&optional back-limit) + "Move to the end of the token point is before or in the middle of. +BACK-LIMIT bounds the backward search. Return non-nil if point moved." + (let ((start (point))) + (cond + ((> (skip-syntax-forward "w_") 0)) + ((< (skip-syntax-backward ".()" back-limit) 0) + (while (< (point) start) + (if (looking-at php-re-token-symbols) + (goto-char (match-end 0)) + ;; `php-re-token-symbols' should always match after skipping + ;; backward over punctuation or paren syntax, but move forward + ;; a character in case it doesn't so we never regress. + (forward-char)))) + (t (if (looking-at php-re-token-symbols) + (goto-char (match-end 0))))) + (> (point) start))) + +(defun php--beginning-of-current-token (&optional back-limit) + "Move to the beginning of the token point is in the middle of. +Do not move when not in the middle of a token. BACK-LIMIT bounds the +backward search. Return non-nil if point moved." + (let ((start (point))) + (if (looking-at "\\w\\|\\s_") + (skip-syntax-backward "w_" back-limit) + (when (< (skip-syntax-backward ".()" back-limit) 0) + (while (let ((pos (or (and (looking-at php-re-token-symbols) + (match-end 0)) + (1+ (point))))) + (if (<= pos start) + (goto-char pos)))))) + (< (point) start))) + +(defconst php--jump-syntax-token "\\w\\|\\s_\\|\\s\"\\|\\s|" + "Syntax classes jumped over as a balanced sexp by `php--backward-token'.") + +(defun php--backward-token (&optional limit) + "Move backward over one PHP token, skipping comments and whitespace. +LIMIT bounds the backward movement. Return 0 on success, otherwise the +number of tokens that could not be moved over." + (or limit (setq limit (point-min))) + (let ((last (point)) + (count 1)) + (condition-case nil + (while (and (> count 0) + (progn + (php--backward-syntactic-ws limit) + (backward-char) + (if (looking-at php--jump-syntax-token) + (goto-char (scan-sexps (1+ (point)) -1)) + (php--beginning-of-current-token)) + (>= (point) limit))) + (setq last (point) + count (1- count))) + (error (goto-char last))) + (when (< (point) limit) + (goto-char last)) + count)) + +(defun php--backward-syntactic-ws (&optional limit) + "Move backward over comments and whitespace, not past LIMIT." + (let ((limit (or limit (point-min))) + (moved t)) + (while (and moved (> (point) limit)) + (setq moved nil) + (when (< (skip-chars-backward " \t\n\r\f\v" limit) 0) + (setq moved t)) + (when (forward-comment -1) + (setq moved t))) + (when (< (point) limit) + (goto-char limit)))) (defun php-leading-tokens (length) "Return a list of leading LENGTH tokens from cursor point. @@ -544,7 +686,7 @@ The order is reversed by calling as follows: repeat length do (progn (forward-comment (- (point))) - (c-backward-token-2 1 nil)) + (php--backward-token)) collect (cond ((when-let* ((bounds (php--thing-at-point-bounds-of-string-at-point))) @@ -554,13 +696,39 @@ The order is reversed by calling as follows: (prog1 (match-string-no-properties 0) (goto-char (match-beginning 0)))) ((buffer-substring-no-properties (point) - (save-excursion (php--c-end-of-token) (point)))))))))) + (save-excursion (php--end-of-token) (point)))))))))) (defun php-get-pattern () "Find the pattern we want to complete. `find-tag-default' from GNU Emacs etags.el." (car (php-leading-tokens 1))) +(defun php-dot-context () + "Classify the context immediately before point for the \".\" key. + +Return one of the following symbols: + + `string-or-comment' -- point is inside a string or comment. + `next-to-string' -- the preceding token is a string literal or one of + `php-magical-constants' (for example, point follows + \"__DIR__\"), so a concatenation such as \". \" reads + naturally. + `code' -- anything else. + +This is a dependency-free primitive meant to drive context-sensitive +insertion of the \".\" key with packages such as smartchr or key-combo. +It shares its notion of \"string\" and \"magic constant\" with +`php-complete-complete-path', so key-driven insertion and +completion-at-point stay consistent. See the README for a recipe." + (cond + ((php-in-string-or-comment-p) 'string-or-comment) + ((when-let* ((token (car (php-leading-tokens 1)))) + (or (string-prefix-p "'" token) + (string-prefix-p "\"" token) + (member token php-magical-constants))) + 'next-to-string) + (t 'code))) + ;;; Provide support for Flymake so that users can see warnings and ;;; errors in real-time as they write code. (defun php-flymake-php-init () diff --git a/tests/8.1/readonly.php.faces b/tests/8.1/readonly.php.faces index d5526835..ef5fb1ec 100644 --- a/tests/8.1/readonly.php.faces +++ b/tests/8.1/readonly.php.faces @@ -63,11 +63,7 @@ ("__halt_compiler" . php-keyword) ("(); -") - ("claas" . font-lock-type-face) - (" ") - ("Err" . font-lock-variable-name-face) - (" +claas Err { ") ("/** Readonly property must have type */" . font-lock-doc-face) diff --git a/tests/8.4/property-hooks-indent.php b/tests/8.4/property-hooks-indent.php new file mode 100644 index 00000000..d85a3ca4 --- /dev/null +++ b/tests/8.4/property-hooks-indent.php @@ -0,0 +1,69 @@ +'. + * + * @see https://www.php.net/releases/8.4/en.php + */ + +class Person +{ + // A "virtual" property. It may not be set explicitly. + public string $fullName { // ###php-mode-test### ((indent 4)) + get => $this->firstName . ' ' . $this->lastName; // ###php-mode-test### ((indent 8)) + } // ###php-mode-test### ((indent 4)) + + public string $firstName { // ###php-mode-test### ((indent 4)) + set => ucfirst(strtolower($value)); // ###php-mode-test### ((indent 8)) + } // ###php-mode-test### ((indent 4)) + + public string $lastName { // ###php-mode-test### ((indent 4)) + set { // ###php-mode-test### ((indent 8)) + if (strlen($value) < 2) { // ###php-mode-test### ((indent 12)) + throw new \InvalidArgumentException('Too short'); // ###php-mode-test### ((indent 16)) + } // ###php-mode-test### ((indent 12)) + $this->lastName = $value; // ###php-mode-test### ((indent 12)) + } // ###php-mode-test### ((indent 8)) + } // ###php-mode-test### ((indent 4)) + + // Both hooks on one property. + public string $email { // ###php-mode-test### ((indent 4)) + get { // ###php-mode-test### ((indent 8)) + return strtolower($this->email); // ###php-mode-test### ((indent 12)) + } // ###php-mode-test### ((indent 8)) + set { // ###php-mode-test### ((indent 8)) + $this->email = trim($value); // ###php-mode-test### ((indent 12)) + } // ###php-mode-test### ((indent 8)) + } // ###php-mode-test### ((indent 4)) + + // A short `get' whose expression runs on past the first line. + public string $label { // ###php-mode-test### ((indent 4)) + get => $this->firstName // ###php-mode-test### ((indent 8)) + . ' ' // ###php-mode-test### ((indent 12)) + . $this->lastName; // ###php-mode-test### ((indent 12)) + } // ###php-mode-test### ((indent 4)) + + // An interface-style abstract hook. + abstract public string $slug { get; } // ###php-mode-test### ((indent 4)) + + // A hook with an explicit parameter type. + public string $nickname { // ###php-mode-test### ((indent 4)) + set(string $value) { // ###php-mode-test### ((indent 8)) + $this->nickname = $value; // ###php-mode-test### ((indent 12)) + } // ###php-mode-test### ((indent 8)) + } // ###php-mode-test### ((indent 4)) +} + +// Hooks in a constructor-promoted property. +class Point +{ + public function __construct( // ###php-mode-test### ((indent 4)) + public int $x { // ###php-mode-test### ((indent 8)) + set => abs($value); // ###php-mode-test### ((indent 12)) + }, // ###php-mode-test### ((indent 8)) + ) {} // ###php-mode-test### ((indent 4)) +} diff --git a/tests/8.5/attribute-expression.php b/tests/8.5/attribute-expression.php new file mode 100644 index 00000000..81a3873a --- /dev/null +++ b/tests/8.5/attribute-expression.php @@ -0,0 +1,60 @@ +user === $post->getAuthor(); // ###php-mode-test### ((indent 8)) + })] // ###php-mode-test### ((indent 4)) + public function update( // ###php-mode-test### ((indent 4)) + Request $request, // ###php-mode-test### ((indent 8)) + Post $post, // ###php-mode-test### ((indent 8)) + ): Response { // ###php-mode-test### ((indent 4)) + // ... // ###php-mode-test### ((indent 8)) + } // ###php-mode-test### ((indent 4)) +} + +// A first-class callable and an arrow function as arguments. +final class Handlers +{ + #[Listener(strlen(...))] // ###php-mode-test### ((indent 4)) + #[Validator(static fn(int $n): bool => $n > 0)] // ###php-mode-test### ((indent 4)) + public int $count = 0; // ###php-mode-test### ((indent 4)) +} + +// An attribute on a plain function, with a nested array argument. +#[Route('/posts', methods: [ // ###php-mode-test### ((indent 0)) + 'GET', // ###php-mode-test### ((indent 4)) + 'HEAD', // ###php-mode-test### ((indent 4)) +])] // ###php-mode-test### ((indent 0)) +function index(): Response // ###php-mode-test### ((indent 0)) +{ + return new Response(); // ###php-mode-test### ((indent 4)) +} diff --git a/tests/8.5/clone-with.php b/tests/8.5/clone-with.php new file mode 100644 index 00000000..e5e5885e --- /dev/null +++ b/tests/8.5/clone-with.php @@ -0,0 +1,43 @@ + $alpha, // ###php-mode-test### ((indent 12)) + ]); // ###php-mode-test### ((indent 8)) + } // ###php-mode-test### ((indent 4)) +} + +$blue = new Color(79, 91, 147); +$transparentBlue = $blue->withAlpha(128); + +// All on one line. +$copy = clone($blue, ['alpha' => 0]); // ###php-mode-test### ((indent 0)) + +// Plain `clone' keeps working. +$plain = clone $blue; // ###php-mode-test### ((indent 0)) + +// The argument list broken across lines. +$other = clone( + $blue, // ###php-mode-test### ((indent 4)) + [ // ###php-mode-test### ((indent 4)) + 'red' => 0, // ###php-mode-test### ((indent 8)) + 'green' => 0, // ###php-mode-test### ((indent 8)) + ], // ###php-mode-test### ((indent 4)) +); // ###php-mode-test### ((indent 0)) diff --git a/tests/8.5/pipe-operator.php b/tests/8.5/pipe-operator.php new file mode 100644 index 00000000..2f25668e --- /dev/null +++ b/tests/8.5/pipe-operator.php @@ -0,0 +1,40 @@ +'. + * + * @see https://www.php.net/releases/8.5/en.php + */ + +// The example from the release announcement. +$title = ' PHP 8.5 Released '; + +$slug = $title + |> trim(...) // ###php-mode-test### ((indent 4)) + |> (fn($str) => str_replace(' ', '-', $str)) // ###php-mode-test### ((indent 4)) + |> (fn($str) => str_replace('.', '', $str)) // ###php-mode-test### ((indent 4)) + |> strtolower(...); // ###php-mode-test### ((indent 4)) + +// A pipe starting on the same line as the assignment. +$result = $input |> trim(...) // ###php-mode-test### ((indent 0)) + |> strtolower(...); // ###php-mode-test### ((indent 4)) + +// Inside a function body. +function slugify(string $title): string +{ + return $title // ###php-mode-test### ((indent 4)) + |> trim(...) // ###php-mode-test### ((indent 8)) + |> strtolower(...); // ###php-mode-test### ((indent 8)) +} + +// Inside an argument list. +var_dump( + $title // ###php-mode-test### ((indent 4)) + |> trim(...) // ###php-mode-test### ((indent 8)) + |> strtolower(...), // ###php-mode-test### ((indent 8)) +); // ###php-mode-test### ((indent 0)) + +// Piping into a static method and a method call. +$out = $data + |> Formatter::normalize(...) // ###php-mode-test### ((indent 4)) + |> $encoder->encode(...); // ###php-mode-test### ((indent 4)) diff --git a/tests/indent/trailing-arrow.php b/tests/indent/trailing-arrow.php new file mode 100644 index 00000000..b1546e76 --- /dev/null +++ b/tests/indent/trailing-arrow.php @@ -0,0 +1,54 @@ +' left dangling at the end of a line. + * + * `=>' is excluded from `php-indent--indent-operator-re' because it is + * PHP's array key operator, so nothing treated the line after it as a + * continuation: an arrow function's body fell all the way back to + * column zero. + */ + +// An arrow function's body on the next line. +$f = fn($x) => + $x + 1; // ###php-mode-test### ((indent 4)) + +$g = static fn(int $n): bool => + $n > 0; // ###php-mode-test### ((indent 4)) + +// An array value on the next line. +$config = [ + 'key' => // ###php-mode-test### ((indent 4)) + 'a value long enough to wrap', // ###php-mode-test### ((indent 8)) + 'other' => 1, // ###php-mode-test### ((indent 4)) +]; // ###php-mode-test### ((indent 0)) + +// The opening bracket on the same line as the arrow: the value anchors +// to the line holding the bracket, not to the key's column. +$inline = ['key' => + 'value']; // ###php-mode-test### ((indent 4)) + +// A property hook's expression on the next line. +class Person +{ + public string $label { // ###php-mode-test### ((indent 4)) + get => // ###php-mode-test### ((indent 8)) + $this->firstName // ###php-mode-test### ((indent 12)) + . ' ' // ###php-mode-test### ((indent 12)) + . $this->lastName; // ###php-mode-test### ((indent 12)) + } // ###php-mode-test### ((indent 4)) +} + +// A match arm's result on the next line. +$label = match ($n) { + 1 => // ###php-mode-test### ((indent 4)) + 'one', // ###php-mode-test### ((indent 8)) + default => 'many', // ###php-mode-test### ((indent 4)) +}; // ###php-mode-test### ((indent 0)) + +// Tokens that end in `>' but are not `=>' keep their own handling. +if ($a >= + $b // ###php-mode-test### ((indent 4)) +) { + echo 1; // ###php-mode-test### ((indent 4)) +} diff --git a/tests/issue-136.php.faces b/tests/issue-136.php.faces index 0e5d6b71..a461a987 100644 --- a/tests/issue-136.php.faces +++ b/tests/issue-136.php.faces @@ -1,163 +1,145 @@ ;; -*- mode: emacs-lisp -*- (("\n * " . font-lock-doc-face) - ("@link" php-doc-annotation-tag font-lock-doc-face) - (" https://github.com/emacs-php/php-mode\n * " . font-lock-doc-face) - ("@package" php-doc-annotation-tag font-lock-doc-face) - (" " . font-lock-doc-face) - ("Emacs\\PHPMode" php-string font-lock-doc-face) - ("\n */" . font-lock-doc-face) + ("/**\n * GitHub Issue: https://github.com/emacs-php/php-mode/issues/136\n *\n * The code below contains strings with interpolated variables. PHP\n * Mode must use font-lock-variable-name-face to highlight all of\n * those variables. The page\n *\n * http://www.php.net/manual/en/language.types.string.php#language.types.string.parsing\n *\n * describes all of the variable syntaxes which PHP Mode must\n * recognize and highlight appropriately.\n * \n */" . font-lock-doc-face) ("\n\n") - ("// " . font-lock-comment-delimiter-face) - ("one-line comment\n" . font-lock-comment-face) - ("// " . font-lock-comment-delimiter-face) - ("@annotation This is NOT annotation. 1\n" . font-lock-comment-face) - ("\n") - ("/*" . font-lock-comment-delimiter-face) - ("------------------------------------------------\n Multi-line comment\n\n * @annotation This is NOT annotation. 2\n -------------------------------------------------*/" . font-lock-comment-face) - ("\n\n") - ("// " . font-lock-comment-delimiter-face) - ("/**\n" . font-lock-comment-face) - ("// " . font-lock-comment-delimiter-face) - ("* Comment outed class implementation\n" . font-lock-comment-face) - ("// " . font-lock-comment-delimiter-face) - ("*\n" . font-lock-comment-face) - ("// " . font-lock-comment-delimiter-face) - ("* @annotation This is NOT annotation. 3\n" . font-lock-comment-face) - ("// " . font-lock-comment-delimiter-face) - ("*/\n" . font-lock-comment-face) - ("// " . font-lock-comment-delimiter-face) - ("class CommentOuted\n" . font-lock-comment-face) - ("// " . font-lock-comment-delimiter-face) - ("{\n" . font-lock-comment-face) - ("// " . font-lock-comment-delimiter-face) - ("}\n" . font-lock-comment-face) - ("\n") - ("/**\n * Class level doc-comment\n *\n * Description " . font-lock-doc-face) - ("{@internal " php-doc-annotation-tag font-lock-doc-face) - ("Description" php-string php-doc-annotation-tag font-lock-doc-face) - ("}" php-doc-annotation-tag font-lock-doc-face) - (" inline tag.\n *\n * " . font-lock-doc-face) - ("@property-read" php-doc-annotation-tag font-lock-doc-face) - (" " . font-lock-doc-face) - ("string" font-lock-type-face php-string font-lock-doc-face) - ("[]" php-string font-lock-doc-face) - (" " . font-lock-doc-face) - ("$" php-doc-variable-sigil font-lock-doc-face) - ("name" php-variable-name font-lock-doc-face) - ("\n * " . font-lock-doc-face) - ("@ORM\\Table" php-doc-annotation-tag font-lock-doc-face) - ("(name=\"majormodes\")\n * " . font-lock-doc-face) - ("@ORM\\Entity" php-doc-annotation-tag font-lock-doc-face) - ("(repositoryClass=\"Emacs\\Repository\\MajorModeRepository\")\n */" . font-lock-doc-face) - ("\n") - ("final" . php-keyword) + ("$" . php-variable-sigil) + ("name" . php-variable-name) (" ") + ("=" . php-assignment-op) + (" ") + ("\"Eric\"" . php-string) + (";\n\n") ("class" . php-class-declaration) (" ") - ("SampleClass" . font-lock-type-face) - ("\n{\n ") - ("/** Const doc-comment */" . font-lock-doc-face) - ("\n ") - ("const" . php-keyword) - (" ") - ("SAMPLE" . font-lock-type-face) - (" = ") - ("'SAMPLE'" . php-string) - (";\n ") - ("/** " . font-lock-doc-face) - ("@var" php-doc-annotation-tag font-lock-doc-face) - (" " . font-lock-doc-face) - ("string" font-lock-type-face php-string font-lock-doc-face) - (" sample property doc-comment */" . font-lock-doc-face) - ("\n ") - ("private" . php-keyword) + ("User" . font-lock-type-face) + ("\n{\n ") + ("public" . php-keyword) (" ") ("$" . php-variable-sigil) ("name" . php-variable-name) - (";\n\n ") - ("/**\n * " . font-lock-doc-face) - ("@param" php-doc-annotation-tag font-lock-doc-face) - (" " . font-lock-doc-face) - ("string" font-lock-type-face php-string font-lock-doc-face) - (" " . font-lock-doc-face) - ("$" php-doc-variable-sigil font-lock-doc-face) - ("name" php-variable-name font-lock-doc-face) - ("\n */" . font-lock-doc-face) - ("\n ") + (";\n ") + ("public" . php-keyword) + (" ") + ("$" . php-variable-sigil) + ("id" . php-variable-name) + (" ") + ("=" . php-assignment-op) + (" 0;\n\n ") ("public" . php-keyword) (" ") ("function" . php-keyword) (" ") - ("__construct" . php-function-name) - ("(") - ("$" . php-variable-sigil) - ("name" . php-variable-name) - (")\n {\n ") + ("getName" . php-function-name) + ("()\n {\n ") + ("return" . php-keyword) + (" ") ("$" . php-this-sigil) ("this" . php-this) ("->" . php-object-op) ("name" . php-property-name) - (" = ") - ("$" . php-variable-sigil) - ("name" . php-variable-name) - ("; ") - ("// " . font-lock-comment-delimiter-face) - ("comment in after code\n" . font-lock-comment-face) - ("\n ") - ("/** " . font-lock-doc-face) - ("@var" php-doc-annotation-tag font-lock-doc-face) - (" " . font-lock-doc-face) - ("string" font-lock-type-face php-string font-lock-doc-face) - ("|" php-string font-lock-doc-face) - ("bool" font-lock-type-face php-string font-lock-doc-face) - ("|" php-string font-lock-doc-face) - ("array" font-lock-type-face php-string font-lock-doc-face) - ("[]|ArrayObject" php-string font-lock-doc-face) - (" */" . font-lock-doc-face) - ("\n ") - ("$" . php-variable-sigil) - ("foo" . php-variable-name) - (" = hoge();\n\n ") - ("// " . font-lock-comment-delimiter-face) - ("one-line comment\n" . font-lock-comment-face) - (" ") - ("// " . font-lock-comment-delimiter-face) - ("@annotation This is NOT annotation. 4\n" . font-lock-comment-face) - ("\n ") - ("/** " . font-lock-doc-face) - ("@var" php-doc-annotation-tag font-lock-doc-face) - (" " . font-lock-doc-face) - ("int" font-lock-type-face php-string font-lock-doc-face) - (" internal linter variable */" . font-lock-doc-face) - ("\n ") - ("$" . php-variable-sigil) - ("offset" . php-variable-name) - (" = 0;\n }\n\n ") - ("/**\n * Summary\n *\n * " . font-lock-doc-face) - ("@throws" php-doc-annotation-tag font-lock-doc-face) - (" " . font-lock-doc-face) - ("\\RuntimeException" php-string font-lock-doc-face) - ("\n */" . font-lock-doc-face) - ("\n ") + (";\n }\n\n ") ("public" . php-keyword) (" ") ("function" . php-keyword) (" ") - ("test" . php-function-name) - ("()\n {\n ") - ("throw" . php-keyword) + ("getID" . php-function-name) + ("()\n {\n ") + ("return" . php-keyword) + (" ") + ("$" . php-this-sigil) + ("this" . php-this) + ("->" . php-object-op) + ("id" . php-property-name) + (";\n }\n}\n\n") + ("$" . php-variable-sigil) + ("user" . php-variable-name) + (" ") + ("=" . php-assignment-op) (" ") ("new" . php-keyword) (" ") - ("\\RuntimeException" . font-lock-type-face) - (";\n }\n}\n")) + ("User" . font-lock-type-face) + ("();\n") + ("$" . php-variable-sigil) + ("user" . php-variable-name) + ("->" . php-object-op) + ("name" . php-property-name) + (" ") + ("=" . php-assignment-op) + (" ") + ("\"Eric\"" . php-string) + (";\n\n") + ("$" . php-variable-sigil) + ("users" . php-variable-name) + (" ") + ("=" . php-assignment-op) + (" ") + ("array" . php-keyword) + ("(") + ("$" . php-variable-sigil) + ("user" . php-variable-name) + (");\n") + ("$" . php-variable-sigil) + ("index" . php-variable-name) + (" ") + ("=" . php-assignment-op) + (" 0;\n\n") + ("ob_start" . php-function-call-traditional) + ("();\n\n") + ("echo" . php-keyword) + (" ") + ("\"My name is " . php-string) + ("$name" . php-variable-name) + ("\"" . php-string) + (";\n") + ("echo" . php-keyword) + (" ") + ("\"My name is " . php-string) + ("${name}" . php-variable-name) + ("\"" . php-string) + (";\n") + ("echo" . php-keyword) + (" ") + ("\"My name is " . php-string) + ("{$name}" . php-variable-name) + ("\"" . php-string) + (";\n") + ("echo" . php-keyword) + (" ") + ("\"My name is " . php-string) + ("{$user->name}" . php-variable-name) + ("\"" . php-string) + (";\n") + ("echo" . php-keyword) + (" ") + ("\"My name is " . php-string) + ("{$user->getName()}" . php-variable-name) + ("\"" . php-string) + (";\n") + ("echo" . php-keyword) + (" ") + ("\"My name is " . php-string) + ("{$users[0]->name}" . php-variable-name) + ("\"" . php-string) + (";\n") + ("echo" . php-keyword) + (" ") + ("\"My name is " . php-string) + ("{$users[$index]->name}" . php-variable-name) + ("\"" . php-string) + (";\n") + ("echo" . php-keyword) + (" ") + ("\"My name is " . php-string) + ("{$users[$user->id]->name}" . php-variable-name) + ("\"" . php-string) + (";\n") + ("echo" . php-keyword) + (" ") + ("\"My name is " . php-string) + ("{$users[$user->getID()]->name}" . php-variable-name) + ("\"" . php-string) + (";\n\n") + ("ob_end_clean" . php-function-call-traditional) + ("();\n")) diff --git a/tests/issue-443.php.27.faces b/tests/issue-443.php.27.faces deleted file mode 100644 index f6842d29..00000000 --- a/tests/issue-443.php.27.faces +++ /dev/null @@ -1,81 +0,0 @@ -;; -*- mode: emacs-lisp -*- -(("" . php-php-tag) - ("\n") - ("/** GitHub Issue: https://github.com/emacs-php/php-mode/issues/443 */" . font-lock-doc-face) - ("\n") - ("" . php-php-tag) - ("\n") - ("" . php-php-tag) - ("\n") - ("" . php-php-tag) - ("\n") - ("<%" . php-php-tag) - (" ") - ("echo" . php-keyword) - (" ") - ("\"obsolete ASP tags\"" . php-string) - (" ") - ("%>" . php-php-tag) - ("\n") - ("" . php-php-tag) - ("\n") - ("" . php-php-tag) - ("\n") - ("" . php-php-tag) - ("\n") - ("" . php-php-tag) - ("\n")) diff --git a/tests/lang/doc-comment/comments.php.27.faces b/tests/lang/doc-comment/comments.php.27.faces index 70c23fb3..52340e97 100644 --- a/tests/lang/doc-comment/comments.php.27.faces +++ b/tests/lang/doc-comment/comments.php.27.faces @@ -1,32 +1,31 @@ ;; -*- mode: emacs-lisp -*- -(("\n * " . font-lock-doc-face) ("@link" php-doc-annotation-tag font-lock-doc-face) - (" https://github.com/emacs-php/php-mode\n * " . font-lock-doc-face) + (" https://github.com/emacs-php/php-mode\n * " + . font-lock-doc-face) ("@package" php-doc-annotation-tag font-lock-doc-face) (" " . font-lock-doc-face) ("Emacs\\PHPMode" php-string font-lock-doc-face) - ("\n */" . font-lock-doc-face) - ("\n\n") + ("\n */" . font-lock-doc-face) ("\n\n") ("// " . font-lock-comment-delimiter-face) ("one-line comment\n" . font-lock-comment-face) ("// " . font-lock-comment-delimiter-face) ("@annotation This is NOT annotation. 1\n" . font-lock-comment-face) - ("\n") - ("/*" . font-lock-comment-delimiter-face) - ("------------------------------------------------\n Multi-line comment\n\n * @annotation This is NOT annotation. 2\n -------------------------------------------------" . font-lock-comment-face) - ("*/" . font-lock-comment-delimiter-face) - ("\n\n") - ("// " . font-lock-comment-delimiter-face) + ("\n") ("/*" . font-lock-comment-delimiter-face) + ("------------------------------------------------\n Multi-line comment\n\n * @annotation This is NOT annotation. 2\n -------------------------------------------------*/" + . font-lock-comment-face) + ("\n\n") ("// " . font-lock-comment-delimiter-face) ("/**\n" . font-lock-comment-face) ("// " . font-lock-comment-delimiter-face) ("* Comment outed class implementation\n" . font-lock-comment-face) @@ -41,9 +40,9 @@ ("// " . font-lock-comment-delimiter-face) ("{\n" . font-lock-comment-face) ("// " . font-lock-comment-delimiter-face) - ("}\n" . font-lock-comment-face) - ("\n") - ("/**\n * Class level doc-comment\n *\n * Description " . font-lock-doc-face) + ("}\n" . font-lock-comment-face) ("\n") + ("/**\n * Class level doc-comment\n *\n * Description " + . font-lock-doc-face) ("{@internal " php-doc-annotation-tag font-lock-doc-face) ("Description" php-string php-doc-annotation-tag font-lock-doc-face) ("}" php-doc-annotation-tag font-lock-doc-face) @@ -51,43 +50,28 @@ ("@property-read" php-doc-annotation-tag font-lock-doc-face) (" " . font-lock-doc-face) ("string" font-lock-type-face php-string font-lock-doc-face) - ("[]" php-string font-lock-doc-face) - (" " . font-lock-doc-face) + ("[]" php-string font-lock-doc-face) (" " . font-lock-doc-face) ("$" php-doc-variable-sigil font-lock-doc-face) ("name" php-variable-name font-lock-doc-face) ("\n * " . font-lock-doc-face) ("@ORM\\Table" php-doc-annotation-tag font-lock-doc-face) ("(name=\"majormodes\")\n * " . font-lock-doc-face) ("@ORM\\Entity" php-doc-annotation-tag font-lock-doc-face) - ("(repositoryClass=\"Emacs\\Repository\\MajorModeRepository\")\n */" . font-lock-doc-face) - ("\n") - ("final" . php-class-modifier) - (" ") - ("class" . php-class-declaration) - (" ") - ("SampleClass" . font-lock-type-face) - ("\n{\n ") - ("/** Const doc-comment */" . font-lock-doc-face) - ("\n ") - ("const" . php-keyword) - (" ") - ("SAMPLE" . php-constant-assign) - (" ") - ("=" . php-assignment-op) - (" ") - ("'SAMPLE'" . php-string) - (";\n ") + ("(repositoryClass=\"Emacs\\Repository\\MajorModeRepository\")\n */" + . font-lock-doc-face) + ("\n") ("final" . php-class-modifier) (" ") + ("class" . php-class-declaration) (" ") + ("SampleClass" . font-lock-type-face) ("\n{\n ") + ("/** Const doc-comment */" . font-lock-doc-face) ("\n ") + ("const" . php-keyword) (" ") ("SAMPLE" . php-constant-assign) (" ") + ("=" . php-assignment-op) (" ") ("'SAMPLE'" . php-string) (";\n ") ("/** " . font-lock-doc-face) ("@var" php-doc-annotation-tag font-lock-doc-face) (" " . font-lock-doc-face) ("string" font-lock-type-face php-string font-lock-doc-face) - (" sample property doc-comment */" . font-lock-doc-face) - ("\n ") - ("private" . php-keyword) - (" ") - ("$" . php-variable-sigil) - ("name" . php-variable-name) - (";\n\n ") + (" sample property doc-comment */" . font-lock-doc-face) ("\n ") + ("private" . php-keyword) (" ") ("$" . php-variable-sigil) + ("name" . php-variable-name) (";\n\n ") ("/**\n * " . font-lock-doc-face) ("@param" php-doc-annotation-tag font-lock-doc-face) (" " . font-lock-doc-face) @@ -95,30 +79,15 @@ (" " . font-lock-doc-face) ("$" php-doc-variable-sigil font-lock-doc-face) ("name" php-variable-name font-lock-doc-face) - ("\n */" . font-lock-doc-face) - ("\n ") - ("public" . php-keyword) - (" ") - ("function" . php-keyword) - (" ") - ("__construct" . php-function-name) - ("(") - ("$" . php-variable-sigil) - ("name" . php-variable-name) - (")\n {\n ") - ("$" . php-this-sigil) - ("this" . php-this) - ("->" . php-object-op) - ("name" . php-property-name) - (" ") - ("=" . php-assignment-op) - (" ") - ("$" . php-variable-sigil) - ("name" . php-variable-name) - ("; ") + ("\n */" . font-lock-doc-face) ("\n ") + ("public" . php-keyword) (" ") ("function" . php-keyword) (" ") + ("__construct" . php-function-name) ("(") ("$" . php-variable-sigil) + ("name" . php-variable-name) (")\n {\n ") + ("$" . php-this-sigil) ("this" . php-this) ("->" . php-object-op) + ("name" . php-property-name) (" ") ("=" . php-assignment-op) (" ") + ("$" . php-variable-sigil) ("name" . php-variable-name) ("; ") ("// " . font-lock-comment-delimiter-face) - ("comment in after code\n" . font-lock-comment-face) - ("\n ") + ("comment in after code\n" . font-lock-comment-face) ("\n ") ("/** " . font-lock-doc-face) ("@var" php-doc-annotation-tag font-lock-doc-face) (" " . font-lock-doc-face) @@ -128,47 +97,27 @@ ("|" php-string font-lock-doc-face) ("array" font-lock-type-face php-string font-lock-doc-face) ("[]|ArrayObject" php-string font-lock-doc-face) - (" */" . font-lock-doc-face) - ("\n ") - ("$" . php-variable-sigil) - ("foo" . php-variable-name) - (" ") - ("=" . php-assignment-op) - (" ") - ("hoge" . php-function-call-traditional) - ("();\n\n ") + (" */" . font-lock-doc-face) ("\n ") + ("$" . php-variable-sigil) ("foo" . php-variable-name) (" ") + ("=" . php-assignment-op) (" ") + ("hoge" . php-function-call-traditional) ("();\n\n ") ("// " . font-lock-comment-delimiter-face) - ("one-line comment\n" . font-lock-comment-face) - (" ") + ("one-line comment\n" . font-lock-comment-face) (" ") ("// " . font-lock-comment-delimiter-face) ("@annotation This is NOT annotation. 4\n" . font-lock-comment-face) - ("\n ") - ("/** " . font-lock-doc-face) + ("\n ") ("/** " . font-lock-doc-face) ("@var" php-doc-annotation-tag font-lock-doc-face) (" " . font-lock-doc-face) ("int" font-lock-type-face php-string font-lock-doc-face) - (" internal linter variable */" . font-lock-doc-face) - ("\n ") - ("$" . php-variable-sigil) - ("offset" . php-variable-name) - (" ") - ("=" . php-assignment-op) - (" 0;\n }\n\n ") + (" internal linter variable */" . font-lock-doc-face) ("\n ") + ("$" . php-variable-sigil) ("offset" . php-variable-name) (" ") + ("=" . php-assignment-op) (" 0;\n }\n\n ") ("/**\n * Summary\n *\n * " . font-lock-doc-face) ("@throws" php-doc-annotation-tag font-lock-doc-face) (" " . font-lock-doc-face) ("\\RuntimeException" php-string font-lock-doc-face) - ("\n */" . font-lock-doc-face) - ("\n ") - ("public" . php-keyword) - (" ") - ("function" . php-keyword) - (" ") - ("test" . php-function-name) - ("()\n {\n ") - ("throw" . php-keyword) - (" ") - ("new" . php-keyword) - (" ") - ("\\RuntimeException" . font-lock-type-face) - (";\n }\n}\n")) + ("\n */" . font-lock-doc-face) ("\n ") + ("public" . php-keyword) (" ") ("function" . php-keyword) (" ") + ("test" . php-function-name) ("()\n {\n ") + ("throw" . php-keyword) (" ") ("new" . php-keyword) (" ") + ("\\RuntimeException" . font-lock-type-face) (";\n }\n}\n")) diff --git a/tests/lang/errorcontrol.php.faces b/tests/lang/errorcontrol.php.faces index d8e59455..74a43e16 100644 --- a/tests/lang/errorcontrol.php.faces +++ b/tests/lang/errorcontrol.php.faces @@ -1,33 +1,12 @@ ;; -*- mode: emacs-lisp -*- -((" +;; Maintainer: USAMI Kenta +;; URL: https://github.com/emacs-php/php-mode +;; License: GPL-3.0-or-later + +;;; License + +;; 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: + +;; Regression tests for the frozen CC Mode based `php-cc-mode' (the old +;; `php-mode' implementation). These mirror a representative subset of +;; the indentation and font-lock cases from `php-mode-test.el' but drive +;; `php-cc-mode' instead of the new cc-mode independent `php-mode', so we +;; keep exercising the legacy mode while both major modes coexist. +;; +;; The test fixtures under tests/ are shared with `php-mode-test.el'. +;; They were originally generated from CC Mode, so the `.faces' files +;; used here are the ones that were NOT regenerated for the new engine. + +;;; Code: +(require 'php-cc-mode) +(require 'ert) +(require 'cl-lib) +(require 'imenu) + +;; Work around bug #14325 +;; . +(c-after-font-lock-init) + +(defvar php-cc-mode-test-dir (if load-file-name + (file-name-directory load-file-name) + default-directory) + "Directory containing the `php-cc-mode' test files.") + +(defvar php-cc-mode-test-valid-magics '(indent) + "List of allowed \"magic\" directives which can appear in test cases.") + +(defvar php-cc-mode-test-magic-regexp "###php-mode-test### \\((.+)\\)" + "Regexp which identifies a magic comment.") + +(defun php-cc-mode-test-process-magics () + "Process the ###php-mode-test### directives in the current buffer." + (cl-letf (((symbol-function 'indent) + (lambda (offset) + (let ((current-offset (current-indentation))) + (unless (eq current-offset offset) + (list :line (line-number-at-pos) + :expected offset + :actual current-offset)))))) + (let (directives) + (save-excursion + (goto-char (point-min)) + (cl-loop while (re-search-forward php-cc-mode-test-magic-regexp nil t) + for directives = (read (buffer-substring (match-beginning 1) (match-end 1))) + for result = (mapcar (lambda (expr) + (let ((fn (car expr)) + (args (mapcar 'eval (cdr-safe expr)))) + (if (memq fn php-cc-mode-test-valid-magics) + (apply fn args)))) + directives) + append (cl-remove-if #'null result)))))) + +(defun php-cc-mode-test--buffer-face-list (buffer) + "Return list of (STRING . FACE) from BUFFER." + (with-temp-buffer + (jit-lock-mode -1) + (insert (with-current-buffer buffer (buffer-substring (point-min) (point-max)))) + (goto-char (point-min)) + (let (retval begin-pos last-face current-face str) + (setq last-face (get-text-property (point) 'face)) + (setq begin-pos (point)) + (forward-char 1) + (while (< (point) (point-max)) + (setq current-face (get-text-property (point) 'face)) + (unless (equal current-face last-face) + (setq str (buffer-substring-no-properties begin-pos (point))) + (setq retval (nconc retval (list (cons str last-face)))) + (setq begin-pos (point)) + (setq last-face current-face)) + (forward-char 1)) + (setq str (buffer-substring-no-properties begin-pos (point))) + (nconc retval (list (cons str last-face)))))) + +(defun php-cc-mode-test--parse-list-file (file-path) + "Return list from FILE-PATH." + (with-temp-buffer + (insert-file-contents file-path) + (let ((read-circle t)) + (read (current-buffer))))) + +(cl-defmacro with-php-cc-mode-test ((file &key style indent magic faces) &rest body) + "Set up environment for testing `php-cc-mode'. +Execute BODY in a temporary buffer containing the contents of FILE, in +`php-cc-mode'. The keyword arguments mirror `with-php-mode-test' but +drive the CC Mode based mode and its `php-cc-set-style'." + (declare (indent 1)) + `(with-temp-buffer + (insert-file-contents (expand-file-name ,file php-cc-mode-test-dir)) + (setq default-directory + (expand-file-name ".." (expand-file-name ,file php-cc-mode-test-dir))) + (php-cc-mode) + ,(if (fboundp 'font-lock-ensure) + '(font-lock-ensure) + '(with-no-warnings (font-lock-fontify-buffer))) + ,(cl-case style + (pear '(php-cc-set-style "pear")) + (drupal '(php-cc-set-style "drupal")) + (wordpress '(php-cc-set-style "wordpress")) + (psr2 '(php-cc-set-style "psr2")) + (t '(php-cc-set-style "php"))) + ,(if indent + '(let ((inhibit-message t)) (indent-region (point-min) (point-max)))) + ,(if magic + `(should (equal + (cons ,file nil) + (cons ,file (php-cc-mode-test-process-magics))))) + ,(if faces + `(should (equal + (cons ,file + (php-cc-mode-test--parse-list-file + (concat (expand-file-name ,file php-cc-mode-test-dir) + (if (eq t ,faces) ".faces" ,faces)))) + (cons ,file + (php-cc-mode-test--buffer-face-list (current-buffer)))))) + (goto-char (point-min)) + (let ((case-fold-search nil)) + ,@body))) + +;;; Indentation regression tests + +(ert-deftest php-cc-mode-test-namespace-block () + "Proper indentation for classes and functions in a namespace block." + (with-php-cc-mode-test ("namespace-block.php" :indent t :magic t))) + +(ert-deftest php-cc-mode-test-issue-14 () + "Array indentation." + (with-php-cc-mode-test ("issue-14.php" :indent t :magic t))) + +(ert-deftest php-cc-mode-test-issue-18 () + "Indentation of strings which include \"//\"." + (with-php-cc-mode-test ("issue-18.php" :indent t :magic t))) + +(ert-deftest php-cc-mode-test-issue-29 () + "Indentation of anonymous functions as arguments." + (with-php-cc-mode-test ("issue-29.php" :indent t :magic t))) + +(ert-deftest php-cc-mode-test-issue-99 () + "Proper indentation for `foreach' statements without braces." + (with-php-cc-mode-test ("issue-99.php" :indent t :magic t))) + +(ert-deftest php-cc-mode-test-issue-186 () + "Indentation of switch case body preceded by multiple case statements." + (with-php-cc-mode-test ("issue-186.php" :indent t :magic t))) + +(ert-deftest php-cc-mode-test-switch-statements () + "Test indentation inside switch statements." + (with-php-cc-mode-test ("switch-statements.php" :indent t :style pear) + (search-forward "case true:") + (should (eq (current-indentation) 0))) + (with-php-cc-mode-test ("switch-statements.php" :indent t :style psr2) + (search-forward "case true:") + (should (eq (current-indentation) c-basic-offset)))) + +;;; Font-lock regression tests +;; +;; These use `.faces' fixtures that were NOT regenerated for the new +;; engine, so they still describe CC Mode's output. + +(ert-deftest php-cc-mode-test-constants () + "Proper highlighting for constants." + (with-php-cc-mode-test ("constants.php" :faces t))) + +(ert-deftest php-cc-mode-test-identifiers () + "Proper highlighting for identifiers including their namespace." + (with-php-cc-mode-test ("identifiers.php" :faces t))) + +(ert-deftest php-cc-mode-test-variables () + "Proper highlighting for variables." + (with-php-cc-mode-test ("variables.php" :faces t))) + +(ert-deftest php-cc-mode-test-arrays () + "Proper highlighting for the array keyword." + (with-php-cc-mode-test ("arrays.php" :faces t))) + +(ert-deftest php-cc-mode-test-static-method-calls () + "Highlighting of static method calls named the same as a keyword." + (with-php-cc-mode-test ("static-method-calls.php" :faces t))) + +(ert-deftest php-cc-mode-test-issue-305 () + "Highlighting of variables which contain `this' or `that'." + (with-php-cc-mode-test ("issue-305.php" :faces t))) + +(ert-deftest php-cc-mode-test-issue-197 () + "Highlighting of member and function names (should not have type face)." + (with-php-cc-mode-test ("issue-197.php" :faces t))) + +(ert-deftest php-cc-mode-test-issue-201 () + "Highlighting of special variables." + (with-php-cc-mode-test ("issue-201.php" :faces t))) + +;;; php-mode-debug (CC Mode specific) ----------------------------------------- + +(ert-deftest php-cc-mode-debug-test () + "Test running `php-mode-debug' and `php-mode-debug--buffer'. +`php-mode-debug' inspects CC Mode internals, so it is only valid in a +`php-cc-mode' buffer." + (require 'php-mode-debug) + (with-temp-buffer + (php-cc-mode) + (php-mode-debug) + (should (string= (buffer-name) "*PHP Mode DEBUG*")) + (php-mode-debug--buffer 'top) + (search-forward "--- PHP-MODE DEBUG BEGIN ---") + (search-forward "--- PHP-MODE DEBUG END ---")) + (with-current-buffer (php-mode-debug--buffer 'init) + (should (eq 0 (- (point-max) (point-min)))))) + +;;; php-cc-mode-test.el ends here diff --git a/tests/php-mode-test.el b/tests/php-mode-test.el index 1d5ef11f..19d211c4 100644 --- a/tests/php-mode-test.el +++ b/tests/php-mode-test.el @@ -32,16 +32,12 @@ ;;; Code: (require 'php) (require 'php-mode) -(require 'php-mode-debug) (require 'php-project) +(require 'php-complete) (require 'ert) (require 'cl-lib) (require 'imenu) -;; Work around bug #14325 -;; . -(c-after-font-lock-init) - (defvar php-mode-test-dir (if load-file-name (file-name-directory load-file-name) default-directory) @@ -61,12 +57,19 @@ "Process the test directives in the current buffer. These are the ###php-mode-test### comments. Valid magics are listed in `php-mode-test-valid-magics'; no other directives will -be processed." +be processed. + +For backward compatibility the magic expressions may still refer to +`c-basic-offset'; it is bound to the current `php-indent-offset' so +that fixtures shared with the CC Mode based `php-cc-mode' keep working." + (defvar c-basic-offset) + (let ((c-basic-offset (if (boundp 'php-indent-offset) php-indent-offset 4))) (cl-letf (((symbol-function 'indent) (lambda (offset) (let ((current-offset (current-indentation))) (unless (eq current-offset offset) - (warn "line: %d context: %s\n" (line-number-at-pos) (c-guess-basic-syntax)) + (warn "line: %d context: %S\n" (line-number-at-pos) + (save-excursion (back-to-indentation) (syntax-ppss))) (list :line (line-number-at-pos) :expected offset :actual current-offset)))))) @@ -81,7 +84,7 @@ be processed." (if (memq fn php-mode-test-valid-magics) (apply fn args)))) directives) - append (cl-remove-if #'null result)))))) + append (cl-remove-if #'null result))))))) (defun php-mode-test--buffer-face-list (buffer) "Return list of (STRING . FACE) from `BUFFER'." @@ -121,11 +124,10 @@ the coding style to one of the following: 1. `pear' 2. `drupal' 3. `wordpress' -4. `symfony2' -5. `psr2' +4. `psr2' Using any other symbol for STYLE results in undefined behavior. -The test will use the PHP style by default. +The test will use the PER (\"php\") style by default. If the `:custom' keyword is set, customized variables are not reset to their default state prior to starting the test. Use this if the test should @@ -135,7 +137,6 @@ If the `:faces' keyword is set, read the file with `.faces' added to that file name and check that the faces of the fonts in the buffer match." (declare (indent 1)) `(with-temp-buffer - (setq php-mode-enable-backup-style-variables nil) (insert-file-contents (expand-file-name ,file php-mode-test-dir)) (setq default-directory (expand-file-name ".." (expand-file-name ,file php-mode-test-dir))) @@ -148,11 +149,10 @@ file name and check that the faces of the fonts in the buffer match." (pear '(php-enable-pear-coding-style)) (drupal '(php-enable-drupal-coding-style)) (wordpress '(php-enable-wordpress-coding-style)) - (symfony2 '(php-enable-symfony2-coding-style)) (psr2 '(php-enable-psr2-coding-style)) (t '(php-enable-default-coding-style))) - ,(unless custom '(custom-set-variables '(php-lineup-cascaded-calls nil))) + ,(unless custom '(custom-set-variables '(php-indent-chain-indent nil))) ,(if indent '(let ((inhibit-message t)) (indent-region (point-min) (point-max)))) @@ -208,7 +208,7 @@ Gets the face of the text after the comma." (ert-deftest php-mode-test-issue-19 () "Alignment of arrow operators." - (custom-set-variables '(php-lineup-cascaded-calls t)) + (custom-set-variables '(php-indent-chain-indent t)) (with-php-mode-test ("issue-19.php" :indent t :custom t) (while (search-forward "$object->" (point-max) t) ;; Point is just after `->' @@ -216,14 +216,14 @@ Gets the face of the text after the comma." (search-forward "->") (should (= (current-column) col))))) - ;; Test indentation again, but without php-lineup-cascaded-calls enabled + ;; Test indentation again, but without php-indent-chain-indent enabled (with-php-mode-test ("issue-19.php" :indent t) (while (search-forward "\\($object->\\)" (point-max) t) (match-beginning 0) ;; Point is just on `$' (let ((col (current-column))) (search-forward "->") - (should (= (current-column) (+ col c-basic-offset))))))) + (should (= (current-column) (+ col php-indent-offset))))))) (ert-deftest php-mode-test-issue-21 () "Font locking multi-line string." @@ -273,7 +273,9 @@ an error." (ert-deftest php-mode-test-issue-53 () "Check if whitespace effects are undone when changing coding style from Drupal." - (dolist (mode '(pear wordpress symfony2)) + ;; Styles that do not enable `show-trailing-whitespace' (unlike drupal + ;; and psr2), so switching to them from drupal must clear it again. + (dolist (mode '(pear wordpress)) ;; the file written to has no significance, only the buffer (let ((tmp-filename (concat (make-temp-name temporary-file-directory) ".php")) (auto-mode-alist '(("\\.php\\'" . php-mode)))) @@ -298,6 +300,24 @@ style from Drupal." (should (equal (list "after-write-file" mode t) (list "after-write-file" mode (looking-at-p "$")))))))) +(ert-deftest php-mode-test-legacy-c-basic-offset () + "Regression test for the CC Mode migration layer. +A buffer-local `c-basic-offset' (as still set by some projects through +directory/file local variables) must be reflected into +`php-indent-offset' by `php-style--honor-legacy-c-basic-offset', which +runs from `hack-local-variables-hook'." + (defvar c-basic-offset) + (with-temp-buffer + (insert " accessor. - (with-php-mode-test ("indent/issue-623.php" :indent t :magic t)) ;; Proper alignment arglist. (with-php-mode-test ("indent/issue-702.php" :indent t :magic t)) (with-php-mode-test ("indent/issue-726.php" :indent t :magic t)) ;; Proper alignment arglist that contains empty lines. (with-php-mode-test ("indent/issue-793.php" :indent t :magic t))) +(ert-deftest php-mode-test-indentation-trailing-arrow () + "Indentation of a line following a `=>' left at the end of a line. + +`=>' is excluded from `php-indent--indent-operator-re' because it is +PHP's array key operator, so nothing treated the next line as a +continuation: an arrow function's body fell back to column zero." + (with-php-mode-test ("indent/trailing-arrow.php" :indent t :magic t))) + +(ert-deftest php-mode-test-indentation-object-accessor () + "Alignment of chained object accessors split across lines." + (with-php-mode-test ("indent/issue-623.php" :indent t :magic t))) + (ert-deftest php-mode-test-poly-php-html-indentation () "Indentation must work inside PHP chunks of a PHP-in-HTML polymode. Regression: `php-check-html-for-indentation' returned nil in polymode @@ -796,13 +816,25 @@ path; sending those to an HTML mode would take most PHP files away from (ert-deftest php-mode-test-php81 () "Test highlighting language constructs added in PHP 8.1." - (with-php-mode-test ("8.1/enum.php" :faces t)) + (with-php-mode-test ("8.1/enum.php" :faces t))) + +(ert-deftest php-mode-test-php81-readonly () + "Test highlighting of PHP 8.1 readonly properties. +The fixture deliberately contains a syntactically invalid declaration +\(\"claas Err\"); the regexp-based font-lock leaves it unfontified." (with-php-mode-test ("8.1/readonly.php" :faces t))) (ert-deftest php-mode-test-php84 () "Test highlighting language constructs added in PHP 8.4." (with-php-mode-test ("8.4/property-hooks.php" :faces t))) +(ert-deftest php-mode-test-php84-property-hooks-indent () + "Test indentation of PHP 8.4 property hooks. + +A hook puts a brace block after a property declaration rather than +after a function signature, and the short forms end in `=>'." + (with-php-mode-test ("8.4/property-hooks-indent.php" :indent t :magic t))) + (defun php-mode-test--faces-of (code token) "Return the list of faces on TOKEN's characters after fontifying CODE." (with-temp-buffer @@ -833,6 +865,22 @@ half-fontified: the `|' plain and the `>' as `php-comparison-op'." (should (equal (cons token expected) (cons token (php-mode-test--faces-of code token))))))) +(ert-deftest php-mode-test-php85 () + "Test indentation of language constructs added in PHP 8.5. + +The pipe operator `|>' introduces a continuation line that starts with +an operator, and \"clone with\" gives `clone' an argument list." + (with-php-mode-test ("8.5/pipe-operator.php" :indent t :magic t)) + (with-php-mode-test ("8.5/clone-with.php" :indent t :magic t))) + +(ert-deftest php-mode-test-attribute-expression-indent () + "Test indentation of expressions and closures in attribute arguments. + +PHP 8.5 allows a static closure as an attribute argument, so a `#[...]' +now has to survive a brace block inside it; the pre-8.5 spelling wrapped +the same logic in an object." + (with-php-mode-test ("8.5/attribute-expression.php" :indent t :magic t))) + (ert-deftest php-mode-test-lang () "Test highlighting for language constructs." (with-php-mode-test ("lang/class/anonymous-class.php" :indent t :magic t :faces t)) @@ -860,6 +908,56 @@ 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-mode-test-dot-context () + "`php-dot-context' classifies the context immediately before point." + (cl-flet ((ctx (code) (with-temp-buffer + (php-mode) + (insert "