PHP: expose file_filter via ls_specific_settings (#1710) (#1711)

- PHP/Intelephense: expose `file_filter` via `ls_specific_settings["php"]`, so that additional
  extensions containing PHP sources (e.g. Drupal's `.module` / `.install` / `.inc` / `.theme`)
  become visible to the symbol tools and are indexed by the language server
- PHP: treat `.phtml` files as PHP sources by default (all PHP language servers)
This commit is contained in:
Vasil Boychev authored and GitHub committed 2026-07-20 11:47:29 +02:00
1 parent 5c85d961a0
commit f631a4bceb
6 files changed
+157 -2

No files matched your search

+6
View File
@@ -42,6 +42,12 @@ Status of the `main` branch. Changes prior to the next official version change w
it fits, otherwise truncated with a trailing '...' and a note) before falling back to bare line
numbers, so agents can pick the right match without re-reading files. #1640
* Language Servers:
- PHP: treat `.phtml` files as PHP sources by default (all PHP language servers) #1710
- PHP/Intelephense: expose `file_filter` via `ls_specific_settings["php"]`, so that additional
extensions containing PHP sources (e.g. Drupal's `.module` / `.install` / `.inc` / `.theme`)
become visible to the symbol tools and are indexed by the language server #1710
* JetBrains:
- Allow external files from dependencies (specified via references like "<ext:FileUtil.class|472e0a13>") to be
- read via `ReadFileTool`
+13
View File
@@ -931,6 +931,19 @@ Supported settings:
| `ignore_vendor` | `true` | Ignore directories named `vendor` while indexing the project. |
| `maxFileSize` | unset | Forwarded as `intelephense.files.maxSize` in `initializationOptions`. |
| `maxMemory` | unset | Forwarded as `intelephense.maxMemory` in `initializationOptions`. |
| `file_filter` | unset | Additional file extensions (with leading dot) to treat as PHP sources, e.g. `[".module", ".install"]`; added to the defaults `.php` / `.phtml` (#1710). |
Example configuration making Drupal source files visible to the symbol tools:
```yaml
ls_specific_settings:
php:
file_filter: [".module", ".install", ".inc", ".theme", ".profile", ".engine"]
```
Notes:
- Extensions added via `file_filter` are synced into Serena's PHP source-file matcher and pushed to Intelephense as `intelephense.files.associations` globs at startup, so `find_symbol` and the language server treat the same files as PHP sources.
- The matcher is reset on every language server activation, so one project's `file_filter` does not leak into another.
#### PHP (`Phpactor`)
+23 -1
View File
@@ -12,7 +12,7 @@ from overrides import override
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_utils import PlatformId, PlatformUtils
from solidlsp.lsp_protocol_handler.lsp_types import Definition, DefinitionParams, LocationLink
from solidlsp.lsp_protocol_handler.lsp_types import Definition, DefinitionParams, DidChangeConfigurationParams, LocationLink
from solidlsp.settings import SolidLSPSettings
from ..lsp_protocol_handler import lsp_types
@@ -35,6 +35,11 @@ class Intelephense(SolidLanguageServer):
- maxMemory: sets intelephense.maxMemory
- maxFileSize: sets intelephense.files.maxSize
- ignore_vendor: whether or ignore directories named "vendor" (default: true)
- file_filter: list of additional file extensions (with leading dot) to treat as PHP
sources, e.g. [".module", ".inc"]. The extensions are added to the defaults (".php",
".phtml") of Serena's PHP source-file matcher and are pushed to Intelephense via
files.associations, such that the symbol tools and the language server treat the
same files as PHP sources (see #1710).
"""
@override
@@ -106,6 +111,12 @@ class Intelephense(SolidLanguageServer):
self._ignored_dirnames.add("vendor")
log.info(f"Ignoring the following directories for PHP projects: {', '.join(sorted(self._ignored_dirnames))}")
# extend the source-file matcher with any additional extensions configured via `file_filter`,
# such that Serena's symbol tools treat the added extensions as PHP sources as well (#1710)
file_filter = self._custom_settings.get("file_filter")
if file_filter:
self.language.get_source_fn_matcher().add_extensions(*file_filter)
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
return self.DependencyProvider(self._custom_settings, self._ls_resources_dir)
@@ -184,6 +195,17 @@ class Intelephense(SolidLanguageServer):
self.server.notify.initialized({})
# push the file associations derived from the source-file matcher (the default extensions
# plus any configured via `file_filter`), such that Intelephense indexes the same set of
# files that Serena's symbol tools treat as PHP sources (#1710). This cannot be passed in
# the initialize request: Intelephense reads configuration exclusively from
# workspace/didChangeConfiguration (its initializationOptions only cover storagePath,
# globalStoragePath, clearCache and licenceKey).
associations = [f"*{ext}" for ext in self.language.get_source_fn_matcher().file_extensions]
intelephense_config: DidChangeConfigurationParams = {"settings": {"intelephense": {"files": {"associations": associations}}}}
log.info(f"Sending workspace/didChangeConfiguration with file associations: {associations}")
self.server.notify.workspace_did_change_configuration(intelephense_config)
# Intelephense server is typically ready immediately after initialization
# TODO: This is probably incorrect; the server does send an initialized notification, which we could wait for!
+9 -1
View File
@@ -42,6 +42,13 @@ class FilenameMatcher:
"""
self._file_extensions = list(self._initial_file_extensions)
@property
def file_extensions(self) -> list[str]:
"""The file extensions currently registered with this matcher, including any added via
:meth:`add_extensions`. Returned as a copy.
"""
return list(self._file_extensions)
def add_extensions(self, *file_extensions: str) -> None:
"""
Add further file extensions to this matcher (idempotent).
@@ -448,7 +455,8 @@ class Language(str, Enum):
case self.DART:
return FilenameMatcher(".dart")
case self.PHP | self.PHP_PHPACTOR | self.PHP_PHPANTOM:
return FilenameMatcher(".php")
# .phtml is a standard (yet outdated) extension for PHP sources
return FilenameMatcher(".php", ".phtml")
case self.R:
return FilenameMatcher(".R", ".r", ".Rmd", ".Rnw")
case self.PERL:
@@ -0,0 +1,29 @@
<?php
/**
* @file
* Drupal-style module file: ordinary PHP kept in a non-.php extension.
*
* Only visible to the symbol tools when ls_specific_settings["php"]["file_filter"] includes
* ".module" (see #1710); inert for all tests running with default settings.
*/
/**
* Implements hook_help().
*/
function drupal_module_help($route_name) {
return 'Help for ' . $route_name;
}
/**
* Implements hook_cron().
*/
function drupal_module_cron() {
helperFunction();
}
class DrupalModuleController {
public function build(): string {
return drupal_module_help('help.page.drupal_module');
}
}
+77
View File
@@ -0,0 +1,77 @@
"""Configuration plumbing for ``Intelephense``'s ``file_filter``.
Serena treats only ``.php`` / ``.phtml`` files as PHP sources by default, while Drupal projects
keep ordinary PHP in ``.module`` / ``.install`` / ``.inc`` / ``.theme`` / ``.profile`` /
``.engine`` files, which therefore stay invisible to ``find_symbol`` and friends.
``ls_specific_settings["php"]`` accepts a ``file_filter`` key with additional extensions,
mirroring the Perl mechanism from #1449 / #1642 (see #1710).
The unit tests cover the default source-file matcher. The configuration plumbing itself lives
inline in ``Intelephense.__init__`` (matcher sync) and ``_start_server`` (the
``intelephense.files.associations`` push) and is exercised end-to-end by the integration test
at the bottom, which starts a real Intelephense and is gated by the ``php`` marker like the
rest of the PHP suite.
"""
import pytest
from solidlsp.ls_config import FilenameMatcher, Language
from solidlsp.ls_utils import SymbolUtils
from test.conftest import get_repo_path, start_ls_context
class TestPhpSourceFnMatcherDefaults:
def test_phtml_matched_by_default_for_all_php_language_servers(self) -> None:
# .phtml is a standard (yet outdated) PHP extension, so all PHP language servers treat it
# as a PHP source by default (#1710).
for language in (Language.PHP, Language.PHP_PHPACTOR, Language.PHP_PHPANTOM):
matcher = language.get_source_fn_matcher()
assert matcher.is_relevant_filename("index.php"), f"{language}: .php not matched"
assert matcher.is_relevant_filename("template.phtml"), f"{language}: .phtml not matched"
def test_module_not_matched_by_default(self) -> None:
# guard for the integration test below: .module files only become visible via file_filter
assert not Language.PHP.get_source_fn_matcher().is_relevant_filename("hooks.module")
def test_file_extensions_property_returns_copy(self) -> None:
# _create_base_initialize_params derives the files.associations globs from this property;
# mutating the returned list must not affect the matcher.
matcher = FilenameMatcher(".php", ".phtml")
extensions = matcher.file_extensions
extensions.append(".module")
assert ".module" not in matcher.file_extensions
assert not matcher.is_relevant_filename("hooks.module")
@pytest.mark.php
class TestFileFilterIntegration:
"""End-to-end check that a custom ``file_filter`` makes a Drupal-style file visible.
Starts a real Intelephense, hence gated by the ``php`` marker. The ``drupal_module.module``
fixture stays invisible to every test that runs with default settings.
"""
def test_module_file_symbols_and_references_visible(self) -> None:
with start_ls_context(
Language.PHP,
ls_specific_settings={Language.PHP: {"file_filter": [".module"]}},
) as ls:
# Layer 2 (files.associations) must be asserted FIRST: the reference in the
# never-opened drupal_module.module can only come from the server's association-driven
# background index. request_full_symbol_tree below didOpens every matched file in the
# LS, after which this assertion could pass even without the associations.
helper_php_path = str(get_repo_path(Language.PHP) / "helper.php")
references = ls.request_references(helper_php_path, 2, len("function "))
assert any(ref["uri"].endswith("drupal_module.module") for ref in references), (
f"helperFunction call in drupal_module.module not found in references: {references}"
)
# Layer 1 (Serena's source matcher): the .module file takes part in symbol traversal.
symbols = ls.request_full_symbol_tree()
assert SymbolUtils.symbol_tree_contains_name(symbols, "drupal_module_help"), (
"drupal_module_help from drupal_module.module not found in the symbol tree"
)
assert SymbolUtils.symbol_tree_contains_name(symbols, "DrupalModuleController"), (
"DrupalModuleController from drupal_module.module not found in the symbol tree"
)