mago 1.20.1

A comprehensive suite of PHP tooling inspired by Rust’s approach, providing parsing, linting, formatting, and more through a unified CLI and library interface.
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
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
//! Error types for the Mago CLI application.
//!
//! This module defines the [`Error`] enum which encompasses all possible errors that can occur
//! during the execution of Mago commands. The error type provides detailed error messages,
//! proper error chaining via [`std::error::Error::source`], and automatic conversion from
//! various underlying error types.
//!
//! # Error Handling Strategy
//!
//! The CLI uses a centralized error handling approach where all errors are:
//!
//! 1. **Converted**: Underlying errors from dependencies are converted into [`Error`] variants
//!    using [`From`] implementations for ergonomic error propagation with the `?` operator
//! 2. **Logged**: All errors are logged via the tracing framework in [`main()`](crate::main)
//!    before the application exits
//! 3. **Displayed**: User-friendly error messages are provided via the [`Display`](std::fmt::Display)
//!    implementation, with technical details available through [`source()`](std::error::Error::source)
//! 4. **Mapped to Exit Codes**: Errors result in [`EXIT_CODE_ERROR`](crate::EXIT_CODE_ERROR).
//!    Analysis/lint failures result in [`ExitCode::FAILURE`](std::process::ExitCode::FAILURE).
//!
//! # Error Categories
//!
//! Errors are organized into several categories:
//!
//! - **I/O Errors**: File reading, writing, and path canonicalization failures
//! - **Configuration Errors**: TOML/JSON parsing and configuration building errors
//! - **Database Errors**: File system scanning and database loading errors
//! - **Analysis Errors**: Static analysis, linting, and formatting errors
//! - **Runtime Errors**: Thread pool initialization and runtime building errors
//! - **User Interaction Errors**: Terminal interaction and dialoguer errors
//! - **Version Errors**: PHP version validation and parsing errors

use dialoguer::Error as DialoguerError;

use mago_analyzer::error::AnalysisError;
use mago_database::error::DatabaseError;
use mago_orchestrator::OrchestratorError;
use mago_php_version::PHPVersion;
use mago_php_version::error::ParsingError;
use mago_reporting::error::ReportingError;
use rayon::ThreadPoolBuildError;

use crate::version_check::VersionPinParseError;

/// The main error type for all Mago CLI operations.
///
/// This enum encompasses all possible errors that can occur during command execution,
/// from configuration loading to analysis and reporting. Each variant wraps the underlying
/// error type and provides context-specific error messages.
///
/// # Error Propagation
///
/// The error type implements [`From`] for common underlying error types, enabling
/// ergonomic error propagation using the `?` operator:
///
/// ```ignore
/// fn load_config() -> Result<Config, Error> {
///     let content = std::fs::read_to_string("mago.toml")?; // io::Error -> Error
///     let config: Config = toml::from_str(&content)?;      // toml::de::Error -> Error
///     Ok(config)
/// }
/// ```
///
/// # Error Chain
///
/// All variants that wrap underlying errors preserve the error chain via the
/// [`source()`](std::error::Error::source) method, allowing full error context
/// to be accessed programmatically or logged for debugging.
#[derive(Debug)]
pub enum Error {
    /// Failed to load or scan the file database.
    ///
    /// This error occurs during file system scanning when the database loader encounters
    /// issues such as permission errors, invalid paths, or I/O failures while discovering
    /// PHP files in the workspace.
    Database(DatabaseError),

    /// Failed to generate or output analysis reports.
    ///
    /// This error occurs when the reporting system cannot format or write analysis results,
    /// such as when writing to stdout/stderr fails or when generating JSON/SARIF output
    /// encounters serialization errors.
    Reporting(ReportingError),

    /// Failed to build the async runtime.
    ///
    /// This error occurs if the Tokio runtime cannot be initialized, typically due to
    /// system resource constraints or incompatible runtime configurations.
    BuildingRuntime(std::io::Error),

    /// Failed to build or merge configuration from multiple sources.
    ///
    /// This error occurs when loading configuration from environment variables, TOML files,
    /// or command-line arguments fails due to invalid settings, missing required fields,
    /// or incompatible configuration values.
    BuildingConfiguration(config::ConfigError),

    /// Failed to deserialize TOML configuration.
    ///
    /// This error occurs when parsing `mago.toml` or global configuration files fails due to
    /// syntax errors, type mismatches, or invalid TOML structure.
    DeserializingToml(toml::de::Error),

    /// Failed to serialize configuration to TOML format.
    ///
    /// This error occurs during the `mago init` command when the default configuration
    /// cannot be serialized to TOML, typically indicating a programming error rather than
    /// user error.
    SerializingToml(toml::ser::Error),

    /// Failed to canonicalize a file path.
    ///
    /// This error occurs when attempting to resolve a path to its absolute canonical form
    /// fails, typically because the path does not exist, involves broken symlinks, or
    /// permission issues prevent accessing parent directories.
    ///
    /// The first field contains the path that failed to canonicalize, and the second
    /// field contains the underlying I/O error.
    CanonicalizingPath(std::path::PathBuf, std::io::Error),

    /// Failed to parse or serialize JSON data.
    ///
    /// This error occurs when working with JSON files such as `composer.json` or when
    /// generating JSON output for analysis results. Common causes include malformed JSON,
    /// unexpected data types, or serialization failures.
    Json(serde_json::Error),

    /// Failed to perform self-update operation.
    ///
    /// This error occurs during the `mago self-update` command when the update process
    /// fails due to network issues, missing release assets, permission errors when
    /// replacing the binary, or version comparison failures.
    SelfUpdate(crate::updater::error::UpdateError),

    /// The configured PHP version is too old and not supported.
    ///
    /// This error occurs when the configured PHP version is below the minimum supported
    /// version. The first field contains the minimum supported version, and the second
    /// field contains the actual configured version.
    ///
    /// This check can be bypassed using the `--allow-unsupported-php-version` flag.
    PHPVersionIsTooOld(PHPVersion, PHPVersion),

    /// The configured PHP version is too new and not supported.
    ///
    /// This error occurs when the configured PHP version exceeds the maximum supported
    /// version. The first field contains the maximum supported version, and the second
    /// field contains the actual configured version.
    ///
    /// This check can be bypassed using the `--allow-unsupported-php-version` flag.
    PHPVersionIsTooNew(PHPVersion, PHPVersion),

    /// Failed to parse the PHP version string.
    ///
    /// This error occurs when a PHP version string (from CLI arguments, environment
    /// variables, or configuration files) cannot be parsed into a valid [`PHPVersion`].
    /// The first field contains the invalid version string, and the second field
    /// contains the parsing error details.
    InvalidPHPVersion(String, ParsingError),

    /// Failed to interact with the user via the terminal.
    ///
    /// This error occurs during interactive prompts (such as `mago init`) when dialoguer
    /// cannot read user input due to terminal I/O errors, interrupted input, or when
    /// running in a non-interactive environment.
    Dialoguer(DialoguerError),

    /// Failed to write the configuration file to disk.
    ///
    /// This error occurs during the `mago init` command when writing the new `mago.toml`
    /// file fails due to permission errors, disk full, or I/O failures.
    WritingConfiguration(std::io::Error),

    /// Failed to read the `composer.json` file.
    ///
    /// This error occurs when attempting to load `composer.json` for PHP version detection
    /// or autoload path resolution fails due to missing file, permission errors, or I/O
    /// failures.
    ReadingComposerJson(std::io::Error),

    /// Failed to read the baseline file.
    ///
    /// This error occurs when loading an existing baseline file for incremental issue
    /// tracking fails due to missing file, permission errors, or I/O failures.
    ReadingBaselineFile(std::io::Error),

    /// Failed to create or write the baseline file.
    ///
    /// This error occurs when generating a new baseline file fails due to permission
    /// errors, disk full, or I/O failures.
    CreatingBaselineFile(std::io::Error),

    /// Failed to parse the `composer.json` file.
    ///
    /// This error occurs when `composer.json` contains invalid JSON syntax or unexpected
    /// structure that prevents extracting PHP version or autoload configuration.
    ParsingComposerJson(serde_json::Error),

    /// Failed to initialize the thread pool for parallel processing.
    ///
    /// This error occurs when Rayon cannot build the global thread pool with the
    /// configured number of threads and stack size, typically due to system resource
    /// constraints or invalid configuration values.
    ThreadPoolBuildError(ThreadPoolBuildError),

    /// Failed to perform static analysis.
    ///
    /// This error occurs during the `mago analyze` command when the static analyzer
    /// encounters internal errors, such as type system failures, control flow analysis
    /// errors, or unrecoverable analysis state.
    Analysis(AnalysisError),

    /// Orchestrator operation failed.
    ///
    /// This error occurs when the orchestrator encounters errors while coordinating
    /// analysis tools, such as database loading failures, service initialization errors,
    /// or failures during parallel file processing.
    Orchestrator(OrchestratorError),

    /// Not inside a git repository.
    ///
    /// This error occurs when attempting to use git-related features (such as
    /// `--staged` formatting) outside of a git repository.
    NotAGitRepository,

    /// A staged file has unstaged changes.
    ///
    /// This error occurs when attempting to format staged files but a file has both
    /// staged and unstaged changes. Formatting in this case could cause data loss,
    /// so the operation is aborted. The string contains the path to the problematic file.
    StagedFileHasUnstagedChanges(String),

    /// An unknown formatter preset was requested.
    UnknownFormatterPreset(String),

    /// The `version` pin in `mago.toml` could not be parsed, or the
    /// installed mago binary's own version string could not be parsed.
    /// The wrapped error carries both the offending string and the reason.
    InvalidProjectVersionPin(VersionPinParseError),

    /// The installed mago binary is on a different major version than the
    /// `version` pinned in `mago.toml`. Always fatal: a major bump may have
    /// changed the config schema, so continuing would risk silently
    /// misinterpreting the project's configuration.
    ///
    /// Fields are: pinned version string, installed version string.
    ProjectMajorVersionMismatch(String, String),

    /// The `self-update --to-project-version` flag was used, but the current
    /// `mago.toml` does not set a `version` pin.
    NoPinnedProjectVersion,

    /// The `self-update --to-project-version` flag was used with a non-exact
    /// pin (e.g. `"1"` or `"1.19"`), and the latest available release does
    /// not satisfy it. The first field is the pin; the second is the latest
    /// release version we found.
    LatestReleaseDoesNotSatisfyPin(String, String),
}

/// Formats the error for user-friendly display.
///
/// This implementation provides context-specific error messages for each variant,
/// making errors readable in terminal output and logs. For technical details and
/// the error chain, use [`source()`](std::error::Error::source).
impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Database(error) => write!(f, "Failed to load database: {error}"),
            Self::Reporting(error) => write!(f, "Failed to report results: {error}"),
            Self::BuildingRuntime(error) => write!(f, "Failed to build the runtime: {error}"),
            Self::BuildingConfiguration(error) => write!(f, "Failed to build the configuration: {error}"),
            Self::DeserializingToml(error) => write!(f, "Failed to deserialize TOML: {error}"),
            Self::SerializingToml(error) => write!(f, "Failed to serialize TOML: {error}"),
            Self::CanonicalizingPath(path, error) => write!(f, "Failed to canonicalize path `{path:?}`: {error}"),
            Self::Json(error) => write!(f, "Failed to parse JSON: {error}"),
            Self::SelfUpdate(error) => write!(f, "Failed to self update: {error}"),
            Self::PHPVersionIsTooOld(minimum, actual) => {
                write!(f, "PHP version {actual} is not supported, minimum supported version is {minimum}")
            }
            Self::PHPVersionIsTooNew(maximum, actual) => {
                write!(f, "PHP version {actual} is not supported, maximum supported version is {maximum}")
            }
            Self::InvalidPHPVersion(version, error) => {
                write!(f, "Invalid PHP version `{version}`: {error}")
            }
            Self::Dialoguer(error) => write!(f, "Failed to interact with the user: {error}"),
            Self::WritingConfiguration(error) => write!(f, "Failed to write the configuration file: {error}"),
            Self::ReadingComposerJson(error) => write!(f, "Failed to read the `composer.json` file: {error}"),
            Self::ParsingComposerJson(error) => write!(f, "Failed to parse the `composer.json` file: {error}"),
            Self::ReadingBaselineFile(error) => write!(f, "Failed to read the baseline file: {error}"),
            Self::CreatingBaselineFile(error) => write!(f, "Failed to create the baseline file: {error}"),
            Self::Analysis(error) => write!(f, "Failed to analyze the source code: {error}"),
            Self::ThreadPoolBuildError(error) => {
                write!(f, "Failed to build the thread pool: {error}")
            }
            Self::Orchestrator(error) => write!(f, "Orchestrator error: {error}"),
            Self::NotAGitRepository => write!(f, "Not inside a git repository"),
            Self::StagedFileHasUnstagedChanges(path) => {
                write!(f, "Cannot format staged files: '{path}' has both staged and unstaged changes")
            }
            Self::UnknownFormatterPreset(preset) => {
                write!(f, "Unknown formatter preset: `{preset}`. Available presets are: laravel, psr12, default")
            }
            Self::InvalidProjectVersionPin(error) => write!(f, "{error}"),
            Self::ProjectMajorVersionMismatch(pinned, installed) => {
                write!(
                    f,
                    "mago.toml is pinned to major version `{pinned}`, but the installed mago binary is `{installed}`"
                )
            }
            Self::NoPinnedProjectVersion => {
                write!(f, "`self-update --to-project-version` requires a `version` pin in mago.toml")
            }
            Self::LatestReleaseDoesNotSatisfyPin(pinned, latest) => {
                write!(
                    f,
                    "No published mago release satisfies the `version` pin `{pinned}` in mago.toml (most recent: `{latest}`)"
                )
            }
        }
    }
}

/// Implements the standard error trait, providing access to the error chain.
///
/// The [`source()`](std::error::Error::source) method returns the underlying error
/// for variants that wrap other errors, allowing full error context to be accessed
/// programmatically. This is used by error reporting libraries and the tracing
/// framework to display complete error chains.
impl std::error::Error for Error {
    /// Returns the underlying error that caused this error, if any.
    ///
    /// This method enables error chain traversal, allowing callers to access the
    /// full context of why an operation failed. Most variants return `Some`,
    /// except for variants like `PHPVersionIsTooOld` and `PHPVersionIsTooNew`
    /// which represent validation errors without an underlying cause.
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Self::Database(error) => Some(error),
            Self::Reporting(error) => Some(error),
            Self::BuildingConfiguration(error) => Some(error),
            Self::BuildingRuntime(error) => Some(error),
            Self::DeserializingToml(error) => Some(error),
            Self::SerializingToml(error) => Some(error),
            Self::CanonicalizingPath(_, error) => Some(error),
            Self::Json(error) => Some(error),
            Self::SelfUpdate(error) => Some(error),
            Self::InvalidPHPVersion(_, error) => Some(error),
            Self::Dialoguer(error) => Some(error),
            Self::WritingConfiguration(error) => Some(error),
            Self::ReadingComposerJson(error) => Some(error),
            Self::ParsingComposerJson(error) => Some(error),
            Self::ReadingBaselineFile(error) => Some(error),
            Self::CreatingBaselineFile(error) => Some(error),
            Self::Analysis(error) => Some(error),
            Self::ThreadPoolBuildError(error) => Some(error),
            Self::Orchestrator(error) => Some(error),
            Self::InvalidProjectVersionPin(error) => Some(error),
            _ => None,
        }
    }
}

/// Converts database errors into CLI errors.
///
/// This enables the `?` operator to automatically convert [`DatabaseError`] into
/// [`Error`] when propagating errors from database operations.
impl From<DatabaseError> for Error {
    fn from(error: DatabaseError) -> Self {
        Self::Database(error)
    }
}

/// Converts reporting errors into CLI errors.
///
/// This enables the `?` operator to automatically convert [`ReportingError`] into
/// [`Error`] when propagating errors from report generation operations.
impl From<ReportingError> for Error {
    fn from(error: ReportingError) -> Self {
        Self::Reporting(error)
    }
}

/// Converts configuration building errors into CLI errors.
///
/// This enables the `?` operator to automatically convert [`config::ConfigError`]
/// into [`Error`] when propagating errors from configuration loading operations.
impl From<config::ConfigError> for Error {
    fn from(error: config::ConfigError) -> Self {
        Self::BuildingConfiguration(error)
    }
}

/// Converts TOML deserialization errors into CLI errors.
///
/// This enables the `?` operator to automatically convert [`toml::de::Error`]
/// into [`Error`] when propagating errors from TOML parsing operations.
impl From<toml::de::Error> for Error {
    fn from(error: toml::de::Error) -> Self {
        Self::DeserializingToml(error)
    }
}

/// Converts TOML serialization errors into CLI errors.
///
/// This enables the `?` operator to automatically convert [`toml::ser::Error`]
/// into [`Error`] when propagating errors from TOML generation operations.
impl From<toml::ser::Error> for Error {
    fn from(error: toml::ser::Error) -> Self {
        Self::SerializingToml(error)
    }
}

/// Converts JSON serialization/deserialization errors into CLI errors.
///
/// This enables the `?` operator to automatically convert [`serde_json::Error`]
/// into [`Error`] when propagating errors from JSON operations.
impl From<serde_json::Error> for Error {
    fn from(error: serde_json::Error) -> Self {
        Self::Json(error)
    }
}

/// Converts self-update errors into CLI errors.
///
/// This enables the `?` operator to automatically convert [`UpdateError`](crate::updater::error::UpdateError)
/// into [`Error`] when propagating errors from the `mago self-update` command.
impl From<crate::updater::error::UpdateError> for Error {
    fn from(error: crate::updater::error::UpdateError) -> Self {
        Self::SelfUpdate(error)
    }
}

/// Converts dialoguer interaction errors into CLI errors.
///
/// This enables the `?` operator to automatically convert [`DialoguerError`]
/// into [`Error`] when propagating errors from interactive terminal prompts.
impl From<DialoguerError> for Error {
    fn from(error: DialoguerError) -> Self {
        Self::Dialoguer(error)
    }
}

/// Converts analysis errors into CLI errors.
///
/// This enables the `?` operator to automatically convert [`AnalysisError`]
/// into [`Error`] when propagating errors from static analysis operations.
impl From<AnalysisError> for Error {
    fn from(error: AnalysisError) -> Self {
        Self::Analysis(error)
    }
}

/// Converts thread pool build errors into CLI errors.
///
/// This enables the `?` operator to automatically convert [`ThreadPoolBuildError`]
/// into [`Error`] when propagating errors from Rayon thread pool initialization.
impl From<ThreadPoolBuildError> for Error {
    fn from(error: ThreadPoolBuildError) -> Self {
        Self::ThreadPoolBuildError(error)
    }
}

/// Converts orchestrator errors into CLI errors.
///
/// This enables the `?` operator to automatically convert [`OrchestratorError`]
/// into [`Error`] when propagating errors from orchestrator operations.
impl From<OrchestratorError> for Error {
    fn from(error: OrchestratorError) -> Self {
        Self::Orchestrator(error)
    }
}

/// Converts [`VersionPinParseError`] into CLI errors, enabling `?`-based
/// propagation through `VersionPin::parse` / `VersionPin::check`.
impl From<VersionPinParseError> for Error {
    fn from(error: VersionPinParseError) -> Self {
        Self::InvalidProjectVersionPin(error)
    }
}