Skip to main content

mago_analyzer/
settings.rs

1use mago_algebra::AlgebraThresholds;
2use mago_codex::metadata::class_like::ClassLikeMetadata;
3use mago_codex::ttype::combiner::CombinerOptions;
4use mago_php_version::PHPVersion;
5use mago_word::Word;
6use mago_word::WordSet;
7use mago_word::ascii_lowercase_word;
8
9/// Default maximum logical formula size during conditional analysis.
10pub const DEFAULT_FORMULA_SIZE_THRESHOLD: u16 = 512;
11
12/// Default cap on the loop assignment-graph depth that the analyzer will
13/// explore when running fixed-point iteration over loop bodies.
14///
15/// The default of `1` means each loop body is re-analyzed at most once after
16/// the initial pass, which is sufficient to stabilise the vast majority of
17/// real-world code and keeps per-file cost bounded. Projects that care about
18/// maximally precise narrowing of long loop-carried dependency chains can
19/// raise this in their config at the cost of analysis time.
20pub const DEFAULT_LOOP_ASSIGNMENT_DEPTH_THRESHOLD: u8 = 1;
21
22/// Configuration settings that control the behavior of the Mago analyzer.
23///
24/// This struct allows you to enable/disable specific checks, suppress categories of issues,
25/// and tune the analyzer's performance and strictness.
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct Settings {
28    /// The target PHP version for the analysis.
29    pub version: PHPVersion,
30
31    /// Find and report expressions whose results are not used (e.g., `$a + $b;`). Defaults to `false`.
32    pub find_unused_expressions: bool,
33
34    /// Find and report unused definitions (e.g., private methods that are never called). Defaults to `false`.
35    pub find_unused_definitions: bool,
36
37    /// Warn when a function's declared return type contains a branch the body never actually returns
38    /// (e.g. `: string|false` on a function that always returns a string). Defaults to `false`.
39    pub find_overly_wide_return_types: bool,
40
41    /// Analyze code that appears to be unreachable. Defaults to `false`.
42    pub analyze_dead_code: bool,
43
44    /// Track the literal values of class properties when they are assigned.
45    /// This improves type inference but may increase memory usage. Defaults to `true`.
46    pub memoize_properties: bool,
47
48    /// Allow accessing array keys that may not be defined without reporting an issue. Defaults to `true`.
49    pub allow_possibly_undefined_array_keys: bool,
50
51    /// Enable checking for unhandled thrown exceptions.
52    ///
53    /// When `true`, the analyzer will report any exception that is thrown but not caught
54    /// in a `try-catch` block or documented in a `@throws` tag.
55    ///
56    /// This check is disabled by default (`false`) as it can be computationally expensive.
57    pub check_throws: bool,
58
59    /// Exceptions to ignore including all subclasses (hierarchy-aware).
60    ///
61    /// When an exception class is in this set, any exception of that class or any of its
62    /// subclasses will be ignored during `check_throws` analysis.
63    ///
64    /// For example, adding `LogicException` will ignore `LogicException`, `InvalidArgumentException`,
65    /// `OutOfBoundsException`, and all other subclasses.
66    pub unchecked_exceptions: WordSet,
67
68    /// Exceptions to ignore (exact class match only, not subclasses).
69    ///
70    /// When an exception class is in this set, only that exact class will be ignored
71    /// during `check_throws` analysis. Parent classes and subclasses are not affected.
72    pub unchecked_exception_classes: WordSet,
73
74    /// Check for missing `#[Override]` attributes on overriding methods.
75    ///
76    /// When enabled, the analyzer reports methods that override a parent method without
77    /// the `#[Override]` attribute (PHP 8.3+).
78    ///
79    /// Defaults to `true`.
80    pub check_missing_override: bool,
81
82    /// Find and report unused function/method parameters.
83    ///
84    /// When enabled, the analyzer reports parameters that are declared but never used
85    /// within the function body.
86    ///
87    /// Defaults to `true`.
88    pub find_unused_parameters: bool,
89
90    /// Enforce strict checks when accessing list elements by index.
91    ///
92    /// When `true`, the analyzer requires that any integer used to access a `list`
93    /// element is provably non-negative (e.g., of type `int<0, max>`). This helps
94    /// prevent potential runtime errors from using a negative index.
95    ///
96    /// When `false` (the default), any `int` is permitted as an index, offering
97    /// more flexibility at the cost of type safety.
98    pub strict_list_index_checks: bool,
99
100    /// Treat array/list indices that are not provably present as `T|null` and warn on access.
101    ///
102    /// When `true`, reading a key from any array-like type whose presence is not
103    /// guaranteed emits `possibly-undefined-int-array-index` /
104    /// `possibly-undefined-string-array-index` and the resulting type is widened to
105    /// `T|null`. This applies to `list<T>` (non-zero indices), non-required entries of
106    /// `array{...}` shapes, and `array<K, V>` lookups with arbitrary keys. It lets
107    /// `=== null`, `??`, and `??=` checks behave correctly against PHP's runtime
108    /// semantics — PHP turns missing reads into `null` with an `Undefined array key`
109    /// warning.
110    ///
111    /// When `false` (the default), the analyzer keeps the looser behavior: the value is
112    /// flagged as possibly-undefined internally but is not unioned with `null` and no
113    /// warning is emitted. This is friendlier for typical PHP code that destructures or
114    /// reads from arrays/lists by index without first asserting existence.
115    pub strict_array_index_existence: bool,
116
117    /// Allow arrays as operands of logical operators (`&&`, `||`, `xor`).
118    ///
119    /// When `true`, the analyzer accepts an array on either side of a logical operator
120    /// without emitting `invalid-operand`. PHP coerces empty arrays to `false` and
121    /// non-empty arrays to `true`, mirroring the truthiness used by `if ($array)`.
122    ///
123    /// When `false` (the default), the analyzer flags array operands of `&&`/`||`/`xor`
124    /// to call out the implicit `bool` coercion. This matches the long-standing default
125    /// behavior; standalone `if ($array)` is still accepted and never produces this warning.
126    pub allow_array_truthy_operand: bool,
127
128    /// Disable comparisons to boolean literals (`true`/`false`).
129    ///
130    /// When enabled, comparisons to boolean literals will not be reported as issues.
131    ///
132    /// Defaults to `false`.
133    pub no_boolean_literal_comparison: bool,
134
135    /// Enforce that concrete classes are declared `final`.
136    ///
137    /// When enabled, the analyzer reports a warning for any class that is not
138    /// `final`, `abstract`, or annotated with `@api`, provided the class has no children.
139    ///
140    /// Defaults to `false`.
141    pub enforce_class_finality: bool,
142
143    /// Require `@api` or `@internal` annotations on abstract classes, interfaces, and traits.
144    ///
145    /// When enabled, the analyzer reports a warning for any abstract class, interface,
146    /// or trait that is not annotated with either `@api` or `@internal`.
147    ///
148    /// Defaults to `false`.
149    pub require_api_or_internal: bool,
150
151    /// Check for missing type hints on parameters, properties, and return types.
152    ///
153    /// When enabled, the analyzer will report warnings for function parameters, class properties,
154    /// and function return types that lack explicit type declarations. The analyzer uses its
155    /// type system knowledge to avoid false positives - for instance, it won't require a type hint
156    /// on a property if adding one would conflict with a parent class or trait that has no type hint.
157    ///
158    /// Defaults to `false`.
159    pub check_missing_type_hints: bool,
160
161    /// Check for missing type hints (both parameters and return types) in closures when `check_missing_type_hints` is enabled.
162    ///
163    /// When `true`, closures (anonymous functions declared with `function() {}`) will be
164    /// checked for missing type hints. When `false`, closures are ignored, which is useful
165    /// because closures often rely on type inference.
166    ///
167    /// Defaults to `false`.
168    pub check_closure_missing_type_hints: bool,
169
170    /// Check for missing type hints (both parameters and return types) in arrow functions when `check_missing_type_hints` is enabled.
171    ///
172    /// When `true`, arrow functions (declared with `fn() => ...`) will be checked for missing
173    /// type hints. When `false`, arrow functions are ignored, which is useful because arrow
174    /// functions often rely on type inference and are typically short, making types obvious.
175    ///
176    /// Defaults to `false`.
177    pub check_arrow_function_missing_type_hints: bool,
178
179    /// Skip the missing-type-hint checks for closures and arrow functions used
180    /// directly as the right-hand side of the pipe operator (`|>`).
181    ///
182    /// When `true`, an inline pipe callable like
183    /// `$x |> fn($p) => strtoupper($p)` will not warn about its parameter or
184    /// return type being missing, even when `check-closure-missing-type-hints`
185    /// or `check-arrow-function-missing-type-hints` is on. The pipe operand's
186    /// type is enough to derive the parameter type, so requiring a hint here
187    /// is mostly noise.
188    ///
189    /// Defaults to `false`.
190    pub allow_implicit_pipe_callable_types: bool,
191
192    /// Register superglobals (e.g., `$_GET`, `$_POST`, `$_SERVER`) in the analysis context.
193    ///
194    /// If disabled, super globals won't be available unless explicitly imported using
195    /// the `global` keyword.
196    ///
197    /// Defaults to `true`.
198    pub register_super_globals: bool,
199
200    /// Enable colored output in terminal environments that support it. Defaults to `true`.
201    ///
202    /// This setting is primarily used for enabling/disabling colored diffs in
203    /// issue reports.
204    pub use_colors: bool,
205
206    /// **Internal use only.**
207    ///
208    /// Enables a diffing mode for incremental analysis, used by integrations like LSPs.
209    /// This avoids re-analyzing unchanged code in the same session. Defaults to `false`.
210    pub diff: bool,
211
212    /// Trust symbol existence checks to narrow types.
213    ///
214    /// When enabled, conditional checks like `method_exists()`, `property_exists()`,
215    /// `function_exists()`, and `defined()` will narrow the type within the conditional block,
216    /// suppressing errors for symbols that are verified to exist at runtime.
217    ///
218    /// When disabled, these checks are ignored and the analyzer requires explicit type hints,
219    /// which is stricter but may produce more false positives for dynamic code.
220    ///
221    /// Defaults to `true`.
222    pub trust_existence_checks: bool,
223
224    /// Method names treated as class initializers (like `__construct`).
225    ///
226    /// Properties initialized in these methods count as "definitely initialized"
227    /// just like in the constructor. This is useful for frameworks that use
228    /// lifecycle methods like `PHPUnit`'s `setUp()` or framework `boot()` methods.
229    ///
230    /// Entries can be either bare method names (applying to any class that has
231    /// that method) or qualified as `Fully\\Qualified\\Class::method` to scope
232    /// the rule to a specific class hierarchy.
233    ///
234    /// Example: `["setUp", "boot", "PHPUnit\\Framework\\TestCase::setUpBeforeClass"]`
235    ///
236    /// Defaults to empty (no additional initializers).
237    pub class_initializers: Vec<ClassInitializer>,
238
239    /// Enable property initialization checking (`missing-constructor`, `uninitialized-property`).
240    ///
241    /// When `false`, disables both `missing-constructor` and `uninitialized-property` issues
242    /// entirely. This is useful for projects that prefer to rely on runtime errors for
243    /// property initialization.
244    ///
245    /// Defaults to `false`.
246    pub check_property_initialization: bool,
247
248    /// Check for non-existent symbols in use statements.
249    ///
250    /// When enabled, the analyzer will report use statements that import symbols
251    /// (classes, interfaces, traits, enums, functions, or constants) that do not exist
252    /// in the codebase.
253    ///
254    /// Defaults to `false`.
255    pub check_use_statements: bool,
256
257    /// Check for usage of `@experimental` symbols from non-experimental contexts.
258    ///
259    /// When enabled, the analyzer reports warnings when a symbol marked `@experimental`
260    /// is used from a context that is not itself marked `@experimental`.
261    ///
262    /// Defaults to `false`.
263    pub check_experimental: bool,
264
265    /// Check for incorrect casing when referencing classes, interfaces, traits, enums,
266    /// and functions.
267    ///
268    /// When enabled, the analyzer reports warnings when a symbol is referenced with
269    /// different casing than its definition (e.g., `new fooBar()` when defined as `FooBar`).
270    /// This helps prevent autoloading failures on case-sensitive file systems.
271    ///
272    /// Defaults to `false`.
273    pub check_name_casing: bool,
274
275    /// Whether to allow calls to impure functions inside conditions.
276    ///
277    /// When set to `false`, any call to a function not marked `@pure` or
278    /// `@mutation-free` inside an `if`, `while`, `for`, ternary, or `match`
279    /// condition is reported. This helps catch surprising evaluation-order
280    /// bugs where a side effect in one part of a condition silently alters
281    /// a variable used in another part.
282    ///
283    /// Defaults to `true` (impure calls in conditions are allowed).
284    pub allow_side_effects_in_conditions: bool,
285
286    // Performance tuning thresholds
287    // Higher values allow deeper analysis at the cost of performance.
288    // Lower values improve speed but may reduce precision on complex code.
289    /// Maximum number of clauses to process during CNF saturation.
290    ///
291    /// Controls how many clauses the simplification algorithm will work with.
292    /// If exceeded, saturation returns an empty result to avoid performance issues.
293    ///
294    /// Defaults to `8192`.
295    pub saturation_complexity_threshold: u16,
296
297    /// Maximum number of clauses per side in disjunction operations.
298    ///
299    /// Controls the complexity limit for OR operations between clause sets.
300    /// If either side exceeds this, the disjunction returns an empty result.
301    ///
302    /// Defaults to `4096`.
303    pub disjunction_complexity_threshold: u16,
304
305    /// Maximum cumulative complexity during formula negation.
306    ///
307    /// Controls how complex the negation of a formula can become.
308    /// If exceeded, negation gives up to avoid exponential blowup.
309    ///
310    /// Defaults to `4096`.
311    pub negation_complexity_threshold: u16,
312
313    /// Upper limit for consensus optimization during saturation.
314    ///
315    /// Controls when the consensus rule is applied during saturation.
316    /// Only applies when clause count is between 3 and this limit.
317    ///
318    /// Defaults to `256`.
319    pub consensus_limit_threshold: u16,
320
321    /// Maximum logical formula size during conditional analysis.
322    ///
323    /// Limits the size of generated formulas to prevent exponential blowup
324    /// in deeply nested conditionals.
325    ///
326    /// Defaults to `512`.
327    pub formula_size_threshold: u16,
328
329    /// Maximum number of literal strings to track before generalizing.
330    ///
331    /// When combining types with many different literal string values, tracking each
332    /// literal individually causes O(n) memory and O(n²) comparison time.
333    /// Once the threshold is exceeded, we generalize to the base string type.
334    ///
335    /// Defaults to `128`.
336    pub string_combination_threshold: u16,
337
338    /// Maximum number of literal integers to track before generalizing.
339    ///
340    /// When combining types with many different literal integer values, tracking each
341    /// literal individually causes O(n) memory and O(n²) comparison time.
342    /// Once the threshold is exceeded, we generalize to the base int type.
343    ///
344    /// Defaults to `128`.
345    pub integer_combination_threshold: u16,
346
347    /// Maximum number of array elements to track individually.
348    ///
349    /// When building array types through repeated push operations (`$arr[] = ...`),
350    /// this limits how many individual elements are tracked before generalizing
351    /// to a simpler array type. This prevents memory explosion on files with
352    /// thousands of array pushes.
353    ///
354    /// Defaults to `128`.
355    pub array_combination_threshold: u16,
356
357    /// Maximum depth of the loop assignment dependency graph that the fixed-point
358    /// analyzer will explore when re-analysing loop bodies.
359    ///
360    /// The analyzer uses fixed-point iteration to propagate widened types along
361    /// loop-carried dependency chains. A chain of length `N` can require up to
362    /// `N` extra passes for the type at the end of the chain to fully stabilise,
363    /// and each pass re-analyses the entire loop body. On large, complex loops
364    /// (think thousand-line procedural functions with deeply nested conditionals)
365    /// the per-pass cost dominates file analysis time.
366    ///
367    /// The default of `1` means each loop body is re-analysed at most once after
368    /// the initial pass; enough to stabilise virtually all real-world code while
369    /// keeping analysis cost bounded. Projects that require maximally precise
370    /// narrowing of long loop-carried chains can raise this value (typically to
371    /// `2` or `3`) at the cost of significantly slower analysis on complex files.
372    ///
373    /// Setting this to `0` disables fixed-point iteration entirely and analyses
374    /// each loop body exactly once. This is the fastest option but may produce
375    /// less precise types for variables that depend on themselves across
376    /// iterations.
377    ///
378    /// Defaults to `1`.
379    pub loop_assignment_depth_threshold: u8,
380}
381
382impl Default for Settings {
383    fn default() -> Self {
384        Self::new(PHPVersion::LATEST)
385    }
386}
387
388impl Settings {
389    #[must_use]
390    pub fn new(version: PHPVersion) -> Self {
391        let default_thresholds = AlgebraThresholds::default();
392        let default_combiner_options = CombinerOptions::default();
393
394        Self {
395            version,
396            find_unused_expressions: true,
397            find_unused_definitions: true,
398            find_overly_wide_return_types: false,
399            analyze_dead_code: false,
400            memoize_properties: true,
401            allow_possibly_undefined_array_keys: true,
402            check_throws: false,
403            unchecked_exceptions: WordSet::default(),
404            unchecked_exception_classes: WordSet::default(),
405            use_colors: true,
406            check_missing_override: false,
407            find_unused_parameters: false,
408            strict_list_index_checks: false,
409            strict_array_index_existence: false,
410            allow_array_truthy_operand: false,
411            no_boolean_literal_comparison: false,
412            enforce_class_finality: false,
413            require_api_or_internal: false,
414            check_missing_type_hints: false,
415            check_closure_missing_type_hints: false,
416            check_arrow_function_missing_type_hints: false,
417            allow_implicit_pipe_callable_types: false,
418            register_super_globals: true,
419            diff: false,
420            trust_existence_checks: true,
421            class_initializers: Vec::new(),
422            check_property_initialization: false,
423            check_use_statements: false,
424            check_experimental: false,
425            check_name_casing: false,
426            allow_side_effects_in_conditions: true,
427            saturation_complexity_threshold: default_thresholds.saturation_complexity,
428            disjunction_complexity_threshold: default_thresholds.disjunction_complexity,
429            negation_complexity_threshold: default_thresholds.negation_complexity,
430            consensus_limit_threshold: default_thresholds.consensus_limit,
431            formula_size_threshold: DEFAULT_FORMULA_SIZE_THRESHOLD,
432            string_combination_threshold: default_combiner_options.string_combination_threshold,
433            integer_combination_threshold: default_combiner_options.integer_combination_threshold,
434            array_combination_threshold: default_combiner_options.array_combination_threshold,
435            loop_assignment_depth_threshold: DEFAULT_LOOP_ASSIGNMENT_DEPTH_THRESHOLD,
436        }
437    }
438
439    /// Returns the algebra thresholds derived from the settings.
440    #[must_use]
441    pub fn algebra_thresholds(&self) -> AlgebraThresholds {
442        AlgebraThresholds {
443            saturation_complexity: self.saturation_complexity_threshold,
444            disjunction_complexity: self.disjunction_complexity_threshold,
445            negation_complexity: self.negation_complexity_threshold,
446            consensus_limit: self.consensus_limit_threshold,
447        }
448    }
449
450    /// Returns the combiner options derived from the settings.
451    #[must_use]
452    pub fn combiner_options(&self) -> CombinerOptions {
453        CombinerOptions {
454            overwrite_empty_array: false,
455            array_combination_threshold: self.array_combination_threshold,
456            string_combination_threshold: self.string_combination_threshold,
457            integer_combination_threshold: self.integer_combination_threshold,
458        }
459    }
460
461    /// Returns `true` when `method_name` is a configured class initializer
462    /// applicable to `meta` (either an unrestricted entry, or one whose class
463    /// qualifier `meta` is a subclass/implementer of).
464    #[must_use]
465    pub fn is_class_initializer_for(&self, meta: &ClassLikeMetadata, method_name: Word) -> bool {
466        self.class_initializers.iter().any(|init| init.method == method_name && init.applies_to(meta))
467    }
468
469    /// Iterator over initializer method names applicable to `meta`.
470    pub fn applicable_class_initializers<'cfg>(
471        &'cfg self,
472        meta: &'cfg ClassLikeMetadata,
473    ) -> impl Iterator<Item = Word> + 'cfg {
474        self.class_initializers.iter().filter(move |init| init.applies_to(meta)).map(|init| init.method)
475    }
476}
477
478/// A class-initializer entry: a method that, when present on a class, marks
479/// any properties it assigns as definitely initialized.
480///
481/// The optional `class` qualifier restricts the rule to a specific class hierarchy.
482#[derive(Debug, Clone, PartialEq, Eq, Hash)]
483pub struct ClassInitializer {
484    /// Lowercased FQN of the class/interface this entry is scoped to. `None`
485    /// means the entry applies to any class that has the named method.
486    pub class: Option<Word>,
487    /// Lowercased method name.
488    pub method: Word,
489}
490
491impl ClassInitializer {
492    /// Parse `"Class::method"` or `"method"`. Returns `None` for empty input
493    /// or empty halves.
494    #[must_use]
495    pub fn parse(raw: &str) -> Option<Self> {
496        match raw.split_once("::") {
497            Some((class, method)) => {
498                let class = class.trim_start_matches('\\').trim();
499                let method = method.trim();
500                if class.is_empty() || method.is_empty() {
501                    return None;
502                }
503
504                Some(Self {
505                    class: Some(ascii_lowercase_word(class.as_bytes())),
506                    method: ascii_lowercase_word(method.as_bytes()),
507                })
508            }
509            None => {
510                let method = raw.trim();
511                if method.is_empty() {
512                    return None;
513                }
514                Some(Self { class: None, method: ascii_lowercase_word(method.as_bytes()) })
515            }
516        }
517    }
518
519    /// `true` when this entry's class qualifier (if any) covers `meta`.
520    #[must_use]
521    pub fn applies_to(&self, meta: &ClassLikeMetadata) -> bool {
522        let Some(class) = self.class else { return true };
523        meta.name == class || meta.all_parent_classes.contains(&class) || meta.all_parent_interfaces.contains(&class)
524    }
525}