Skip to main content

php_lsp/
config.rs

1/// Per-category diagnostic toggle flags.
2/// The master `enabled` switch defaults to `true`. Individual category flags
3/// also default to `true`, so all diagnostics are on out of the box; set
4/// `initializationOptions.diagnostics.enabled = false` to silence everything,
5/// or turn off specific categories individually.
6#[derive(Debug, Clone)]
7pub struct DiagnosticsConfig {
8    /// Master switch: when `false`, no diagnostics are emitted. Defaults to `true`.
9    pub enabled: bool,
10    /// Undefined variable references.
11    pub undefined_variables: bool,
12    /// Calls to undefined functions.
13    pub undefined_functions: bool,
14    /// References to undefined classes / interfaces / traits.
15    pub undefined_classes: bool,
16    /// Wrong number of arguments passed to a function.
17    pub arity_errors: bool,
18    /// Return-type mismatches.
19    pub type_errors: bool,
20    /// Calls to `@deprecated` members.
21    pub deprecated_calls: bool,
22    /// Duplicate class / function declarations.
23    pub duplicate_declarations: bool,
24    /// Unused-symbol warnings (unused variables / parameters / methods /
25    /// properties / functions). New in mir 0.22; defaults to `false` so the
26    /// LSP doesn't add noisy warnings to existing workspaces without an
27    /// opt-in. Toggle via `diagnostics.unusedSymbols` in initializationOptions.
28    pub unused_symbols: bool,
29}
30
31impl Default for DiagnosticsConfig {
32    fn default() -> Self {
33        DiagnosticsConfig {
34            enabled: true,
35            undefined_variables: true,
36            undefined_functions: true,
37            undefined_classes: true,
38            arity_errors: true,
39            type_errors: true,
40            deprecated_calls: true,
41            duplicate_declarations: true,
42            unused_symbols: false,
43        }
44    }
45}
46
47impl DiagnosticsConfig {
48    /// All categories on. Used in tests and by clients that explicitly enable
49    /// diagnostics without overriding individual flags.
50    #[cfg(test)]
51    pub fn all_enabled() -> Self {
52        DiagnosticsConfig {
53            enabled: true,
54            ..DiagnosticsConfig::default()
55        }
56    }
57
58    pub(crate) fn from_value(v: &serde_json::Value) -> Self {
59        let mut cfg = DiagnosticsConfig::default();
60        let Some(obj) = v.as_object() else { return cfg };
61        let flag = |key: &str| obj.get(key).and_then(|x| x.as_bool()).unwrap_or(true);
62        cfg.enabled = obj.get("enabled").and_then(|x| x.as_bool()).unwrap_or(true);
63        cfg.undefined_variables = flag("undefinedVariables");
64        cfg.undefined_functions = flag("undefinedFunctions");
65        cfg.undefined_classes = flag("undefinedClasses");
66        cfg.arity_errors = flag("arityErrors");
67        cfg.type_errors = flag("typeErrors");
68        cfg.deprecated_calls = flag("deprecatedCalls");
69        cfg.duplicate_declarations = flag("duplicateDeclarations");
70        cfg.unused_symbols = obj
71            .get("unusedSymbols")
72            .and_then(|x| x.as_bool())
73            .unwrap_or(false);
74        cfg
75    }
76}
77
78/// Per-feature capability toggles. All default to `true` (enabled).
79/// Set `initializationOptions.features.<name> = false` to suppress a capability.
80#[derive(Debug, Clone)]
81pub struct FeaturesConfig {
82    pub completion: bool,
83    pub hover: bool,
84    pub definition: bool,
85    pub declaration: bool,
86    pub references: bool,
87    pub document_symbols: bool,
88    pub workspace_symbols: bool,
89    pub rename: bool,
90    pub signature_help: bool,
91    pub inlay_hints: bool,
92    pub semantic_tokens: bool,
93    pub selection_range: bool,
94    pub call_hierarchy: bool,
95    pub document_highlight: bool,
96    pub implementation: bool,
97    pub code_action: bool,
98    pub type_definition: bool,
99    pub code_lens: bool,
100    pub formatting: bool,
101    pub range_formatting: bool,
102    pub on_type_formatting: bool,
103    pub document_link: bool,
104    pub linked_editing_range: bool,
105    pub inline_values: bool,
106}
107
108impl Default for FeaturesConfig {
109    fn default() -> Self {
110        FeaturesConfig {
111            completion: true,
112            hover: true,
113            definition: true,
114            declaration: true,
115            references: true,
116            document_symbols: true,
117            workspace_symbols: true,
118            rename: true,
119            signature_help: true,
120            inlay_hints: true,
121            semantic_tokens: true,
122            selection_range: true,
123            call_hierarchy: true,
124            document_highlight: true,
125            implementation: true,
126            code_action: true,
127            type_definition: true,
128            code_lens: true,
129            formatting: true,
130            range_formatting: true,
131            on_type_formatting: true,
132            document_link: true,
133            linked_editing_range: true,
134            inline_values: true,
135        }
136    }
137}
138
139impl FeaturesConfig {
140    pub(crate) fn from_value(v: &serde_json::Value) -> Self {
141        let mut cfg = FeaturesConfig::default();
142        let Some(obj) = v.as_object() else { return cfg };
143        let flag = |key: &str| obj.get(key).and_then(|x| x.as_bool()).unwrap_or(true);
144        cfg.completion = flag("completion");
145        cfg.hover = flag("hover");
146        cfg.definition = flag("definition");
147        cfg.declaration = flag("declaration");
148        cfg.references = flag("references");
149        cfg.document_symbols = flag("documentSymbols");
150        cfg.workspace_symbols = flag("workspaceSymbols");
151        cfg.rename = flag("rename");
152        cfg.signature_help = flag("signatureHelp");
153        cfg.inlay_hints = flag("inlayHints");
154        cfg.semantic_tokens = flag("semanticTokens");
155        cfg.selection_range = flag("selectionRange");
156        cfg.call_hierarchy = flag("callHierarchy");
157        cfg.document_highlight = flag("documentHighlight");
158        cfg.implementation = flag("implementation");
159        cfg.code_action = flag("codeAction");
160        cfg.type_definition = flag("typeDefinition");
161        cfg.code_lens = flag("codeLens");
162        cfg.formatting = flag("formatting");
163        cfg.range_formatting = flag("rangeFormatting");
164        cfg.on_type_formatting = flag("onTypeFormatting");
165        cfg.document_link = flag("documentLink");
166        cfg.linked_editing_range = flag("linkedEditingRange");
167        cfg.inline_values = flag("inlineValues");
168        cfg
169    }
170}
171
172/// Maximum number of PHP files indexed during a workspace scan.
173/// Prevents excessive memory use on projects with very large vendor trees.
174pub const MAX_INDEXED_FILES: usize = 50_000;
175
176/// Configuration received from the client via `initializationOptions`.
177#[derive(Debug, Clone)]
178pub struct LspConfig {
179    /// PHP version string, e.g. `"8.1"`.  Set explicitly via `initializationOptions`
180    /// or auto-detected from `composer.json` / the `php` binary at startup.
181    pub php_version: Option<String>,
182    /// Glob patterns for paths to exclude from workspace indexing.
183    pub exclude_paths: Vec<String>,
184    /// Glob patterns for paths that must be indexed even if they match an
185    /// `excludePaths` entry.  Patterns are matched against path components
186    /// (same semantics as `excludePaths`).  Example: `["vendor/yiisoft"]`.
187    pub include_paths: Vec<String>,
188    /// Per-category diagnostic toggles.
189    pub diagnostics: DiagnosticsConfig,
190    /// Per-feature capability toggles.
191    pub features: FeaturesConfig,
192    /// Hard cap on the number of PHP files indexed during a workspace scan.
193    /// Defaults to [`MAX_INDEXED_FILES`]. Set lower via `initializationOptions`
194    /// to reduce memory on projects with very large vendor trees.
195    pub max_indexed_files: usize,
196}
197
198impl Default for LspConfig {
199    fn default() -> Self {
200        LspConfig {
201            php_version: None,
202            exclude_paths: Vec::new(),
203            include_paths: Vec::new(),
204            diagnostics: DiagnosticsConfig::default(),
205            features: FeaturesConfig::default(),
206            max_indexed_files: MAX_INDEXED_FILES,
207        }
208    }
209}
210
211impl LspConfig {
212    /// Merge a `.php-lsp.json` value with editor `initializationOptions` /
213    /// `workspace/configuration`. Editor settings win per-key; `excludePaths`
214    /// arrays are **concatenated** (file entries first, editor entries appended)
215    /// rather than replaced, since exclusion patterns are additive.
216    ///
217    /// Hot-reload of `.php-lsp.json` on file change is not supported; the file
218    /// is only read during `initialize` and `did_change_configuration`.
219    pub(crate) fn merge_project_configs(
220        file: Option<&serde_json::Value>,
221        editor: Option<&serde_json::Value>,
222    ) -> serde_json::Value {
223        let mut merged = file
224            .cloned()
225            .unwrap_or(serde_json::Value::Object(Default::default()));
226        let Some(editor_obj) = editor.and_then(|e| e.as_object()) else {
227            return merged;
228        };
229        let merged_obj = merged
230            .as_object_mut()
231            .expect("merged base is always an object");
232        for (key, val) in editor_obj {
233            // Both excludePaths and includePaths are concatenated rather than replaced.
234            if key == "excludePaths" || key == "includePaths" {
235                let file_arr = merged_obj
236                    .get(key)
237                    .and_then(|v| v.as_array())
238                    .cloned()
239                    .unwrap_or_default();
240                let editor_arr = val.as_array().cloned().unwrap_or_default();
241                merged_obj.insert(
242                    key.clone(),
243                    serde_json::Value::Array([file_arr, editor_arr].concat()),
244                );
245            } else {
246                merged_obj.insert(key.clone(), val.clone());
247            }
248        }
249        merged
250    }
251
252    pub(crate) fn from_value(v: &serde_json::Value) -> Self {
253        let mut cfg = LspConfig::default();
254        if let Some(ver) = v.get("phpVersion").and_then(|x| x.as_str()) {
255            if crate::autoload::is_valid_php_version(ver) {
256                cfg.php_version = Some(ver.to_string());
257            } else {
258                // Invalid version: skip environment detection, use the latest stubs.
259                cfg.php_version = Some(crate::autoload::PHP_8_5.to_string());
260            }
261        }
262        if let Some(arr) = v.get("excludePaths").and_then(|x| x.as_array()) {
263            cfg.exclude_paths = arr
264                .iter()
265                .filter_map(|x| x.as_str().map(str::to_string))
266                .collect();
267        }
268        if let Some(arr) = v.get("includePaths").and_then(|x| x.as_array()) {
269            cfg.include_paths = arr
270                .iter()
271                .filter_map(|x| x.as_str().map(str::to_string))
272                .collect();
273        }
274        if let Some(diag_val) = v.get("diagnostics") {
275            cfg.diagnostics = DiagnosticsConfig::from_value(diag_val);
276        }
277        if let Some(feat_val) = v.get("features") {
278            cfg.features = FeaturesConfig::from_value(feat_val);
279        }
280        if let Some(n) = v.get("maxIndexedFiles").and_then(|x| x.as_u64()) {
281            cfg.max_indexed_files = n as usize;
282        }
283        cfg
284    }
285}