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.
use std::path::PathBuf;

use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;

use mago_database::GlobSettings;

use crate::config::CURRENT_DIR;
use crate::consts::PHP_EXTENSION;
use crate::error::Error;

/// Configuration options for source discovery.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
pub struct SourceConfiguration {
    /// The workspace directory from which to start scanning.
    ///
    /// Defaults to the current working directory.
    pub workspace: PathBuf,

    /// Paths or glob patterns for user defined source files.
    ///
    /// Supports both directory paths (e.g., "src") and glob patterns (e.g., "src/**/*.php").
    /// If empty, all files in the workspace directory are included.
    ///
    /// Defaults to `[]`.
    #[serde(default)]
    pub paths: Vec<String>,

    /// Paths or glob patterns for non-user defined files to include in the scan.
    ///
    /// Supports both directory paths and glob patterns (same as `paths`).
    ///
    /// Defaults to `[]`.
    #[serde(default)]
    pub includes: Vec<String>,

    /// Patterns to exclude from the scan.
    ///
    /// Defaults to `[]`.
    #[serde(default)]
    pub excludes: Vec<String>,

    /// File extensions to filter by.
    ///
    /// Defaults to `[".php"]`.
    #[serde(default = "default_extensions")]
    pub extensions: Vec<String>,

    /// Settings for glob pattern matching behavior.
    #[serde(default)]
    pub glob: GlobConfiguration,
}

/// Configuration for glob pattern matching behavior.
///
/// These settings control how glob patterns in `paths`, `includes`, and `excludes` are interpreted.
/// All defaults match standard glob behavior for backwards compatibility.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
pub struct GlobConfiguration {
    /// Match patterns case-insensitively.
    ///
    /// Default: `false`.
    pub case_insensitive: bool,

    /// When `true`, a single `*` does not match path separators (`/`).
    ///
    /// This makes `src/*/Test` match only `src/foo/Test`, not `src/foo/bar/Test`.
    /// Use `**` for recursive matching across directories.
    ///
    /// Default: `false`.
    pub literal_separator: bool,

    /// Whether `\` escapes special characters in patterns.
    ///
    /// Default: `true`.
    pub backslash_escape: bool,

    /// Whether an empty case in alternates is allowed.
    ///
    /// When enabled, `{,a}` matches both `""` and `"a"`.
    ///
    /// Default: `false`.
    pub empty_alternates: bool,
}

impl Default for GlobConfiguration {
    fn default() -> Self {
        Self { case_insensitive: false, literal_separator: false, backslash_escape: true, empty_alternates: false }
    }
}

impl GlobConfiguration {
    pub fn to_database_settings(&self) -> GlobSettings {
        GlobSettings {
            case_insensitive: self.case_insensitive,
            literal_separator: self.literal_separator,
            backslash_escape: self.backslash_escape,
            empty_alternates: self.empty_alternates,
        }
    }
}

impl Default for SourceConfiguration {
    fn default() -> Self {
        Self::from_workspace(CURRENT_DIR.clone())
    }
}

impl SourceConfiguration {
    /// Creates a new `SourceConfiguration` with the given workspace directory.
    ///
    /// # Arguments
    ///
    /// * `workspace` - The workspace directory from which to start scanning.
    ///
    /// # Returns
    ///
    /// A new `SourceConfiguration` with the given workspace directory.
    pub fn from_workspace(workspace: PathBuf) -> Self {
        Self {
            workspace,
            paths: vec![],
            includes: vec![],
            excludes: vec![],
            extensions: vec![PHP_EXTENSION.to_string()],
            glob: GlobConfiguration::default(),
        }
    }
}

impl SourceConfiguration {
    pub fn normalize(&mut self) -> Result<(), Error> {
        // Make workspace absolute if not already
        let workspace =
            if !self.workspace.is_absolute() { (*CURRENT_DIR).join(&self.workspace) } else { self.workspace.clone() };

        self.workspace = workspace.canonicalize().map_err(|e| Error::CanonicalizingPath(workspace, e))?;

        Ok(())
    }
}

fn default_extensions() -> Vec<String> {
    vec![PHP_EXTENSION.to_string()]
}