From f631a4bceb40dd7b38151f2136473035cbfb7820 Mon Sep 17 00:00:00 2001 From: Vasil Boychev Date: Mon, 20 Jul 2026 12:47:29 +0300 Subject: [PATCH] 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) --- CHANGELOG.md | 6 ++ docs/02-usage/050_configuration.md | 13 ++++ src/solidlsp/language_servers/intelephense.py | 24 +++++- src/solidlsp/ls_config.py | 10 ++- .../repos/php/test_repo/drupal_module.module | 29 +++++++ test/solidlsp/php/test_php_config.py | 77 +++++++++++++++++++ 6 files changed, 157 insertions(+), 2 deletions(-) create mode 100644 test/resources/repos/php/test_repo/drupal_module.module create mode 100644 test/solidlsp/php/test_php_config.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a7a0a0f..4b65805d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 "") to be - read via `ReadFileTool` diff --git a/docs/02-usage/050_configuration.md b/docs/02-usage/050_configuration.md index 469ef566..94666266 100644 --- a/docs/02-usage/050_configuration.md +++ b/docs/02-usage/050_configuration.md @@ -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`) diff --git a/src/solidlsp/language_servers/intelephense.py b/src/solidlsp/language_servers/intelephense.py index 6f68c33e..1f937da3 100644 --- a/src/solidlsp/language_servers/intelephense.py +++ b/src/solidlsp/language_servers/intelephense.py @@ -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! diff --git a/src/solidlsp/ls_config.py b/src/solidlsp/ls_config.py index 2aa87fdc..f5b6edea 100644 --- a/src/solidlsp/ls_config.py +++ b/src/solidlsp/ls_config.py @@ -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: diff --git a/test/resources/repos/php/test_repo/drupal_module.module b/test/resources/repos/php/test_repo/drupal_module.module new file mode 100644 index 00000000..02bc7189 --- /dev/null +++ b/test/resources/repos/php/test_repo/drupal_module.module @@ -0,0 +1,29 @@ + 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" + )