mago-analyzer 1.21.0

A PHP static analyzer that can detect type errors in PHP code, and provide suggestions for fixing them.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
use mago_algebra::AlgebraThresholds;
use mago_atom::AtomSet;
use mago_codex::ttype::combiner::CombinerOptions;
use mago_php_version::PHPVersion;

/// Default maximum logical formula size during conditional analysis.
pub const DEFAULT_FORMULA_SIZE_THRESHOLD: u16 = 512;

/// Default cap on the loop assignment-graph depth that the analyzer will
/// explore when running fixed-point iteration over loop bodies.
///
/// The default of `1` means each loop body is re-analyzed at most once after
/// the initial pass, which is sufficient to stabilise the vast majority of
/// real-world code and keeps per-file cost bounded. Projects that care about
/// maximally precise narrowing of long loop-carried dependency chains can
/// raise this in their config at the cost of analysis time.
pub const DEFAULT_LOOP_ASSIGNMENT_DEPTH_THRESHOLD: u8 = 1;

/// Configuration settings that control the behavior of the Mago analyzer.
///
/// This struct allows you to enable/disable specific checks, suppress categories of issues,
/// and tune the analyzer's performance and strictness.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Settings {
    /// The target PHP version for the analysis.
    pub version: PHPVersion,

    /// Find and report expressions whose results are not used (e.g., `$a + $b;`). Defaults to `false`.
    pub find_unused_expressions: bool,

    /// Find and report unused definitions (e.g., private methods that are never called). Defaults to `false`.
    pub find_unused_definitions: bool,

    /// Warn when a function's declared return type contains a branch the body never actually returns
    /// (e.g. `: string|false` on a function that always returns a string). Defaults to `false`.
    pub find_overly_wide_return_types: bool,

    /// Analyze code that appears to be unreachable. Defaults to `false`.
    pub analyze_dead_code: bool,

    /// Track the literal values of class properties when they are assigned.
    /// This improves type inference but may increase memory usage. Defaults to `true`.
    pub memoize_properties: bool,

    /// Allow accessing array keys that may not be defined without reporting an issue. Defaults to `true`.
    pub allow_possibly_undefined_array_keys: bool,

    /// Enable checking for unhandled thrown exceptions.
    ///
    /// When `true`, the analyzer will report any exception that is thrown but not caught
    /// in a `try-catch` block or documented in a `@throws` tag.
    ///
    /// This check is disabled by default (`false`) as it can be computationally expensive.
    pub check_throws: bool,

    /// Exceptions to ignore including all subclasses (hierarchy-aware).
    ///
    /// When an exception class is in this set, any exception of that class or any of its
    /// subclasses will be ignored during `check_throws` analysis.
    ///
    /// For example, adding `LogicException` will ignore `LogicException`, `InvalidArgumentException`,
    /// `OutOfBoundsException`, and all other subclasses.
    pub unchecked_exceptions: AtomSet,

    /// Exceptions to ignore (exact class match only, not subclasses).
    ///
    /// When an exception class is in this set, only that exact class will be ignored
    /// during `check_throws` analysis. Parent classes and subclasses are not affected.
    pub unchecked_exception_classes: AtomSet,

    /// Check for missing `#[Override]` attributes on overriding methods.
    ///
    /// When enabled, the analyzer reports methods that override a parent method without
    /// the `#[Override]` attribute (PHP 8.3+).
    ///
    /// Defaults to `true`.
    pub check_missing_override: bool,

    /// Find and report unused function/method parameters.
    ///
    /// When enabled, the analyzer reports parameters that are declared but never used
    /// within the function body.
    ///
    /// Defaults to `true`.
    pub find_unused_parameters: bool,

    /// Enforce strict checks when accessing list elements by index.
    ///
    /// When `true`, the analyzer requires that any integer used to access a `list`
    /// element is provably non-negative (e.g., of type `int<0, max>`). This helps
    /// prevent potential runtime errors from using a negative index.
    ///
    /// When `false` (the default), any `int` is permitted as an index, offering
    /// more flexibility at the cost of type safety.
    pub strict_list_index_checks: bool,

    /// Disable comparisons to boolean literals (`true`/`false`).
    ///
    /// When enabled, comparisons to boolean literals will not be reported as issues.
    ///
    /// Defaults to `false`.
    pub no_boolean_literal_comparison: bool,

    /// Enforce that concrete classes are declared `final`.
    ///
    /// When enabled, the analyzer reports a warning for any class that is not
    /// `final`, `abstract`, or annotated with `@api`, provided the class has no children.
    ///
    /// Defaults to `false`.
    pub enforce_class_finality: bool,

    /// Require `@api` or `@internal` annotations on abstract classes, interfaces, and traits.
    ///
    /// When enabled, the analyzer reports a warning for any abstract class, interface,
    /// or trait that is not annotated with either `@api` or `@internal`.
    ///
    /// Defaults to `false`.
    pub require_api_or_internal: bool,

    /// Check for missing type hints on parameters, properties, and return types.
    ///
    /// When enabled, the analyzer will report warnings for function parameters, class properties,
    /// and function return types that lack explicit type declarations. The analyzer uses its
    /// type system knowledge to avoid false positives - for instance, it won't require a type hint
    /// on a property if adding one would conflict with a parent class or trait that has no type hint.
    ///
    /// Defaults to `false`.
    pub check_missing_type_hints: bool,

    /// Check for missing type hints (both parameters and return types) in closures when `check_missing_type_hints` is enabled.
    ///
    /// When `true`, closures (anonymous functions declared with `function() {}`) will be
    /// checked for missing type hints. When `false`, closures are ignored, which is useful
    /// because closures often rely on type inference.
    ///
    /// Defaults to `false`.
    pub check_closure_missing_type_hints: bool,

    /// Check for missing type hints (both parameters and return types) in arrow functions when `check_missing_type_hints` is enabled.
    ///
    /// When `true`, arrow functions (declared with `fn() => ...`) will be checked for missing
    /// type hints. When `false`, arrow functions are ignored, which is useful because arrow
    /// functions often rely on type inference and are typically short, making types obvious.
    ///
    /// Defaults to `false`.
    pub check_arrow_function_missing_type_hints: bool,

    /// Register superglobals (e.g., `$_GET`, `$_POST`, `$_SERVER`) in the analysis context.
    ///
    /// If disabled, super globals won't be available unless explicitly imported using
    /// the `global` keyword.
    ///
    /// Defaults to `true`.
    pub register_super_globals: bool,

    /// Enable colored output in terminal environments that support it. Defaults to `true`.
    ///
    /// This setting is primarily used for enabling/disabling colored diffs in
    /// issue reports.
    pub use_colors: bool,

    /// **Internal use only.**
    ///
    /// Enables a diffing mode for incremental analysis, used by integrations like LSPs.
    /// This avoids re-analyzing unchanged code in the same session. Defaults to `false`.
    pub diff: bool,

    /// Trust symbol existence checks to narrow types.
    ///
    /// When enabled, conditional checks like `method_exists()`, `property_exists()`,
    /// `function_exists()`, and `defined()` will narrow the type within the conditional block,
    /// suppressing errors for symbols that are verified to exist at runtime.
    ///
    /// When disabled, these checks are ignored and the analyzer requires explicit type hints,
    /// which is stricter but may produce more false positives for dynamic code.
    ///
    /// Defaults to `true`.
    pub trust_existence_checks: bool,

    /// Method names treated as class initializers (like `__construct`).
    ///
    /// Properties initialized in these methods count as "definitely initialized"
    /// just like in the constructor. This is useful for frameworks that use
    /// lifecycle methods like `PHPUnit`'s `setUp()` or framework `boot()` methods.
    ///
    /// Example: `["setUp", "initialize", "boot"]`
    ///
    /// Defaults to empty (no additional initializers).
    pub class_initializers: AtomSet,

    /// Enable property initialization checking (`missing-constructor`, `uninitialized-property`).
    ///
    /// When `false`, disables both `missing-constructor` and `uninitialized-property` issues
    /// entirely. This is useful for projects that prefer to rely on runtime errors for
    /// property initialization.
    ///
    /// Defaults to `false`.
    pub check_property_initialization: bool,

    /// Check for non-existent symbols in use statements.
    ///
    /// When enabled, the analyzer will report use statements that import symbols
    /// (classes, interfaces, traits, enums, functions, or constants) that do not exist
    /// in the codebase.
    ///
    /// Defaults to `false`.
    pub check_use_statements: bool,

    /// Check for usage of `@experimental` symbols from non-experimental contexts.
    ///
    /// When enabled, the analyzer reports warnings when a symbol marked `@experimental`
    /// is used from a context that is not itself marked `@experimental`.
    ///
    /// Defaults to `false`.
    pub check_experimental: bool,

    /// Check for incorrect casing when referencing classes, interfaces, traits, enums,
    /// and functions.
    ///
    /// When enabled, the analyzer reports warnings when a symbol is referenced with
    /// different casing than its definition (e.g., `new fooBar()` when defined as `FooBar`).
    /// This helps prevent autoloading failures on case-sensitive file systems.
    ///
    /// Defaults to `false`.
    pub check_name_casing: bool,

    /// Whether to allow calls to impure functions inside conditions.
    ///
    /// When set to `false`, any call to a function not marked `@pure` or
    /// `@mutation-free` inside an `if`, `while`, `for`, ternary, or `match`
    /// condition is reported. This helps catch surprising evaluation-order
    /// bugs where a side effect in one part of a condition silently alters
    /// a variable used in another part.
    ///
    /// Defaults to `true` (impure calls in conditions are allowed).
    pub allow_side_effects_in_conditions: bool,

    // Performance tuning thresholds
    // Higher values allow deeper analysis at the cost of performance.
    // Lower values improve speed but may reduce precision on complex code.
    /// Maximum number of clauses to process during CNF saturation.
    ///
    /// Controls how many clauses the simplification algorithm will work with.
    /// If exceeded, saturation returns an empty result to avoid performance issues.
    ///
    /// Defaults to `8192`.
    pub saturation_complexity_threshold: u16,

    /// Maximum number of clauses per side in disjunction operations.
    ///
    /// Controls the complexity limit for OR operations between clause sets.
    /// If either side exceeds this, the disjunction returns an empty result.
    ///
    /// Defaults to `4096`.
    pub disjunction_complexity_threshold: u16,

    /// Maximum cumulative complexity during formula negation.
    ///
    /// Controls how complex the negation of a formula can become.
    /// If exceeded, negation gives up to avoid exponential blowup.
    ///
    /// Defaults to `4096`.
    pub negation_complexity_threshold: u16,

    /// Upper limit for consensus optimization during saturation.
    ///
    /// Controls when the consensus rule is applied during saturation.
    /// Only applies when clause count is between 3 and this limit.
    ///
    /// Defaults to `256`.
    pub consensus_limit_threshold: u16,

    /// Maximum logical formula size during conditional analysis.
    ///
    /// Limits the size of generated formulas to prevent exponential blowup
    /// in deeply nested conditionals.
    ///
    /// Defaults to `512`.
    pub formula_size_threshold: u16,

    /// Maximum number of literal strings to track before generalizing.
    ///
    /// When combining types with many different literal string values, tracking each
    /// literal individually causes O(n) memory and O(n²) comparison time.
    /// Once the threshold is exceeded, we generalize to the base string type.
    ///
    /// Defaults to `128`.
    pub string_combination_threshold: u16,

    /// Maximum number of literal integers to track before generalizing.
    ///
    /// When combining types with many different literal integer values, tracking each
    /// literal individually causes O(n) memory and O(n²) comparison time.
    /// Once the threshold is exceeded, we generalize to the base int type.
    ///
    /// Defaults to `128`.
    pub integer_combination_threshold: u16,

    /// Maximum number of array elements to track individually.
    ///
    /// When building array types through repeated push operations (`$arr[] = ...`),
    /// this limits how many individual elements are tracked before generalizing
    /// to a simpler array type. This prevents memory explosion on files with
    /// thousands of array pushes.
    ///
    /// Defaults to `128`.
    pub array_combination_threshold: u16,

    /// Maximum depth of the loop assignment dependency graph that the fixed-point
    /// analyzer will explore when re-analysing loop bodies.
    ///
    /// The analyzer uses fixed-point iteration to propagate widened types along
    /// loop-carried dependency chains. A chain of length `N` can require up to
    /// `N` extra passes for the type at the end of the chain to fully stabilise,
    /// and each pass re-analyses the entire loop body. On large, complex loops
    /// (think thousand-line procedural functions with deeply nested conditionals)
    /// the per-pass cost dominates file analysis time.
    ///
    /// The default of `1` means each loop body is re-analysed at most once after
    /// the initial pass; enough to stabilise virtually all real-world code while
    /// keeping analysis cost bounded. Projects that require maximally precise
    /// narrowing of long loop-carried chains can raise this value (typically to
    /// `2` or `3`) at the cost of significantly slower analysis on complex files.
    ///
    /// Setting this to `0` disables fixed-point iteration entirely and analyses
    /// each loop body exactly once. This is the fastest option but may produce
    /// less precise types for variables that depend on themselves across
    /// iterations.
    ///
    /// Defaults to `1`.
    pub loop_assignment_depth_threshold: u8,
}

impl Default for Settings {
    fn default() -> Self {
        Self::new(PHPVersion::LATEST)
    }
}

impl Settings {
    #[must_use]
    pub fn new(version: PHPVersion) -> Self {
        let default_thresholds = AlgebraThresholds::default();
        let default_combiner_options = CombinerOptions::default();

        Self {
            version,
            find_unused_expressions: true,
            find_unused_definitions: true,
            find_overly_wide_return_types: false,
            analyze_dead_code: false,
            memoize_properties: true,
            allow_possibly_undefined_array_keys: true,
            check_throws: false,
            unchecked_exceptions: AtomSet::default(),
            unchecked_exception_classes: AtomSet::default(),
            use_colors: true,
            check_missing_override: false,
            find_unused_parameters: false,
            strict_list_index_checks: false,
            no_boolean_literal_comparison: false,
            enforce_class_finality: false,
            require_api_or_internal: false,
            check_missing_type_hints: false,
            check_closure_missing_type_hints: false,
            check_arrow_function_missing_type_hints: false,
            register_super_globals: true,
            diff: false,
            trust_existence_checks: true,
            class_initializers: AtomSet::default(),
            check_property_initialization: false,
            check_use_statements: false,
            check_experimental: false,
            check_name_casing: false,
            allow_side_effects_in_conditions: true,
            saturation_complexity_threshold: default_thresholds.saturation_complexity,
            disjunction_complexity_threshold: default_thresholds.disjunction_complexity,
            negation_complexity_threshold: default_thresholds.negation_complexity,
            consensus_limit_threshold: default_thresholds.consensus_limit,
            formula_size_threshold: DEFAULT_FORMULA_SIZE_THRESHOLD,
            string_combination_threshold: default_combiner_options.string_combination_threshold,
            integer_combination_threshold: default_combiner_options.integer_combination_threshold,
            array_combination_threshold: default_combiner_options.array_combination_threshold,
            loop_assignment_depth_threshold: DEFAULT_LOOP_ASSIGNMENT_DEPTH_THRESHOLD,
        }
    }

    /// Returns the algebra thresholds derived from the settings.
    #[must_use]
    pub fn algebra_thresholds(&self) -> AlgebraThresholds {
        AlgebraThresholds {
            saturation_complexity: self.saturation_complexity_threshold,
            disjunction_complexity: self.disjunction_complexity_threshold,
            negation_complexity: self.negation_complexity_threshold,
            consensus_limit: self.consensus_limit_threshold,
        }
    }

    /// Returns the combiner options derived from the settings.
    #[must_use]
    pub fn combiner_options(&self) -> CombinerOptions {
        CombinerOptions {
            overwrite_empty_array: false,
            array_combination_threshold: self.array_combination_threshold,
            string_combination_threshold: self.string_combination_threshold,
            integer_combination_threshold: self.integer_combination_threshold,
        }
    }
}