use anyhow::{Result, anyhow};
use regex::Regex;
use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
use serde::de::{self};
use serde::{Deserialize, Deserializer, Serialize};
use std::collections::{BTreeMap, BTreeSet};
use std::path::PathBuf;
use std::str::FromStr;
use strum::IntoEnumIterator;
use unicode_normalization::UnicodeNormalization;

use crate::settings::LineEnding;
use ruff_formatter::IndentStyle;
use ruff_graph::Direction;
use ruff_linter::RUFF_PKG_VERSION;

use ruff_linter::line_width::{IndentWidth, LineLength};
use ruff_linter::rules::flake8_import_conventions::settings::BannedAliases;
use ruff_linter::rules::flake8_pytest_style::settings::SettingsError;
use ruff_linter::rules::flake8_pytest_style::types;
use ruff_linter::rules::flake8_quotes::settings::Quote;
use ruff_linter::rules::flake8_tidy_imports::settings::{
    AllImports, ApiBan, ImportSelection, ImportSelector, Strictness,
};
use ruff_linter::rules::isort::settings::RelativeImportsOrder;
use ruff_linter::rules::isort::{ImportSection, ImportType};
use ruff_linter::rules::pep8_naming::settings::IgnoreNames;
use ruff_linter::rules::pydocstyle::settings::Convention;
use ruff_linter::rules::pylint::settings::ConstantType;
use ruff_linter::rules::{
    flake8_copyright, flake8_errmsg, flake8_gettext, flake8_implicit_str_concat,
    flake8_import_conventions, flake8_pytest_style, flake8_quotes, flake8_self,
    flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, isort, mccabe, pep8_naming,
    pycodestyle, pydoclint, pydocstyle, pyflakes, pylint, pyupgrade, ruff,
};
use ruff_linter::settings::types::{
    IdentifierPattern, Language, OutputFormat, PreviewMode, PythonVersion, RequiredVersion,
};
use ruff_linter::{UnresolvedRuleSelector, warn_user_once};
use ruff_macros::{CombineOptions, OptionsMetadata};
use ruff_options_metadata::{OptionsMetadata, Visit};
use ruff_python_ast::name::Name;
use ruff_python_formatter::{DocstringCodeLineWidth, QuoteStyle};
use ruff_python_semantic::NameImports;
use ruff_python_stdlib::identifiers::is_identifier;
use ruff_ranged_value::ValueSourceGuard;

#[derive(Clone, Debug, PartialEq, Eq, Default, OptionsMetadata, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Options {
    /// A path to the cache directory.
    ///
    /// By default, Ruff stores cache results in a `.ruff_cache` directory in
    /// the current project root.
    ///
    /// However, Ruff will also respect the `RUFF_CACHE_DIR` environment
    /// variable, which takes precedence over that default.
    ///
    /// This setting will override even the `RUFF_CACHE_DIR` environment
    /// variable, if set.
    #[option(
        default = r#"".ruff_cache""#,
        value_type = "str",
        example = r#"cache-dir = "~/.cache/ruff""#
    )]
    pub cache_dir: Option<String>,

    /// A path to a local `pyproject.toml` or `ruff.toml` file to merge into this
    /// configuration. User home directory and environment variables will be
    /// expanded.
    ///
    /// To resolve the current configuration file, Ruff will first load
    /// this base configuration file, then merge in properties defined
    /// in the current configuration file. Most settings follow simple override
    /// behavior where the child value replaces the parent value. However,
    /// rule selection (`lint.select` and `lint.ignore`) has special merging
    /// behavior: if the child configuration specifies `lint.select`, it
    /// establishes a new baseline rule set and the parent's `lint.ignore`
    /// rules are discarded; if the child configuration omits `lint.select`,
    /// the parent's rule selection is inherited and both parent and child
    /// `lint.ignore` rules are accumulated together.
    #[option(
        default = r#"null"#,
        value_type = "str",
        example = r#"
            # Extend the `pyproject.toml` file in the parent directory.
            extend = "../pyproject.toml"
            # But use a different line length.
            line-length = 100
        "#
    )]
    pub extend: Option<String>,

    /// The style in which violation messages should be formatted: `"full"` (default)
    /// (shows source), `"concise"`, `"grouped"` (group messages by file), `"json"`
    /// (machine-readable), `"junit"` (machine-readable XML), `"github"` (GitHub
    /// Actions annotations), `"gitlab"` (GitLab CI code quality report),
    /// `"pylint"` (Pylint text format) or `"azure"` (Azure Pipeline logging commands).
    #[option(
        default = r#""full""#,
        value_type = r#""full" | "concise" | "grouped" | "json" | "junit" | "github" | "gitlab" | "pylint" | "azure""#,
        example = r#"
            # Group violations by containing file.
            output-format = "grouped"
        "#
    )]
    pub output_format: Option<OutputFormat>,

    /// Whether to prefer rule codes over human-readable rule names in diagnostic output, even
    /// when preview mode is enabled.
    ///
    /// Diagnostics without rule codes, such as syntax errors and formatting diagnostics, will
    /// continue to use the human-readable name, but those corresponding to lint rules will use the
    /// rule's code. For example, the concise diagnostic for an unused import will use the code
    /// `F401` instead of the name `unused-import`:
    ///
    /// ```console
    /// $ ruff check --preview --config 'output-prefer-rule-codes = true' --output-format=concise example.py
    /// example.py:1:8: F401 [*] `math` imported but unused
    /// $ ruff check --preview --config 'output-prefer-rule-codes = false' --output-format=concise example.py
    /// example.py:1:8: unused-import: [*] `math` imported but unused
    /// ```
    #[option(
        default = "false",
        value_type = "bool",
        example = r#"
            # Display rule codes instead of human-readable rule names.
            output-prefer-rule-codes = true
        "#
    )]
    pub output_prefer_rule_codes: Option<bool>,

    /// Enable fix behavior by-default when running `ruff` (overridden
    /// by the `--fix` and `--no-fix` command-line flags).
    /// Only includes automatic fixes unless `--unsafe-fixes` is provided.
    #[option(default = "false", value_type = "bool", example = "fix = true")]
    pub fix: Option<bool>,

    /// Enable application of unsafe fixes.
    /// If excluded, a hint will be displayed when unsafe fixes are available.
    /// If set to false, the hint will be hidden.
    #[option(
        default = r#"null"#,
        value_type = "bool",
        example = "unsafe-fixes = true"
    )]
    pub unsafe_fixes: Option<bool>,

    /// Like [`fix`](#fix), but disables reporting on leftover violation. Implies [`fix`](#fix).
    #[option(default = "false", value_type = "bool", example = "fix-only = true")]
    pub fix_only: Option<bool>,

    /// Whether to show an enumeration of all fixed lint violations
    /// (overridden by the `--show-fixes` command-line flag).
    #[option(
        default = "false",
        value_type = "bool",
        example = r#"
            # Enumerate all fixed violations.
            show-fixes = true
        "#
    )]
    pub show_fixes: Option<bool>,

    /// Enforce a requirement on the version of Ruff, to enforce at runtime.
    /// If the version of Ruff does not meet the requirement, Ruff will exit
    /// with an error.
    ///
    /// Useful for unifying results across many environments, e.g., with a
    /// `pyproject.toml` file.
    ///
    /// Accepts a [PEP 440](https://peps.python.org/pep-0440/) specifier, like `==0.3.1` or `>=0.3.1`.
    #[option(
        default = "null",
        value_type = "str",
        example = r#"
            required-version = ">=0.0.193"
        "#
    )]
    pub required_version: Option<RequiredVersion>,

    /// Whether to enable preview mode. When preview mode is enabled, Ruff will
    /// use unstable rules, fixes, and formatting.
    #[option(
        default = "false",
        value_type = "bool",
        example = r#"
            # Enable preview features.
            preview = true
        "#
    )]
    pub preview: Option<bool>,

    // File resolver options
    /// A list of file patterns to exclude from formatting and linting.
    ///
    /// Exclusions are based on globs, and can be either:
    ///
    /// - Single-path patterns, like `.mypy_cache` (to exclude any directory
    ///   named `.mypy_cache` in the tree), `foo.py` (to exclude any file named
    ///   `foo.py`), or `foo_*.py` (to exclude any file matching `foo_*.py` ).
    /// - Relative patterns, like `directory/foo.py` (to exclude that specific
    ///   file) or `directory/*.py` (to exclude any Python files in
    ///   `directory`). Note that these paths are relative to the project root
    ///   (e.g., the directory containing your `pyproject.toml`).
    ///
    /// For more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).
    ///
    /// Note that you'll typically want to use
    /// [`extend-exclude`](#extend-exclude) to modify the excluded paths.
    #[option(
        default = r#"[".bzr", ".direnv", ".eggs", ".git", ".git-rewrite", ".hg", ".mypy_cache", ".nox", ".pants.d", ".pytype", ".ruff_cache", ".svn", ".tox", ".venv", "__pypackages__", "_build", "buck-out", "dist", "node_modules", "venv"]"#,
        value_type = "list[str]",
        example = r#"
            exclude = [".venv"]
        "#
    )]
    pub exclude: Option<Vec<String>>,

    /// A list of file patterns to omit from formatting and linting, in addition to those
    /// specified by [`exclude`](#exclude).
    ///
    /// Exclusions are based on globs, and can be either:
    ///
    /// - Single-path patterns, like `.mypy_cache` (to exclude any directory
    ///   named `.mypy_cache` in the tree), `foo.py` (to exclude any file named
    ///   `foo.py`), or `foo_*.py` (to exclude any file matching `foo_*.py` ).
    /// - Relative patterns, like `directory/foo.py` (to exclude that specific
    ///   file) or `directory/*.py` (to exclude any Python files in
    ///   `directory`). Note that these paths are relative to the project root
    ///   (e.g., the directory containing your `pyproject.toml`).
    ///
    /// For more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).
    #[option(
        default = "[]",
        value_type = "list[str]",
        example = r#"
            # In addition to the standard set of exclusions, omit all tests, plus a specific file.
            extend-exclude = ["tests", "src/bad.py"]
        "#
    )]
    pub extend_exclude: Option<Vec<String>>,

    /// A list of file patterns to include when linting, in addition to those
    /// specified by [`include`](#include).
    ///
    /// Inclusion are based on globs, and should be single-path patterns, like
    /// `*.pyw`, to include any file with the `.pyw` extension.
    ///
    /// For more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).
    #[option(
        default = "[]",
        value_type = "list[str]",
        example = r#"
            # In addition to the standard set of inclusions, include `.pyw` files.
            extend-include = ["*.pyw"]
        "#
    )]
    pub extend_include: Option<Vec<String>>,

    /// Whether to enforce [`exclude`](#exclude) and [`extend-exclude`](#extend-exclude) patterns,
    /// even for paths that are passed to Ruff explicitly. Typically, Ruff will lint
    /// any paths passed in directly, even if they would typically be
    /// excluded. Setting `force-exclude = true` will cause Ruff to
    /// respect these exclusions unequivocally.
    ///
    /// This is useful for [`pre-commit`](https://pre-commit.com/), which explicitly passes all
    /// changed files to the [`ruff-pre-commit`](https://github.com/astral-sh/ruff-pre-commit)
    /// plugin, regardless of whether they're marked as excluded by Ruff's own
    /// settings.
    #[option(
        default = r#"false"#,
        value_type = "bool",
        example = r#"
            force-exclude = true
        "#
    )]
    pub force_exclude: Option<bool>,

    /// A list of file patterns to include when linting.
    ///
    /// Inclusion are based on globs, and should be single-path patterns, like
    /// `*.pyw`, to include any file with the `.pyw` extension.
    /// `pyproject.toml`, `ruff.toml`, and `.ruff.toml` are included here not for
    /// configuration but because we lint whether e.g. the `[project]` matches
    /// the schema in `pyproject.toml` or that rule names are used as selectors.
    ///
    /// Notebook files (`.ipynb` extension) are included by default on Ruff 0.6.0+.
    ///
    /// For more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).
    #[option(
        default = r#"["*.py", "*.pyi", "*.pyw", "*.ipynb", "*.md", "**/pyproject.toml", "**/ruff.toml", "**/.ruff.toml"]"#,
        value_type = "list[str]",
        example = r#"
            include = ["*.py"]
        "#
    )]
    pub include: Option<Vec<String>>,

    /// Whether to automatically exclude files that are ignored by `.ignore`,
    /// `.gitignore`, `.git/info/exclude`, and global `gitignore` files.
    /// Enabled by default.
    #[option(
        default = "true",
        value_type = "bool",
        example = r#"
            respect-gitignore = false
        "#
    )]
    pub respect_gitignore: Option<bool>,

    /// A mapping of custom file extensions to known file types (overridden
    /// by the `--extension` command-line flag).
    ///
    /// Supported file types include `python`, `pyi`, `ipynb`, and `markdown`.
    ///
    /// Any file extensions listed here will be automatically added to the
    /// default `include` list as a `*.{ext}` glob, so that they are linted
    /// and formatted without needing any additional configuration settings.
    #[option(
        default = "{}",
        value_type = "dict[str, Language]",
        example = r#"
            # Add a custom file extension mapped to Python
            extension = {rpy="python"}
        "#
    )]
    pub extension: Option<FxHashMap<String, Language>>,

    // Generic python options
    /// A list of builtins to treat as defined references, in addition to the
    /// system builtins.
    #[option(
        default = r#"[]"#,
        value_type = "list[str]",
        example = r#"
            builtins = ["_"]
        "#
    )]
    pub builtins: Option<Vec<String>>,

    /// Mark the specified directories as namespace packages. For the purpose of
    /// module resolution, Ruff will treat those directories and all their subdirectories
    /// as if they contained an `__init__.py` file.
    #[option(
        default = r#"[]"#,
        value_type = "list[str]",
        example = r#"
            namespace-packages = ["airflow/providers"]
        "#
    )]
    pub namespace_packages: Option<Vec<String>>,

    /// The minimum Python version to target, e.g., when considering automatic
    /// code upgrades, like rewriting type annotations. Ruff will not propose
    /// changes using features that are not available in the given version.
    ///
    /// For example, to represent supporting Python >=3.11 or ==3.11
    /// specify `target-version = "py311"`.
    ///
    /// If you're already using a `pyproject.toml` file, we recommend
    /// `project.requires-python` instead, as it's based on Python packaging
    /// standards, and will be respected by other tools. For example, Ruff
    /// treats the following as identical to `target-version = "py38"`:
    ///
    /// ```toml
    /// [project]
    /// requires-python = ">=3.8"
    /// ```
    ///
    /// If both are specified, `target-version` takes precedence over
    /// `requires-python`. See [_Inferring the Python version_](https://docs.astral.sh/ruff/configuration/#inferring-the-python-version)
    /// for a complete description of how the `target-version` is determined
    /// when left unspecified.
    ///
    /// Note that a stub file can [sometimes make use of a typing feature](https://typing.python.org/en/latest/spec/distributing.html#syntax)
    /// before it is available at runtime, as long as the stub does not make
    /// use of new *syntax*. For example, a type checker will understand
    /// `int | str` in a stub as being a `Union` type annotation, even if the
    /// type checker is run using Python 3.9, despite the fact that the `|`
    /// operator can only be used to create union types at runtime on Python
    /// 3.10+. As such, Ruff will often recommend newer features in a stub
    /// file than it would for an equivalent runtime file with the same target
    /// version.
    #[option(
        default = r#""py310""#,
        value_type = r#""py37" | "py38" | "py39" | "py310" | "py311" | "py312" | "py313" | "py314""#,
        example = r#"
            # Always generate Python 3.7-compatible code.
            target-version = "py37"
        "#
    )]
    pub target_version: Option<PythonVersion>,

    /// A list of mappings from glob-style file pattern to Python version to use when checking the
    /// corresponding file(s).
    ///
    /// This may be useful for overriding the global Python version settings in `target-version` or
    /// `requires-python` for a subset of files. For example, if you have a project with a minimum
    /// supported Python version of 3.9 but a subdirectory of developer scripts that want to use a
    /// newer feature like the `match` statement from Python 3.10, you can use
    /// `per-file-target-version` to specify `"developer_scripts/*.py" = "py310"`.
    ///
    /// This setting is used by the linter to enforce any enabled version-specific lint rules, as
    /// well as by the formatter for any version-specific formatting options, such as parenthesizing
    /// context managers on Python 3.10+.
    #[option(
        default = "{}",
        value_type = "dict[str, PythonVersion]",
        scope = "per-file-target-version",
        example = r#"
            # Override the project-wide Python version for a developer scripts directory:
            "scripts/*.py" = "py312"
        "#
    )]
    pub per_file_target_version: Option<FxHashMap<String, PythonVersion>>,

    /// The directories to consider when resolving first- vs. third-party
    /// imports.
    ///
    /// When omitted, the `src` directory will typically default to including both:
    ///
    /// 1. The directory containing the nearest `pyproject.toml`, `ruff.toml`, or `.ruff.toml` file (the "project root").
    /// 2. The `"src"` subdirectory of the project root.
    ///
    /// These defaults ensure that Ruff supports both flat layouts and `src` layouts out-of-the-box.
    /// (If a configuration file is explicitly provided (e.g., via the `--config` command-line
    /// flag), the current working directory will be considered the project root.)
    ///
    /// As an example, consider an alternative project structure, like:
    ///
    /// ```text
    /// my_project
    /// ├── pyproject.toml
    /// └── lib
    ///     └── my_package
    ///         ├── __init__.py
    ///         ├── foo.py
    ///         └── bar.py
    /// ```
    ///
    /// In this case, the `./lib` directory should be included in the `src` option
    /// (e.g., `src = ["lib"]`), such that when resolving imports, `my_package.foo`
    /// is considered first-party.
    ///
    /// This field supports globs. For example, if you have a series of Python
    /// packages in a `python_modules` directory, `src = ["python_modules/*"]`
    /// would expand to incorporate all packages in that directory. User home
    /// directory and environment variables will also be expanded.
    #[option(
        default = r#"[".", "src"]"#,
        value_type = "list[str]",
        example = r#"
            # Allow imports relative to the "src" and "test" directories.
            src = ["src", "test"]
        "#
    )]
    pub src: Option<Vec<String>>,

    // Global Formatting options
    /// The line length to use when enforcing long-lines violations (like `E501`)
    /// and at which `isort` and the formatter prefers to wrap lines.
    ///
    /// The length is determined by the number of characters per line, except for lines containing East Asian characters or emojis.
    /// For these lines, the [unicode width](https://unicode.org/reports/tr11/) of each character is added up to determine the length.
    ///
    /// The value must be greater than `0`.
    ///
    /// Note: While the formatter will attempt to format lines such that they remain
    /// within the `line-length`, it isn't a hard upper bound, and formatted lines may
    /// exceed the `line-length`.
    ///
    /// See [`pycodestyle.max-line-length`](#lint_pycodestyle_max-line-length) to configure different lengths for `E501` and the formatter.
    #[option(
        default = "88",
        value_type = "int",
        example = r#"
        # Allow lines to be as long as 120.
        line-length = 120
        "#
    )]
    pub line_length: Option<LineLength>,

    /// The number of spaces per indentation level (tab).
    ///
    /// Used by the formatter and when enforcing long-line violations (like `E501`) to determine the visual
    /// width of a tab.
    ///
    /// This option changes the number of spaces the formatter inserts when
    /// using soft-tabs (`indent-style = space`).
    ///
    /// PEP 8 recommends using 4 spaces per [indentation level](https://peps.python.org/pep-0008/#indentation).
    #[option(
        default = "4",
        value_type = "int",
        example = r#"
            indent-width = 2
        "#
    )]
    pub indent_width: Option<IndentWidth>,

    #[option_group]
    pub lint: Option<LintOptions>,

    /// The lint sections specified at the top level.
    #[serde(flatten)]
    pub lint_top_level: DeprecatedTopLevelLintOptions,

    /// Options to configure code formatting.
    #[option_group]
    pub format: Option<FormatOptions>,

    /// Options to configure import map generation.
    #[option_group]
    pub analyze: Option<AnalyzeOptions>,
}

impl Options {
    /// Deserialize inline configuration in one crate, avoiding repeated code generation.
    pub fn from_toml_table(table: toml::Table) -> Result<Self, toml::de::Error> {
        table.try_into()
    }
}

/// Configures how Ruff checks your code.
///
/// Options specified in the `lint` section take precedence over the deprecated top-level settings.
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Clone, Debug, PartialEq, Eq, Default, OptionsMetadata, Serialize, Deserialize)]
#[serde(
    from = "LintOptionsWire",
    deny_unknown_fields,
    rename_all = "kebab-case"
)]
#[cfg_attr(feature = "schemars", schemars(!from))]
pub struct LintOptions {
    #[serde(flatten)]
    pub common: LintCommonOptions,

    /// A list of file patterns to exclude from linting in addition to the files excluded globally (see [`exclude`](#exclude), and [`extend-exclude`](#extend-exclude)).
    ///
    /// Exclusions are based on globs, and can be either:
    ///
    /// - Single-path patterns, like `.mypy_cache` (to exclude any directory
    ///   named `.mypy_cache` in the tree), `foo.py` (to exclude any file named
    ///   `foo.py`), or `foo_*.py` (to exclude any file matching `foo_*.py` ).
    /// - Relative patterns, like `directory/foo.py` (to exclude that specific
    ///   file) or `directory/*.py` (to exclude any Python files in
    ///   `directory`). Note that these paths are relative to the project root
    ///   (e.g., the directory containing your `pyproject.toml`).
    ///
    /// For more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).
    #[option(
        default = r#"[]"#,
        value_type = "list[str]",
        example = r#"
            exclude = ["generated"]
        "#
    )]
    pub exclude: Option<Vec<String>>,

    /// Options for the `pydoclint` plugin.
    #[option_group]
    pub pydoclint: Option<PydoclintOptions>,

    /// Options for the `ruff` plugin
    #[option_group]
    pub ruff: Option<RuffOptions>,

    /// Whether to enable preview mode. When preview mode is enabled, Ruff will
    /// use unstable rules and fixes.
    #[option(
        default = "false",
        value_type = "bool",
        example = r#"
            # Enable preview features.
            preview = true
        "#
    )]
    pub preview: Option<bool>,

    /// Whether to allow imports from the third-party `typing_extensions` module for Python versions
    /// before a symbol was added to the first-party `typing` module.
    ///
    /// Many rules try to import symbols from the `typing` module but fall back to
    /// `typing_extensions` for earlier versions of Python. This option can be used to disable this
    /// fallback behavior in cases where `typing_extensions` is not installed.
    #[option(
        default = "true",
        value_type = "bool",
        example = r#"
            # Disable `typing_extensions` imports
            typing-extensions = false
        "#
    )]
    pub typing_extensions: Option<bool>,

    /// Whether to allow rules to add `from __future__ import annotations` in cases where this would
    /// simplify a fix or enable a new diagnostic.
    ///
    /// For example, `TC001`, `TC002`, and `TC003` can move more imports into `TYPE_CHECKING` blocks
    /// if `__future__` annotations are enabled.
    ///
    #[option(
        default = "false",
        value_type = "bool",
        example = r#"
            # Enable `from __future__ import annotations` imports
            future-annotations = true
        "#
    )]
    pub future_annotations: Option<bool>,
}

pub(crate) fn validate_required_version(required_version: &RequiredVersion) -> anyhow::Result<()> {
    let ruff_pkg_version = pep440_rs::Version::from_str(RUFF_PKG_VERSION)
        .expect("RUFF_PKG_VERSION is not a valid PEP 440 version specifier");
    if !required_version.contains(&ruff_pkg_version) {
        return Err(anyhow::anyhow!(
            "Required version `{required_version}` does not match the running version \
            `{RUFF_PKG_VERSION}`"
        ));
    }
    Ok(())
}

/// Newtype wrapper for [`LintCommonOptions`] that allows customizing the JSON schema and omitting the fields from the [`OptionsMetadata`].
#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize)]
#[serde(transparent)]
pub struct DeprecatedTopLevelLintOptions(pub(crate) LintCommonOptions);

impl<'de> Deserialize<'de> for DeprecatedTopLevelLintOptions {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        // Temporarily disable span information because the flattened values don't retain spans.
        let _guard = ValueSourceGuard::without_spans();
        LintCommonOptions::deserialize(deserializer).map(Self)
    }
}

impl OptionsMetadata for DeprecatedTopLevelLintOptions {
    fn record(_visit: &mut dyn Visit) {
        // Intentionally empty. Omit all fields from the documentation and instead promote the options under the `lint.` section.
        // This doesn't create an empty 'common' option  because the field in the `Options` struct is marked with `#[serde(flatten)]`.
        // Meaning, the code here flattens no-properties into the parent, which is what we want.
    }
}

#[cfg(feature = "schemars")]
impl schemars::JsonSchema for DeprecatedTopLevelLintOptions {
    fn schema_name() -> std::borrow::Cow<'static, str> {
        std::borrow::Cow::Borrowed("DeprecatedTopLevelLintOptions")
    }
    fn schema_id() -> std::borrow::Cow<'static, str> {
        std::borrow::Cow::Borrowed(concat!(
            module_path!(),
            "::",
            "DeprecatedTopLevelLintOptions"
        ))
    }
    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
        use serde_json::Value;

        let mut schema = LintCommonOptions::json_schema(generator);
        if let Some(properties) = schema
            .ensure_object()
            .get_mut("properties")
            .and_then(|value| value.as_object_mut())
        {
            for property in properties.values_mut() {
                if let Ok(property_schema) = <&mut schemars::Schema>::try_from(property) {
                    property_schema
                        .ensure_object()
                        .insert("deprecated".to_string(), Value::Bool(true));
                }
            }
        }

        schema
    }
}

// Note: This struct should be inlined into [`LintOptions`] once support for the top-level lint settings
// is removed.
// Don't add any new options to this struct. Add them to [`LintOptions`] directly to avoid exposing them in the
// global settings.
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(
    Clone, Debug, PartialEq, Eq, Default, OptionsMetadata, CombineOptions, Serialize, Deserialize,
)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct LintCommonOptions {
    // WARNING: Don't add new options to this type. Add them to `LintOptions` instead.
    /// A list of allowed "confusable" Unicode characters to ignore when
    /// enforcing `RUF001`, `RUF002`, and `RUF003`.
    #[option(
        default = r#"[]"#,
        value_type = "list[str]",
        example = r#"
            # Allow minus-sign (U+2212), greek-small-letter-rho (U+03C1), and the asterisk-operator (U+2217),
            # which could be confused for "-", "p", and "*", respectively.
            allowed-confusables = ["−", "ρ", "∗"]
        "#
    )]
    pub allowed_confusables: Option<Vec<char>>,

    /// A regular expression used to identify "dummy" variables, or those which
    /// should be ignored when enforcing (e.g.) unused-variable rules. The
    /// default expression matches `_`, `__`, and `_var`, but not `_var_`.
    #[option(
        default = r#""^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$""#,
        value_type = "str",
        example = r#"
            # Only ignore variables named "_".
            dummy-variable-rgx = "^_$"
        "#
    )]
    pub dummy_variable_rgx: Option<String>,

    /// A list of rule codes or prefixes to ignore, in addition to those
    /// specified by `ignore`.
    ///
    /// This option is deprecated because it is now interchangeable with
    /// [`ignore`](#lint_ignore). In earlier versions of Ruff, `ignore` would
    /// _replace_ the set of ignored rules when using configuration inheritance
    /// (via the top-level [`extend`](https://docs.astral.sh/ruff/settings/#extend)
    /// setting), while `extend-ignore` would _add_ to the inherited set. Ruff
    /// now merges both `ignore` and `extend-ignore` into a single set, so the
    /// distinction no longer applies. Use [`ignore`](#lint_ignore) instead.
    #[option(
        default = "[]",
        value_type = "list[RuleSelector]",
        example = r#"
            # Skip unused variable rules (`F841`).
            extend-ignore = ["F841"]
        "#
    )]
    #[deprecated(note = "The `extend-ignore` option is now interchangeable with \
        [`ignore`](#lint_ignore). Please update your configuration to use the \
        [`ignore`](#lint_ignore) option instead.")]
    pub extend_ignore: Option<Vec<UnresolvedRuleSelector>>,

    /// A list of rule codes or prefixes to enable, in addition to those
    /// specified by [`select`](#lint_select).
    ///
    /// Unlike [`select`](#lint_select), which _replaces_ the default rule set
    /// when specified, `extend-select` _adds_ to whatever rules are already
    /// active. This makes `extend-select` the preferred option when you want
    /// to enable additional rules on top of the defaults without having to
    /// enumerate them.
    ///
    /// For example, to enable the defaults plus flake8-bugbear:
    ///
    /// ```toml
    /// [tool.ruff.lint]
    /// # Adds flake8-bugbear on top of the default rules.
    /// extend-select = ["B"]
    /// ```
    ///
    /// Using `select = ["B"]` instead would _replace_ the defaults, enabling
    /// only flake8-bugbear.
    #[option(
        default = "[]",
        value_type = "list[RuleSelector]",
        example = r#"
            # On top of the default `select`, enable flake8-bugbear (`B`) and flake8-quotes (`Q`).
            extend-select = ["B", "Q"]
        "#
    )]
    pub extend_select: Option<Vec<UnresolvedRuleSelector>>,

    /// A list of rule codes or prefixes to consider fixable, in addition to those
    /// specified by [`fixable`](#lint_fixable).
    #[option(
        default = r#"[]"#,
        value_type = "list[RuleSelector]",
        example = r#"
            # Enable fix for flake8-bugbear (`B`), on top of any rules specified by `fixable`.
            extend-fixable = ["B"]
        "#
    )]
    pub extend_fixable: Option<Vec<UnresolvedRuleSelector>>,

    /// A list of rule codes or prefixes to consider non-auto-fixable, in addition to those
    /// specified by [`unfixable`](#lint_unfixable).
    #[deprecated(note = "The `extend-unfixable` option is now interchangeable with \
        [`unfixable`](#lint_unfixable). Please update your configuration to \
        use the `unfixable` option instead.")]
    pub extend_unfixable: Option<Vec<UnresolvedRuleSelector>>,

    /// A list of rule codes or prefixes that are unsupported by Ruff, but should be
    /// preserved when (e.g.) validating `# noqa` directives. Useful for
    /// retaining `# noqa` directives that cover plugins not yet implemented
    /// by Ruff.
    #[option(
        default = "[]",
        value_type = "list[str]",
        example = r#"
            # Avoiding flagging (and removing) any codes starting with `V` from any
            # `# noqa` directives, despite Ruff's lack of support for `vulture`.
            external = ["V"]
        "#
    )]
    pub external: Option<Vec<String>>,

    /// A list of rule codes or prefixes to consider fixable. By default,
    /// all rules are considered fixable.
    #[option(
        default = r#"["ALL"]"#,
        value_type = "list[RuleSelector]",
        example = r#"
            # Only allow fix behavior for `E` and `F` rules.
            fixable = ["E", "F"]
        "#
    )]
    pub fixable: Option<Vec<UnresolvedRuleSelector>>,

    /// A list of rule codes or prefixes to ignore. Prefixes can specify exact
    /// rules (like `F841`), entire groups (like `F`), or anything in
    /// between.
    ///
    /// When breaking ties between enabled and disabled rules (via `select` and
    /// `ignore`, respectively), more specific prefixes override less
    /// specific prefixes. `ignore` takes precedence over `select` if the same
    /// prefix appears in both.
    ///
    /// In preview, categories like `correctness` and `suspicious` can be used
    /// in addition to rule codes and linter group prefixes.
    #[option(
        default = "[]",
        value_type = "list[RuleSelector]",
        example = r#"
            # Skip unused variable rules (`F841`).
            ignore = ["F841"]
        "#
    )]
    pub ignore: Option<Vec<UnresolvedRuleSelector>>,

    /// A list of rule codes or prefixes for which unsafe fixes should be considered
    /// safe.
    #[option(
        default = "[]",
        value_type = "list[RuleSelector]",
        example = r#"
            # Allow applying all unsafe fixes in the `E` rules and `F401` without the `--unsafe-fixes` flag
            extend-safe-fixes = ["E", "F401"]
        "#
    )]
    pub extend_safe_fixes: Option<Vec<UnresolvedRuleSelector>>,

    /// A list of rule codes or prefixes for which safe fixes should be considered
    /// unsafe.
    #[option(
        default = "[]",
        value_type = "list[RuleSelector]",
        example = r#"
            # Require the `--unsafe-fixes` flag when fixing the `E` rules and `F401`
            extend-unsafe-fixes = ["E", "F401"]
        "#
    )]
    pub extend_unsafe_fixes: Option<Vec<UnresolvedRuleSelector>>,

    /// Avoid automatically removing unused imports in `__init__.py` files. Such
    /// imports will still be flagged, but with a dedicated message suggesting
    /// that the import is either added to the module's `__all__` symbol, or
    /// re-exported with a redundant alias (e.g., `import os as os`).
    ///
    /// This option is enabled by default, but you can opt-in to removal of imports
    /// via an unsafe fix.
    #[option(
        default = "true",
        value_type = "bool",
        example = r#"
            ignore-init-module-imports = false
        "#
    )]
    #[deprecated(
        since = "0.4.4",
        note = "`ignore-init-module-imports` will be removed in a future version because F401 now \
            recommends appropriate fixes for unused imports in `__init__.py` (currently in \
            preview mode). See documentation for more information and please update your \
            configuration."
    )]
    pub ignore_init_module_imports: Option<bool>,

    /// A list of objects that should be treated equivalently to a
    /// `logging.Logger` object.
    ///
    /// This is useful for ensuring proper diagnostics (e.g., to identify
    /// `logging` deprecations and other best-practices) for projects that
    /// re-export a `logging.Logger` object from a common module.
    ///
    /// For example, if you have a module `logging_setup.py` with the following
    /// contents:
    /// ```python
    /// import logging
    ///
    /// logger = logging.getLogger(__name__)
    /// ```
    ///
    /// Adding `"logging_setup.logger"` to `logger-objects` will ensure that
    /// `logging_setup.logger` is treated as a `logging.Logger` object when
    /// imported from other modules (e.g., `from logging_setup import logger`).
    #[option(
        default = r#"[]"#,
        value_type = "list[str]",
        example = r#"logger-objects = ["logging_setup.logger"]"#
    )]
    pub logger_objects: Option<Vec<String>>,

    /// A list of rule codes or prefixes to enable. Prefixes can specify exact
    /// rules (like `F841`), entire groups (like `F`), or anything in
    /// between.
    ///
    /// When breaking ties between enabled and disabled rules (via `select` and
    /// `ignore`, respectively), more specific prefixes override less
    /// specific prefixes. `ignore` takes precedence over `select` if the
    /// same prefix appears in both.
    ///
    /// In preview, categories like `correctness` and `suspicious` can be used
    /// in addition to rule codes and linter group prefixes.
    #[option(
        default = r#"See https://docs.astral.sh/ruff/default-rules/ or run `ruff check --show-settings --isolated`"#,
        value_type = "list[RuleSelector]",
        example = r#"
            # On top of the defaults, enable flake8-bugbear (`B`) and flake8-quotes (`Q`).
            extend-select = ["B", "Q"]
        "#
    )]
    pub select: Option<Vec<UnresolvedRuleSelector>>,

    /// Whether to require exact codes to select preview rules. When enabled,
    /// preview rules will not be selected by prefixes — the full code of each
    /// preview rule will be required to enable the rule.
    #[option(
        default = "false",
        value_type = "bool",
        example = r#"
            # Require explicit selection of preview rules.
            explicit-preview-rules = true
        "#
    )]
    pub explicit_preview_rules: Option<bool>,

    /// A list of task tags to recognize (e.g., "TODO", "FIXME", "XXX").
    ///
    /// Comments starting with these tags will be ignored by commented-out code
    /// detection (`ERA`), and skipped by line-length rules (`E501`) if
    /// [`ignore-overlong-task-comments`](#lint_pycodestyle_ignore-overlong-task-comments) is set to `true`.
    #[option(
        default = r#"["TODO", "FIXME", "XXX"]"#,
        value_type = "list[str]",
        example = r#"
            task-tags = ["HACK"]
        "#
    )]
    pub task_tags: Option<Vec<String>>,

    /// A list of modules whose exports should be treated equivalently to
    /// members of the `typing` module.
    ///
    /// This is useful for ensuring proper type annotation inference for
    /// projects that re-export `typing` and `typing_extensions` members
    /// from a compatibility module. If omitted, any members imported from
    /// modules apart from `typing` and `typing_extensions` will be treated
    /// as ordinary Python objects.
    #[option(
        default = r#"[]"#,
        value_type = "list[str]",
        example = r#"typing-modules = ["airflow.typing_compat"]"#
    )]
    pub typing_modules: Option<Vec<String>>,

    /// A list of rule codes or prefixes to consider non-fixable.
    #[option(
        default = "[]",
        value_type = "list[RuleSelector]",
        example = r#"
            # Disable fix for unused imports (`F401`).
            unfixable = ["F401"]
        "#
    )]
    pub unfixable: Option<Vec<UnresolvedRuleSelector>>,

    // WARNING: Don't add new options to this type. Add them to `LintOptions` instead.
    /// Options for the `flake8-annotations` plugin.
    #[option_group]
    pub flake8_annotations: Option<Flake8AnnotationsOptions>,

    /// Options for the `flake8-bandit` plugin.
    #[option_group]
    pub flake8_bandit: Option<Flake8BanditOptions>,

    /// Options for the `flake8-boolean-trap` plugin.
    #[option_group]
    pub flake8_boolean_trap: Option<Flake8BooleanTrapOptions>,

    /// Options for the `flake8-bugbear` plugin.
    #[option_group]
    pub flake8_bugbear: Option<Flake8BugbearOptions>,

    /// Options for the `flake8-builtins` plugin.
    #[option_group]
    pub flake8_builtins: Option<Flake8BuiltinsOptions>,

    /// Options for the `flake8-comprehensions` plugin.
    #[option_group]
    pub flake8_comprehensions: Option<Flake8ComprehensionsOptions>,

    /// Options for the `flake8-copyright` plugin.
    #[option_group]
    pub flake8_copyright: Option<Flake8CopyrightOptions>,

    /// Options for the `flake8-errmsg` plugin.
    #[option_group]
    pub flake8_errmsg: Option<Flake8ErrMsgOptions>,

    /// Options for the `flake8-quotes` plugin.
    #[option_group]
    pub flake8_quotes: Option<Flake8QuotesOptions>,

    /// Options for the `flake8_self` plugin.
    #[option_group]
    pub flake8_self: Option<Flake8SelfOptions>,

    /// Options for the `flake8-tidy-imports` plugin.
    #[option_group]
    pub flake8_tidy_imports: Option<Flake8TidyImportsOptions>,

    /// Options for the `flake8-type-checking` plugin.
    #[option_group]
    pub flake8_type_checking: Option<Flake8TypeCheckingOptions>,

    /// Options for the `flake8-gettext` plugin.
    #[option_group]
    pub flake8_gettext: Option<Flake8GetTextOptions>,

    /// Options for the `flake8-implicit-str-concat` plugin.
    #[option_group]
    pub flake8_implicit_str_concat: Option<Flake8ImplicitStrConcatOptions>,

    /// Options for the `flake8-import-conventions` plugin.
    #[option_group]
    pub flake8_import_conventions: Option<Flake8ImportConventionsOptions>,

    /// Options for the `flake8-pytest-style` plugin.
    #[option_group]
    pub flake8_pytest_style: Option<Flake8PytestStyleOptions>,

    /// Options for the `flake8-unused-arguments` plugin.
    #[option_group]
    pub flake8_unused_arguments: Option<Flake8UnusedArgumentsOptions>,

    /// Options for the `isort` plugin.
    #[option_group]
    pub isort: Option<IsortOptions>,

    /// Options for the `mccabe` plugin.
    #[option_group]
    pub mccabe: Option<McCabeOptions>,

    /// Options for the `pep8-naming` plugin.
    #[option_group]
    pub pep8_naming: Option<Pep8NamingOptions>,

    /// Options for the `pycodestyle` plugin.
    #[option_group]
    pub pycodestyle: Option<PycodestyleOptions>,

    /// Options for the `pydocstyle` plugin.
    #[option_group]
    pub pydocstyle: Option<PydocstyleOptions>,

    /// Options for the `pyflakes` plugin.
    #[option_group]
    pub pyflakes: Option<PyflakesOptions>,

    /// Options for the `pylint` plugin.
    #[option_group]
    pub pylint: Option<PylintOptions>,

    /// Options for the `pyupgrade` plugin.
    #[option_group]
    pub pyupgrade: Option<PyUpgradeOptions>,

    // WARNING: Don't add new options to this type. Add them to `LintOptions` instead.

    // Tables are required to go last.
    /// A list of mappings from file pattern to rule codes or prefixes to
    /// exclude, when considering any matching files. An initial '!' negates
    /// the file pattern.
    #[option(
        default = "{}",
        value_type = "dict[str, list[RuleSelector]]",
        scope = "per-file-ignores",
        example = r#"
            # Ignore `E402` (import violations) in all `__init__.py` files, and in `path/to/file.py`.
            "__init__.py" = ["E402"]
            "path/to/file.py" = ["E402"]
            # Ignore `D` rules everywhere except for the `src/` directory.
            "!src/**.py" = ["D"]
        "#
    )]
    pub per_file_ignores: Option<FxHashMap<String, Vec<UnresolvedRuleSelector>>>,

    /// A list of mappings from file pattern to rule codes or prefixes to
    /// exclude, in addition to any rules excluded by [`per-file-ignores`](#lint_per-file-ignores).
    #[option(
        default = "{}",
        value_type = "dict[str, list[RuleSelector]]",
        scope = "extend-per-file-ignores",
        example = r#"
            # Also ignore `E402` in all `__init__.py` files.
            "__init__.py" = ["E402"]
        "#
    )]
    pub extend_per_file_ignores: Option<FxHashMap<String, Vec<UnresolvedRuleSelector>>>,
    // WARNING: Don't add new options to this type. Add them to `LintOptions` instead.
}

/// Options for the `flake8-annotations` plugin.
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(
    Clone, Debug, PartialEq, Eq, Default, OptionsMetadata, CombineOptions, Serialize, Deserialize,
)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Flake8AnnotationsOptions {
    /// Whether to allow the omission of a return type hint for `__init__` if at
    /// least one argument is annotated.
    #[option(
        default = "false",
        value_type = "bool",
        example = "mypy-init-return = true"
    )]
    mypy_init_return: Option<bool>,

    /// Whether to suppress `ANN000`-level violations for arguments matching the
    /// "dummy" variable regex (like `_`).
    #[option(
        default = "false",
        value_type = "bool",
        example = "suppress-dummy-args = true"
    )]
    suppress_dummy_args: Option<bool>,

    /// Whether to suppress `ANN200`-level violations for functions that meet
    /// either of the following criteria:
    ///
    /// - Contain no `return` statement.
    /// - Explicit `return` statement(s) all return `None` (explicitly or
    ///   implicitly).
    #[option(
        default = "false",
        value_type = "bool",
        example = "suppress-none-returning = true"
    )]
    suppress_none_returning: Option<bool>,

    /// Whether to suppress `ANN401` for dynamically typed `*args` and
    /// `**kwargs` arguments.
    #[option(
        default = "false",
        value_type = "bool",
        example = "allow-star-arg-any = true"
    )]
    allow_star_arg_any: Option<bool>,

    /// Whether to suppress `ANN*` rules for any declaration
    /// that hasn't been typed at all.
    /// This makes it easier to gradually add types to a codebase.
    #[option(
        default = "false",
        value_type = "bool",
        example = "ignore-fully-untyped = true"
    )]
    ignore_fully_untyped: Option<bool>,
}

impl Flake8AnnotationsOptions {
    pub(crate) fn into_settings(
        self,
    ) -> ruff_linter::rules::flake8_annotations::settings::Settings {
        ruff_linter::rules::flake8_annotations::settings::Settings {
            mypy_init_return: self.mypy_init_return.unwrap_or(false),
            suppress_dummy_args: self.suppress_dummy_args.unwrap_or(false),
            suppress_none_returning: self.suppress_none_returning.unwrap_or(false),
            allow_star_arg_any: self.allow_star_arg_any.unwrap_or(false),
            ignore_fully_untyped: self.ignore_fully_untyped.unwrap_or(false),
        }
    }
}

/// Options for the `flake8-bandit` plugin.
#[derive(
    Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize, OptionsMetadata, CombineOptions,
)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Flake8BanditOptions {
    /// A list of directories to consider temporary (see `S108`).
    #[option(
        default = "[\"/tmp\", \"/var/tmp\", \"/dev/shm\"]",
        value_type = "list[str]",
        example = "hardcoded-tmp-directory = [\"/foo/bar\"]"
    )]
    hardcoded_tmp_directory: Option<Vec<String>>,

    /// A list of directories to consider temporary, in addition to those
    /// specified by [`hardcoded-tmp-directory`](#lint_flake8-bandit_hardcoded-tmp-directory) (see `S108`).
    #[option(
        default = "[]",
        value_type = "list[str]",
        example = "hardcoded-tmp-directory-extend = [\"/foo/bar\"]"
    )]
    hardcoded_tmp_directory_extend: Option<Vec<String>>,

    /// Whether to disallow `try`-`except`-`pass` (`S110`) for specific
    /// exception types. By default, `try`-`except`-`pass` is only
    /// disallowed for `Exception` and `BaseException`.
    #[option(
        default = "false",
        value_type = "bool",
        example = "check-typed-exception = true"
    )]
    check_typed_exception: Option<bool>,

    /// A list of additional callable names that behave like
    /// [`markupsafe.Markup`](https://markupsafe.palletsprojects.com/en/stable/escaping/#markupsafe.Markup).
    ///
    /// Expects to receive a list of fully-qualified names (e.g., `webhelpers.html.literal`, rather than
    /// `literal`).
    #[option(
        default = "[]",
        value_type = "list[str]",
        example = "extend-markup-names = [\"webhelpers.html.literal\", \"my_package.Markup\"]"
    )]
    extend_markup_names: Option<Vec<String>>,

    /// A list of callable names, whose result may be safely passed into
    /// [`markupsafe.Markup`](https://markupsafe.palletsprojects.com/en/stable/escaping/#markupsafe.Markup).
    ///
    /// Expects to receive a list of fully-qualified names (e.g., `bleach.clean`, rather than `clean`).
    ///
    /// This setting helps you avoid false positives in code like:
    ///
    /// ```python
    /// from bleach import clean
    /// from markupsafe import Markup
    ///
    /// cleaned_markup = Markup(clean(some_user_input))
    /// ```
    ///
    /// Where the use of [`bleach.clean`](https://bleach.readthedocs.io/en/latest/clean.html)
    /// usually ensures that there's no XSS vulnerability.
    ///
    /// Although it is not recommended, you may also use this setting to whitelist other
    /// kinds of calls, e.g. calls to i18n translation functions, where how safe that is
    /// will depend on the implementation and how well the translations are audited.
    ///
    /// Another common use-case is to wrap the output of functions that generate markup
    /// like [`xml.etree.ElementTree.tostring`](https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.tostring)
    /// or template rendering engines where sanitization of potential user input is either
    /// already baked in or has to happen before rendering.
    #[option(
        default = "[]",
        value_type = "list[str]",
        example = "allowed-markup-calls = [\"bleach.clean\", \"my_package.sanitize\"]"
    )]
    allowed_markup_calls: Option<Vec<String>>,
}

impl Flake8BanditOptions {
    pub(crate) fn into_settings(
        self,
        ruff_options: Option<&RuffOptions>,
    ) -> ruff_linter::rules::flake8_bandit::settings::Settings {
        ruff_linter::rules::flake8_bandit::settings::Settings {
            hardcoded_tmp_directory: self
                .hardcoded_tmp_directory
                .unwrap_or_else(ruff_linter::rules::flake8_bandit::settings::default_tmp_dirs)
                .into_iter()
                .chain(self.hardcoded_tmp_directory_extend.unwrap_or_default())
                .collect(),
            check_typed_exception: self.check_typed_exception.unwrap_or(false),
            extend_markup_names: self
                .extend_markup_names
                .or_else(|| {
                    #[expect(deprecated)]
                    ruff_options.and_then(|options| options.extend_markup_names.clone())
                })
                .unwrap_or_default(),
            allowed_markup_calls: self
                .allowed_markup_calls
                .or_else(|| {
                    #[expect(deprecated)]
                    ruff_options.and_then(|options| options.allowed_markup_calls.clone())
                })
                .unwrap_or_default(),
        }
    }
}

/// Options for the `flake8-boolean-trap` plugin
#[derive(
    Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize, OptionsMetadata, CombineOptions,
)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Flake8BooleanTrapOptions {
    /// Additional callable functions with which to allow boolean traps.
    ///
    /// Expects to receive a list of fully-qualified names (e.g., `pydantic.Field`, rather than
    /// `Field`).
    #[option(
        default = "[]",
        value_type = "list[str]",
        example = "extend-allowed-calls = [\"pydantic.Field\", \"django.db.models.Value\"]"
    )]
    extend_allowed_calls: Option<Vec<String>>,
}

impl Flake8BooleanTrapOptions {
    pub(crate) fn into_settings(
        self,
    ) -> ruff_linter::rules::flake8_boolean_trap::settings::Settings {
        ruff_linter::rules::flake8_boolean_trap::settings::Settings {
            extend_allowed_calls: self.extend_allowed_calls.unwrap_or_default(),
        }
    }
}

/// Options for the `flake8-bugbear` plugin.
#[derive(
    Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize, OptionsMetadata, CombineOptions,
)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Flake8BugbearOptions {
    /// Additional callable functions to consider "immutable" when evaluating, e.g., the
    /// `function-call-in-default-argument` rule (`B008`) or `function-call-in-dataclass-defaults`
    /// rule (`RUF009`).
    ///
    /// Expects to receive a list of fully-qualified names (e.g., `fastapi.Query`, rather than
    /// `Query`).
    #[option(
        default = r#"[]"#,
        value_type = "list[str]",
        example = r#"
            # Allow default arguments like, e.g., `data: List[str] = fastapi.Query(None)`.
            extend-immutable-calls = ["fastapi.Depends", "fastapi.Query"]
        "#
    )]
    extend_immutable_calls: Option<Vec<String>>,
}

impl Flake8BugbearOptions {
    pub(crate) fn into_settings(self) -> ruff_linter::rules::flake8_bugbear::settings::Settings {
        ruff_linter::rules::flake8_bugbear::settings::Settings {
            extend_immutable_calls: self.extend_immutable_calls.unwrap_or_default(),
        }
    }
}

/// Options for the `flake8-builtins` plugin.
#[derive(
    Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize, OptionsMetadata, CombineOptions,
)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Flake8BuiltinsOptions {
    /// DEPRECATED: This option has been renamed to `ignorelist`. Use `ignorelist` instead.
    ///
    /// Ignore list of builtins.
    ///
    /// This option is ignored if both `ignorelist` and `builtins-ignorelist` are set.
    #[option(
        default = r#"[]"#,
        value_type = "list[str]",
        example = "builtins-ignorelist = [\"id\"]"
    )]
    #[deprecated(
        since = "0.10.0",
        note = "`builtins-ignorelist` has been renamed to `ignorelist`. Use that instead."
    )]
    pub(crate) builtins_ignorelist: Option<Vec<String>>,

    /// Ignore list of builtins.
    #[option(
        default = r#"[]"#,
        value_type = "list[str]",
        example = "ignorelist = [\"id\"]"
    )]
    pub(crate) ignorelist: Option<Vec<String>>,

    /// DEPRECATED: This option has been renamed to `allowed-modules`. Use `allowed-modules` instead.
    ///
    /// List of builtin module names to allow.
    ///
    /// This option is ignored if both `allowed-modules` and `builtins-allowed-modules` are set.
    #[option(
        default = r#"[]"#,
        value_type = "list[str]",
        example = "builtins-allowed-modules = [\"secrets\"]"
    )]
    #[deprecated(
        since = "0.10.0",
        note = "`builtins-allowed-modules` has been renamed to `allowed-modules`. \
            Use that instead."
    )]
    pub(crate) builtins_allowed_modules: Option<Vec<String>>,

    /// List of builtin module names to allow.
    #[option(
        default = r#"[]"#,
        value_type = "list[str]",
        example = "allowed-modules = [\"secrets\"]"
    )]
    pub(crate) allowed_modules: Option<Vec<String>>,

    /// DEPRECATED: This option has been renamed to `strict-checking`. Use `strict-checking` instead.
    ///
    /// Compare module names instead of full module paths.
    ///
    /// This option is ignored if both `strict-checking` and `builtins-strict-checking` are set.
    #[option(
        default = r#"false"#,
        value_type = "bool",
        example = "builtins-strict-checking = true"
    )]
    #[deprecated(
        since = "0.10.0",
        note = "`builtins-strict-checking` has been renamed to `strict-checking`. \
            Use that instead."
    )]
    pub(crate) builtins_strict_checking: Option<bool>,

    /// Compare module names instead of full module paths.
    ///
    /// Used by [`A005` - `stdlib-module-shadowing`](https://docs.astral.sh/ruff/rules/stdlib-module-shadowing/).
    #[option(
        default = r#"false"#,
        value_type = "bool",
        example = "strict-checking = true"
    )]
    pub(crate) strict_checking: Option<bool>,
}

impl Flake8BuiltinsOptions {
    pub(crate) fn into_settings(self) -> ruff_linter::rules::flake8_builtins::settings::Settings {
        #[expect(deprecated)]
        ruff_linter::rules::flake8_builtins::settings::Settings {
            ignorelist: self
                .ignorelist
                .or(self.builtins_ignorelist)
                .unwrap_or_default(),
            allowed_modules: self
                .allowed_modules
                .or(self.builtins_allowed_modules)
                .unwrap_or_default(),
            strict_checking: self
                .strict_checking
                .or(self.builtins_strict_checking)
                // use the old default of true on non-preview
                .unwrap_or_default(),
        }
    }
}

/// Options for the `flake8-comprehensions` plugin.
#[derive(
    Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize, OptionsMetadata, CombineOptions,
)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Flake8ComprehensionsOptions {
    /// Allow `dict` calls that make use of keyword arguments (e.g., `dict(a=1, b=2)`).
    #[option(
        default = "false",
        value_type = "bool",
        example = "allow-dict-calls-with-keyword-arguments = true"
    )]
    allow_dict_calls_with_keyword_arguments: Option<bool>,
}

impl Flake8ComprehensionsOptions {
    pub(crate) fn into_settings(
        self,
    ) -> ruff_linter::rules::flake8_comprehensions::settings::Settings {
        ruff_linter::rules::flake8_comprehensions::settings::Settings {
            allow_dict_calls_with_keyword_arguments: self
                .allow_dict_calls_with_keyword_arguments
                .unwrap_or_default(),
        }
    }
}

/// Options for the `flake8-copyright` plugin.
#[derive(
    Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize, OptionsMetadata, CombineOptions,
)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Flake8CopyrightOptions {
    /// The regular expression used to match the copyright notice, compiled
    /// with the [`regex`](https://docs.rs/regex/latest/regex/) crate.
    /// Defaults to `(?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})*`, which matches
    /// the following:
    ///
    /// - `Copyright 2023`
    /// - `Copyright (C) 2023`
    /// - `Copyright 2021-2023`
    /// - `Copyright (C) 2021-2023`
    /// - `Copyright (C) 2021, 2023`
    #[option(
        default = r#""(?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})*""#,
        value_type = "str",
        example = r#"notice-rgx = "(?i)Copyright \\(C\\) \\d{4}""#
    )]
    notice_rgx: Option<String>,

    /// Author to enforce within the copyright notice. If provided, the
    /// author must be present immediately following the copyright notice.
    #[option(default = "null", value_type = "str", example = r#"author = "Ruff""#)]
    author: Option<String>,

    /// A minimum file size (in bytes) required for a copyright notice to
    /// be enforced. By default, all files are validated.
    #[option(
        default = r#"0"#,
        value_type = "int",
        example = r#"
            # Avoid enforcing a header on files smaller than 1024 bytes.
            min-file-size = 1024
        "#
    )]
    min_file_size: Option<usize>,
}

impl Flake8CopyrightOptions {
    pub(crate) fn try_into_settings(self) -> anyhow::Result<flake8_copyright::settings::Settings> {
        Ok(flake8_copyright::settings::Settings {
            notice_rgx: self
                .notice_rgx
                .map(|pattern| Regex::new(&pattern))
                .transpose()?
                .unwrap_or_else(|| flake8_copyright::settings::COPYRIGHT.clone()),
            author: self.author,
            min_file_size: self.min_file_size.unwrap_or_default(),
        })
    }
}

/// Options for the `flake8-errmsg` plugin.
#[derive(
    Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize, OptionsMetadata, CombineOptions,
)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Flake8ErrMsgOptions {
    /// Maximum string length for string literals in exception messages.
    #[option(default = "0", value_type = "int", example = "max-string-length = 20")]
    max_string_length: Option<usize>,
}

impl Flake8ErrMsgOptions {
    pub(crate) fn into_settings(self) -> flake8_errmsg::settings::Settings {
        flake8_errmsg::settings::Settings {
            max_string_length: self.max_string_length.unwrap_or_default(),
        }
    }
}

/// Options for the `flake8-gettext` plugin.
#[derive(
    Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize, OptionsMetadata, CombineOptions,
)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Flake8GetTextOptions {
    /// The function names to consider as internationalization calls.
    #[option(
        default = r#"["_", "gettext", "ngettext"]"#,
        value_type = "list[str]",
        example = r#"function-names = ["_", "gettext", "ngettext", "ugettetxt"]"#
    )]
    function_names: Option<Vec<Name>>,

    /// Additional function names to consider as internationalization calls, in addition to those
    /// included in [`function-names`](#lint_flake8-gettext_function-names).
    #[option(
        default = r#"[]"#,
        value_type = "list[str]",
        example = r#"extend-function-names = ["ugettetxt"]"#
    )]
    extend_function_names: Option<Vec<Name>>,
}

impl Flake8GetTextOptions {
    pub(crate) fn into_settings(self) -> flake8_gettext::settings::Settings {
        flake8_gettext::settings::Settings {
            function_names: self
                .function_names
                .unwrap_or_else(flake8_gettext::settings::default_func_names)
                .into_iter()
                .chain(self.extend_function_names.unwrap_or_default())
                .collect(),
        }
    }
}

/// Options for the `flake8-implicit-str-concat` plugin
#[derive(
    Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize, OptionsMetadata, CombineOptions,
)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Flake8ImplicitStrConcatOptions {
    /// Whether to allow implicit string concatenations for multiline strings.
    /// By default, implicit concatenations of multiline strings are
    /// allowed (but continuation lines, delimited with a backslash, are
    /// prohibited).
    ///
    /// Setting `allow-multiline = false` will automatically disable the
    /// `explicit-string-concatenation` (`ISC003`) rule. Otherwise, both
    /// implicit and explicit multiline string concatenations would be seen
    /// as violations, making it impossible to write a linter-compliant multiline
    /// string.
    #[option(
        default = r#"true"#,
        value_type = "bool",
        example = r#"
            allow-multiline = false
        "#
    )]
    allow_multiline: Option<bool>,
}

impl Flake8ImplicitStrConcatOptions {
    pub(crate) fn into_settings(self) -> flake8_implicit_str_concat::settings::Settings {
        flake8_implicit_str_concat::settings::Settings {
            allow_multiline: self.allow_multiline.unwrap_or(true),
        }
    }
}

/// Options for the `flake8-import-conventions` plugin
#[derive(
    Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize, OptionsMetadata, CombineOptions,
)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Flake8ImportConventionsOptions {
    /// The conventional aliases for imports. These aliases can be extended by
    /// the [`extend-aliases`](#lint_flake8-import-conventions_extend-aliases) option.
    #[option(
        default = r#"{"altair": "alt", "matplotlib": "mpl", "matplotlib.pyplot": "plt", "numpy": "np", "numpy.typing": "npt", "pandas": "pd", "seaborn": "sns", "tensorflow": "tf", "tkinter":  "tk", "holoviews": "hv", "panel": "pn", "plotly.express": "px", "polars": "pl", "pyarrow": "pa", "xml.etree.ElementTree": "ET"}"#,
        value_type = "dict[str, str]",
        scope = "aliases",
        example = r#"
            # Declare the default aliases.
            altair = "alt"
            "matplotlib.pyplot" = "plt"
            numpy = "np"
            pandas = "pd"
            seaborn = "sns"
            scipy = "sp"
        "#
    )]
    aliases: Option<FxHashMap<ModuleName, Alias>>,

    /// A mapping from module to conventional import alias. These aliases will
    /// be added to the [`aliases`](#lint_flake8-import-conventions_aliases) mapping
    /// and will override any existing `aliases` if the two settings overlap.
    #[option(
        default = r#"{}"#,
        value_type = "dict[str, str]",
        scope = "extend-aliases",
        example = r#"
            # Declare a custom alias for the `dask` module.
            "dask.dataframe" = "dd"
        "#
    )]
    extend_aliases: Option<FxHashMap<ModuleName, Alias>>,

    /// A mapping from module to its banned import aliases.
    #[option(
        default = r#"{}"#,
        value_type = "dict[str, list[str]]",
        scope = "banned-aliases",
        example = r#"
            # Declare the banned aliases.
            "tensorflow.keras.backend" = ["K"]
    "#
    )]
    banned_aliases: Option<FxHashMap<String, BannedAliases>>,

    /// A list of modules that should not be imported from using the
    /// `from ... import ...` syntax.
    ///
    /// For example, given `banned-from = ["pandas"]`, `from pandas import DataFrame`
    /// would be disallowed, while `import pandas` would be allowed.
    #[option(
        default = r#"[]"#,
        value_type = "list[str]",
        example = r#"
            # Declare the banned `from` imports.
            banned-from = ["typing"]
    "#
    )]
    banned_from: Option<FxHashSet<String>>,
}

#[derive(Clone, Debug, PartialEq, Eq, Hash, Default, Serialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ModuleName(String);

impl ModuleName {
    fn into_string(self) -> String {
        self.0
    }
}

impl<'de> Deserialize<'de> for ModuleName {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let name = String::deserialize(deserializer)?;
        if name.is_empty() || name.split('.').any(|part| !is_identifier(part)) {
            Err(de::Error::invalid_value(
                de::Unexpected::Str(&name),
                &"a sequence of Python identifiers delimited by periods",
            ))
        } else {
            Ok(Self(name))
        }
    }
}

#[derive(Clone, Debug, PartialEq, Eq, Hash, Default, Serialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Alias(String);

impl Alias {
    fn into_string(self) -> String {
        self.0
    }
}

impl<'de> Deserialize<'de> for Alias {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let name = String::deserialize(deserializer)?;
        // Assigning to "__debug__" is a SyntaxError
        // see the note here:
        // https://docs.python.org/3/library/constants.html#debug__
        if &*name == "__debug__" {
            return Err(de::Error::invalid_value(
                de::Unexpected::Str(&name),
                &"an assignable Python identifier",
            ));
        }
        if is_identifier(&name) {
            Ok(Self(name))
        } else {
            Err(de::Error::invalid_value(
                de::Unexpected::Str(&name),
                &"a Python identifier",
            ))
        }
    }
}

impl Flake8ImportConventionsOptions {
    pub(crate) fn try_into_settings(
        self,
        preview: PreviewMode,
    ) -> anyhow::Result<flake8_import_conventions::settings::Settings> {
        let mut aliases: FxHashMap<String, String> = match self.aliases {
            Some(options_aliases) => options_aliases
                .into_iter()
                .map(|(module, alias)| (module.into_string(), alias.into_string()))
                .collect(),
            None => flake8_import_conventions::settings::default_aliases(preview),
        };
        if let Some(extend_aliases) = self.extend_aliases {
            aliases.extend(
                extend_aliases
                    .into_iter()
                    .map(|(module, alias)| (module.into_string(), alias.into_string())),
            );
        }

        let mut normalized_aliases: FxHashMap<String, String> = FxHashMap::default();
        #[expect(
            clippy::iter_over_hash_type,
            reason = "every invalid alias is rejected, regardless of which one is reported first"
        )]
        for (module, alias) in aliases {
            let normalized_alias = alias.nfkc().collect::<String>();
            if normalized_alias == "__debug__" {
                anyhow::bail!(
                    "Invalid alias for module '{module}': alias normalizes to '__debug__', \
                    which is not allowed."
                );
            }
            normalized_aliases.insert(module, normalized_alias);
        }

        let banned_aliases = self.banned_aliases.unwrap_or_else(|| {
            flake8_import_conventions::settings::default_banned_aliases(preview)
        });

        Ok(flake8_import_conventions::settings::Settings {
            aliases: normalized_aliases,
            banned_aliases,
            banned_from: self.banned_from.unwrap_or_default(),
        })
    }
}

/// Options for the `flake8-pytest-style` plugin
#[derive(
    Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize, OptionsMetadata, CombineOptions,
)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Flake8PytestStyleOptions {
    /// Boolean flag specifying whether `@pytest.fixture()` without parameters
    /// should have parentheses. If the option is set to `false` (the default),
    /// `@pytest.fixture` is valid and `@pytest.fixture()` is invalid. If set
    /// to `true`, `@pytest.fixture()` is valid and `@pytest.fixture` is
    /// invalid.
    #[option(
        default = "false",
        value_type = "bool",
        example = "fixture-parentheses = true"
    )]
    fixture_parentheses: Option<bool>,

    /// Expected type for multiple argument names in `@pytest.mark.parametrize`.
    /// The following values are supported:
    ///
    /// - `csv` — a comma-separated list, e.g.
    ///   `@pytest.mark.parametrize("name1,name2", ...)`
    /// - `tuple` (default) — e.g.
    ///   `@pytest.mark.parametrize(("name1", "name2"), ...)`
    /// - `list` — e.g. `@pytest.mark.parametrize(["name1", "name2"], ...)`
    #[option(
        default = "tuple",
        value_type = r#""csv" | "tuple" | "list""#,
        example = "parametrize-names-type = \"list\""
    )]
    parametrize_names_type: Option<types::ParametrizeNameType>,

    /// Expected type for the list of values rows in `@pytest.mark.parametrize`.
    /// The following values are supported:
    ///
    /// - `tuple` — e.g. `@pytest.mark.parametrize("name", (1, 2, 3))`
    /// - `list` (default) — e.g. `@pytest.mark.parametrize("name", [1, 2, 3])`
    #[option(
        default = "list",
        value_type = r#""tuple" | "list""#,
        example = "parametrize-values-type = \"tuple\""
    )]
    parametrize_values_type: Option<types::ParametrizeValuesType>,

    /// Expected type for each row of values in `@pytest.mark.parametrize` in
    /// case of multiple parameters. The following values are supported:
    ///
    /// - `tuple` (default) — e.g.
    ///   `@pytest.mark.parametrize(("name1", "name2"), [(1, 2), (3, 4)])`
    /// - `list` — e.g.
    ///   `@pytest.mark.parametrize(("name1", "name2"), [[1, 2], [3, 4]])`
    #[option(
        default = "tuple",
        value_type = r#""tuple" | "list""#,
        example = "parametrize-values-row-type = \"list\""
    )]
    parametrize_values_row_type: Option<types::ParametrizeValuesRowType>,

    /// List of exception names that require a match= parameter in a
    /// `pytest.raises()` call.
    ///
    /// Supports glob patterns. For more information on the glob syntax, refer
    /// to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).
    #[option(
        default = r#"["BaseException", "Exception", "ValueError", "OSError", "IOError", "EnvironmentError", "socket.error"]"#,
        value_type = "list[str]",
        example = "raises-require-match-for = [\"requests.RequestException\"]"
    )]
    raises_require_match_for: Option<Vec<String>>,

    /// List of additional exception names that require a match= parameter in a
    /// `pytest.raises()` call. This extends the default list of exceptions
    /// that require a match= parameter.
    /// This option is useful if you want to extend the default list of
    /// exceptions that require a match= parameter without having to specify
    /// the entire list.
    /// Note that this option does not remove any exceptions from the default
    /// list.
    ///
    /// Supports glob patterns. For more information on the glob syntax, refer
    /// to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).
    #[option(
        default = "[]",
        value_type = "list[str]",
        example = "raises-extend-require-match-for = [\"requests.RequestException\"]"
    )]
    raises_extend_require_match_for: Option<Vec<String>>,

    /// Boolean flag specifying whether `@pytest.mark.foo()` without parameters
    /// should have parentheses. If the option is set to `false` (the
    /// default), `@pytest.mark.foo` is valid and `@pytest.mark.foo()` is
    /// invalid. If set to `true`, `@pytest.mark.foo()` is valid and
    /// `@pytest.mark.foo` is invalid.
    #[option(
        default = "false",
        value_type = "bool",
        example = "mark-parentheses = true"
    )]
    mark_parentheses: Option<bool>,

    /// List of warning names that require a match= parameter in a
    /// `pytest.warns()` call.
    ///
    /// Supports glob patterns. For more information on the glob syntax, refer
    /// to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).
    #[option(
        default = r#"["Warning", "UserWarning", "DeprecationWarning"]"#,
        value_type = "list[str]",
        example = "warns-require-match-for = [\"requests.RequestsWarning\"]"
    )]
    warns_require_match_for: Option<Vec<String>>,

    /// List of additional warning names that require a match= parameter in a
    /// `pytest.warns()` call. This extends the default list of warnings that
    /// require a match= parameter.
    ///
    /// This option is useful if you want to extend the default list of warnings
    /// that require a match= parameter without having to specify the entire
    /// list.
    ///
    /// Note that this option does not remove any warnings from the default
    /// list.
    ///
    /// Supports glob patterns. For more information on the glob syntax, refer
    /// to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).
    #[option(
        default = "[]",
        value_type = "list[str]",
        example = "warns-extend-require-match-for = [\"requests.RequestsWarning\"]"
    )]
    warns_extend_require_match_for: Option<Vec<String>>,
}

impl Flake8PytestStyleOptions {
    pub(crate) fn try_into_settings(
        self,
    ) -> anyhow::Result<flake8_pytest_style::settings::Settings> {
        Ok(flake8_pytest_style::settings::Settings {
            fixture_parentheses: self.fixture_parentheses.unwrap_or_default(),
            parametrize_names_type: self.parametrize_names_type.unwrap_or_default(),
            parametrize_values_type: self.parametrize_values_type.unwrap_or_default(),
            parametrize_values_row_type: self.parametrize_values_row_type.unwrap_or_default(),
            raises_require_match_for: self
                .raises_require_match_for
                .map(|patterns| {
                    patterns
                        .into_iter()
                        .map(|pattern| IdentifierPattern::new(&pattern))
                        .collect()
                })
                .transpose()
                .map_err(SettingsError::InvalidRaisesRequireMatchFor)?
                .unwrap_or_else(flake8_pytest_style::settings::default_broad_exceptions),
            raises_extend_require_match_for: self
                .raises_extend_require_match_for
                .map(|patterns| {
                    patterns
                        .into_iter()
                        .map(|pattern| IdentifierPattern::new(&pattern))
                        .collect()
                })
                .transpose()
                .map_err(SettingsError::InvalidRaisesExtendRequireMatchFor)?
                .unwrap_or_default(),
            mark_parentheses: self.mark_parentheses.unwrap_or_default(),
            warns_require_match_for: self
                .warns_require_match_for
                .map(|patterns| {
                    patterns
                        .into_iter()
                        .map(|pattern| IdentifierPattern::new(&pattern))
                        .collect()
                })
                .transpose()
                .map_err(SettingsError::InvalidWarnsRequireMatchFor)?
                .unwrap_or_else(flake8_pytest_style::settings::default_broad_warnings),
            warns_extend_require_match_for: self
                .warns_extend_require_match_for
                .map(|patterns| {
                    patterns
                        .into_iter()
                        .map(|pattern| IdentifierPattern::new(&pattern))
                        .collect()
                })
                .transpose()
                .map_err(SettingsError::InvalidWarnsExtendRequireMatchFor)?
                .unwrap_or_default(),
        })
    }
}

/// Options for the `flake8-quotes` plugin.
#[derive(
    Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize, OptionsMetadata, CombineOptions,
)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Flake8QuotesOptions {
    /// Quote style to prefer for inline strings (either "single" or
    /// "double").
    ///
    /// When using the formatter, ensure that [`format.quote-style`](#format_quote-style) is set to
    /// the same preferred quote style.
    #[option(
        default = r#""double""#,
        value_type = r#""single" | "double""#,
        example = r#"
            inline-quotes = "single"
        "#
    )]
    inline_quotes: Option<Quote>,

    /// Quote style to prefer for multiline strings (either "single" or
    /// "double").
    ///
    /// When using the formatter, only "double" is compatible, as the formatter
    /// enforces double quotes for multiline strings.
    #[option(
        default = r#""double""#,
        value_type = r#""single" | "double""#,
        example = r#"
            multiline-quotes = "single"
        "#
    )]
    multiline_quotes: Option<Quote>,

    /// Quote style to prefer for docstrings (either "single" or "double").
    ///
    /// When using the formatter, only "double" is compatible, as the formatter
    /// enforces double quotes for docstrings strings.
    #[option(
        default = r#""double""#,
        value_type = r#""single" | "double""#,
        example = r#"
            docstring-quotes = "single"
        "#
    )]
    docstring_quotes: Option<Quote>,

    /// Whether to avoid using single quotes if a string contains single quotes,
    /// or vice-versa with double quotes, as per [PEP 8](https://peps.python.org/pep-0008/#string-quotes).
    /// This minimizes the need to escape quotation marks within strings.
    #[option(
        default = r#"true"#,
        value_type = "bool",
        example = r#"
            # Don't bother trying to avoid escapes.
            avoid-escape = false
        "#
    )]
    avoid_escape: Option<bool>,
}

impl Flake8QuotesOptions {
    pub(crate) fn into_settings(self) -> flake8_quotes::settings::Settings {
        flake8_quotes::settings::Settings {
            inline_quotes: self.inline_quotes.unwrap_or_default(),
            multiline_quotes: self.multiline_quotes.unwrap_or_default(),
            docstring_quotes: self.docstring_quotes.unwrap_or_default(),
            avoid_escape: self.avoid_escape.unwrap_or(true),
        }
    }
}

/// Options for the `flake8_self` plugin.
#[derive(
    Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize, OptionsMetadata, CombineOptions,
)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Flake8SelfOptions {
    /// A list of names to ignore when considering `flake8-self` violations.
    #[option(
        default = r#"["_make", "_asdict", "_replace", "_fields", "_field_defaults", "_name_", "_value_"]"#,
        value_type = "list[str]",
        example = r#"
            ignore-names = ["_new"]
        "#
    )]
    ignore_names: Option<Vec<Name>>,

    /// Additional names to ignore when considering `flake8-self` violations,
    /// in addition to those included in [`ignore-names`](#lint_flake8-self_ignore-names).
    #[option(
        default = r#"[]"#,
        value_type = "list[str]",
        example = r#"extend-ignore-names = ["_base_manager", "_default_manager",  "_meta"]"#
    )]
    extend_ignore_names: Option<Vec<Name>>,
}

impl Flake8SelfOptions {
    pub(crate) fn into_settings(self) -> flake8_self::settings::Settings {
        let defaults = flake8_self::settings::Settings::default();
        flake8_self::settings::Settings {
            ignore_names: self
                .ignore_names
                .unwrap_or(defaults.ignore_names)
                .into_iter()
                .chain(self.extend_ignore_names.unwrap_or_default())
                .collect(),
        }
    }
}

/// Options for the `flake8-tidy-imports` plugin
#[derive(
    Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize, OptionsMetadata, CombineOptions,
)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Flake8TidyImportsOptions {
    /// Whether to ban all relative imports (`"all"`), or only those imports
    /// that extend into the parent module or beyond (`"parents"`).
    #[option(
        default = r#""parents""#,
        value_type = r#""parents" | "all""#,
        example = r#"
            # Disallow all relative imports.
            ban-relative-imports = "all"
        "#
    )]
    ban_relative_imports: Option<Strictness>,

    /// Specific modules or module members that may not be imported or accessed.
    /// Note that this rule is only meant to flag accidental uses,
    /// and can be circumvented via `eval` or `importlib`.
    #[option(
        default = r#"{}"#,
        value_type = r#"dict[str, { "msg": str }]"#,
        scope = "banned-api",
        example = r#"
            "cgi".msg = "The cgi module is deprecated, see https://peps.python.org/pep-0594/#cgi."
            "typing.TypedDict".msg = "Use typing_extensions.TypedDict instead."
        "#
    )]
    banned_api: Option<FxHashMap<String, ApiBan>>,

    /// List of specific modules that may not be imported at module level, and should instead be
    /// imported lazily (e.g., within a function definition, or an `if TYPE_CHECKING:`
    /// block, or some other nested context). This also affects the rule `import-outside-top-level`
    /// if `banned-module-level-imports` is enabled.
    #[option(
        default = r#"[]"#,
        value_type = r#"list[str]"#,
        example = r#"
            # Ban certain modules from being imported at module level, instead requiring
            # that they're imported lazily (e.g., within a function definition).
            banned-module-level-imports = ["torch", "tensorflow"]
        "#
    )]
    banned_module_level_imports: Option<Vec<String>>,

    /// Specific modules that must be imported lazily in contexts where `lazy import` is legal, or
    /// `"all"` to require every lazily-convertible import to use the `lazy` keyword. Ruff ignores
    /// contexts where `lazy import` is invalid, such as functions, classes, `try`/`except`
    /// blocks, `__future__` imports, and `from ... import *` statements. This rule is only
    /// enforced when targeting Python 3.15 or newer.
    #[option(
        default = r#"[]"#,
        value_type = r#""all" | list[str] | { include = "all" | list[str], exclude = list[str] }"#,
        example = r#"
            # Require lazy imports for specific modules.
            require-lazy = ["typing", "foo"]

            # Require lazy imports by default, except for modules with import-time side effects.
            require-lazy = { include = "all", exclude = ["sitecustomize"] }
        "#
    )]
    require_lazy: Option<ImportSelector>,

    /// Specific modules that may not be imported lazily, or `"all"` to forbid lazy imports except
    /// for any modules excluded from the selector. This rule is only enforced when targeting
    /// Python 3.15 or newer.
    #[option(
        default = r#"[]"#,
        value_type = r#""all" | list[str] | { include = "all" | list[str], exclude = list[str] }"#,
        example = r#"
            # Forbid lazy imports for specific modules.
            ban-lazy = ["sitecustomize"]

            # Forbid lazy imports by default, while allowing specific exceptions.
            ban-lazy = { include = "all", exclude = ["typing"] }
        "#
    )]
    ban_lazy: Option<ImportSelector>,
}

impl Flake8TidyImportsOptions {
    pub(crate) fn try_into_settings(self) -> Result<flake8_tidy_imports::settings::Settings> {
        let require_lazy = self.require_lazy.unwrap_or_default();
        let ban_lazy = self.ban_lazy.unwrap_or_default();

        if conflicting_lazy_import_settings(&require_lazy, &ban_lazy) {
            return Err(anyhow!(
                "`require-lazy` and `ban-lazy` must not overlap after applying exclusions"
            ));
        }

        Ok(flake8_tidy_imports::settings::Settings {
            ban_relative_imports: self.ban_relative_imports.unwrap_or(Strictness::Parents),
            banned_api: self.banned_api.unwrap_or_default(),
            banned_module_level_imports: self.banned_module_level_imports.unwrap_or_default(),
            require_lazy,
            ban_lazy,
        })
    }
}

fn conflicting_lazy_import_settings(
    require_lazy: &ImportSelector,
    ban_lazy: &ImportSelector,
) -> bool {
    overlapping_import_selectors(require_lazy, ban_lazy)
}

fn overlapping_import_selectors(left: &ImportSelector, right: &ImportSelector) -> bool {
    match (left.include(), right.include()) {
        (ImportSelection::All(AllImports::All), ImportSelection::All(AllImports::All)) => true,
        (ImportSelection::All(AllImports::All), ImportSelection::Imports(imports))
        | (ImportSelection::Imports(imports), ImportSelection::All(AllImports::All)) => imports
            .iter()
            .any(|candidate| candidate_has_overlap(candidate, left.exclude(), right.exclude())),
        (ImportSelection::Imports(left_imports), ImportSelection::Imports(right_imports)) => {
            left_imports.iter().any(|left_import| {
                right_imports.iter().any(|right_import| {
                    overlapping_root(left_import, right_import).is_some_and(|candidate| {
                        candidate_has_overlap(candidate, left.exclude(), right.exclude())
                    })
                })
            })
        }
    }
}

fn candidate_has_overlap(
    candidate: &str,
    left_excludes: &[String],
    right_excludes: &[String],
) -> bool {
    !is_fully_excluded(candidate, left_excludes) && !is_fully_excluded(candidate, right_excludes)
}

fn overlapping_root<'a>(left: &'a str, right: &'a str) -> Option<&'a str> {
    if matches_module_prefix(left, right) {
        Some(right)
    } else if matches_module_prefix(right, left) {
        Some(left)
    } else {
        None
    }
}

fn is_fully_excluded(candidate: &str, excludes: &[String]) -> bool {
    excludes
        .iter()
        .any(|exclude| matches_module_prefix(candidate, exclude))
}

fn matches_module_prefix(module: &str, prefix: &str) -> bool {
    module == prefix
        || module
            .strip_prefix(prefix)
            .is_some_and(|suffix| suffix.starts_with('.'))
}

/// Options for the `flake8-type-checking` plugin
#[derive(
    Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize, OptionsMetadata, CombineOptions,
)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Flake8TypeCheckingOptions {
    /// Enforce `TC001`, `TC002`, and `TC003` rules even when valid runtime imports
    /// are present for the same module.
    ///
    /// See flake8-type-checking's [strict](https://github.com/snok/flake8-type-checking#strict) option.
    #[option(
        default = "false",
        value_type = "bool",
        example = r#"
            strict = true
        "#
    )]
    strict: Option<bool>,

    /// Exempt certain modules from needing to be moved into type-checking
    /// blocks.
    #[option(
        default = "[\"typing\"]",
        value_type = "list[str]",
        example = r#"
            exempt-modules = ["typing", "typing_extensions"]
        "#
    )]
    exempt_modules: Option<Vec<String>>,

    /// Exempt classes that list any of the enumerated classes as a base class
    /// from needing to be moved into type-checking blocks.
    ///
    /// Common examples include Pydantic's `pydantic.BaseModel` and SQLAlchemy's
    /// `sqlalchemy.orm.DeclarativeBase`, but can also support user-defined
    /// classes that inherit from those base classes. For example, if you define
    /// a common `DeclarativeBase` subclass that's used throughout your project
    /// (e.g., `class Base(DeclarativeBase) ...` in `base.py`), you can add it to
    /// this list (`runtime-evaluated-base-classes = ["base.Base"]`) to exempt
    /// models from being moved into type-checking blocks.
    #[option(
        default = "[]",
        value_type = "list[str]",
        example = r#"
            runtime-evaluated-base-classes = ["pydantic.BaseModel", "sqlalchemy.orm.DeclarativeBase"]
        "#
    )]
    runtime_evaluated_base_classes: Option<Vec<String>>,

    /// Exempt classes and functions decorated with any of the enumerated
    /// decorators from being moved into type-checking blocks.
    ///
    /// Common examples include Pydantic's `@pydantic.validate_call` decorator
    /// (for functions) and attrs' `@attrs.define` decorator (for classes).
    ///
    /// This also supports framework decorators like FastAPI's `fastapi.FastAPI.get`
    /// which will work across assignments in the same module.
    ///
    /// For example:
    /// ```python
    /// from fastapi import FastAPI
    ///
    /// app = FastAPI("app")
    ///
    /// @app.get("/home")
    /// def home() -> str: ...
    /// ```
    ///
    /// Here `app.get` will correctly be identified as `fastapi.FastAPI.get`.
    #[option(
        default = "[]",
        value_type = "list[str]",
        example = r#"
            runtime-evaluated-decorators = ["pydantic.validate_call", "attrs.define"]
        "#
    )]
    runtime_evaluated_decorators: Option<Vec<String>>,

    /// Whether to add quotes around type annotations, if doing so would allow
    /// the corresponding import to be moved into a type-checking block.
    ///
    /// For example, in the following, Python requires that `Sequence` be
    /// available at runtime, despite the fact that it's only used in a type
    /// annotation:
    ///
    /// ```python
    /// from collections.abc import Sequence
    ///
    ///
    /// def func(value: Sequence[int]) -> None:
    ///     ...
    /// ```
    ///
    /// In other words, moving `from collections.abc import Sequence` into an
    /// `if TYPE_CHECKING:` block above would cause a runtime error, as the
    /// type would no longer be available at runtime.
    ///
    /// By default, Ruff will respect such runtime semantics and avoid moving
    /// the import to prevent such runtime errors.
    ///
    /// Setting `quote-annotations` to `true` will instruct Ruff to add quotes
    /// around the annotation (e.g., `"Sequence[int]"`), which in turn enables
    /// Ruff to move the import into an `if TYPE_CHECKING:` block, like so:
    ///
    /// ```python
    /// from typing import TYPE_CHECKING
    ///
    /// if TYPE_CHECKING:
    ///     from collections.abc import Sequence
    ///
    ///
    /// def func(value: "Sequence[int]") -> None:
    ///     ...
    /// ```
    ///
    /// Note that this setting has no effect when `from __future__ import annotations`
    /// is present, as `__future__` annotations are always treated equivalently
    /// to quoted annotations. Similarly, this setting has no effect on Python
    /// versions after 3.14 because these annotations are also deferred.
    #[option(
        default = "false",
        value_type = "bool",
        example = r#"
            # Add quotes around type annotations, if doing so would allow
            # an import to be moved into a type-checking block.
            quote-annotations = true
        "#
    )]
    quote_annotations: Option<bool>,
}

impl Flake8TypeCheckingOptions {
    pub(crate) fn into_settings(self) -> flake8_type_checking::settings::Settings {
        flake8_type_checking::settings::Settings {
            strict: self.strict.unwrap_or(false),
            exempt_modules: self
                .exempt_modules
                .unwrap_or_else(|| vec!["typing".to_string()]),
            runtime_required_base_classes: self.runtime_evaluated_base_classes.unwrap_or_default(),
            runtime_required_decorators: self.runtime_evaluated_decorators.unwrap_or_default(),
            quote_annotations: self.quote_annotations.unwrap_or_default(),
        }
    }
}

/// Options for the `flake8-unused-arguments` plugin
#[derive(
    Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize, OptionsMetadata, CombineOptions,
)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Flake8UnusedArgumentsOptions {
    /// Whether to allow unused variadic arguments, like `*args` and `**kwargs`.
    #[option(
        default = "false",
        value_type = "bool",
        example = "ignore-variadic-names = true"
    )]
    ignore_variadic_names: Option<bool>,
}

impl Flake8UnusedArgumentsOptions {
    pub(crate) fn into_settings(self) -> flake8_unused_arguments::settings::Settings {
        flake8_unused_arguments::settings::Settings {
            ignore_variadic_names: self.ignore_variadic_names.unwrap_or_default(),
        }
    }
}

/// Options for the `isort` plugin.
#[derive(
    Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize, OptionsMetadata, CombineOptions,
)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct IsortOptions {
    /// Force `import from` statements with multiple members and at least one
    /// alias (e.g., `import A as B`) to wrap such that every line contains
    /// exactly one member. For example, this formatting would be retained,
    /// rather than condensing to a single line:
    ///
    /// ```python
    /// from .utils import (
    ///     test_directory as test_directory,
    ///     test_id as test_id
    /// )
    /// ```
    ///
    /// Note that this setting is only effective when combined with
    /// `combine-as-imports = true`. When [`combine-as-imports`](#lint_isort_combine-as-imports) isn't
    /// enabled, every aliased `import from` will be given its own line, in
    /// which case, wrapping is not necessary.
    ///
    /// When using the formatter, ensure that [`format.skip-magic-trailing-comma`](#format_skip-magic-trailing-comma) is set to `false` (default)
    /// when enabling `force-wrap-aliases` to avoid that the formatter collapses members if they all fit on a single line.
    #[option(
        default = r#"false"#,
        value_type = "bool",
        example = r#"
            force-wrap-aliases = true
            combine-as-imports = true
        "#
    )]
    force_wrap_aliases: Option<bool>,

    /// Forces all from imports to appear on their own line.
    #[option(
        default = r#"false"#,
        value_type = "bool",
        example = r#"force-single-line = true"#
    )]
    force_single_line: Option<bool>,

    /// One or more modules to exclude from the single line rule.
    #[option(
        default = r#"[]"#,
        value_type = "list[str]",
        example = r#"
            single-line-exclusions = ["os", "json"]
        "#
    )]
    single_line_exclusions: Option<Vec<String>>,

    /// Combines as imports on the same line. See isort's [`combine-as-imports`](https://pycqa.github.io/isort/docs/configuration/options.html#combine-as-imports)
    /// option.
    #[option(
        default = r#"false"#,
        value_type = "bool",
        example = r#"
            combine-as-imports = true
        "#
    )]
    combine_as_imports: Option<bool>,

    /// If a comma is placed after the last member in a multi-line import, then
    /// the imports will never be folded into one line.
    ///
    /// See isort's [`split-on-trailing-comma`](https://pycqa.github.io/isort/docs/configuration/options.html#split-on-trailing-comma) option.
    ///
    /// When using the formatter, ensure that [`format.skip-magic-trailing-comma`](#format_skip-magic-trailing-comma) is set to `false` (default) when enabling `split-on-trailing-comma`
    /// to avoid that the formatter removes the trailing commas.
    #[option(
        default = r#"true"#,
        value_type = "bool",
        example = r#"
            split-on-trailing-comma = false
        "#
    )]
    split_on_trailing_comma: Option<bool>,

    /// Order imports by type, which is determined by case, in addition to
    /// alphabetically.
    ///
    /// Note that this option takes precedence over the
    /// [`case-sensitive`](#lint_isort_case-sensitive) setting when enabled.
    #[option(
        default = r#"true"#,
        value_type = "bool",
        example = r#"
            order-by-type = true
        "#
    )]
    order_by_type: Option<bool>,

    /// Don't sort straight-style imports (like `import sys`) before from-style
    /// imports (like `from itertools import groupby`). Instead, sort the
    /// imports by module, independent of import style.
    #[option(
        default = r#"false"#,
        value_type = "bool",
        example = r#"
            force-sort-within-sections = true
        "#
    )]
    force_sort_within_sections: Option<bool>,

    /// Sort imports taking into account case sensitivity.
    ///
    /// Note that the [`order-by-type`](#lint_isort_order-by-type) setting will
    /// take precedence over this one when enabled.
    #[option(
        default = r#"false"#,
        value_type = "bool",
        example = r#"
            case-sensitive = true
        "#
    )]
    case_sensitive: Option<bool>,

    /// Force specific imports to the top of their appropriate section.
    #[option(
        default = r#"[]"#,
        value_type = "list[str]",
        example = r#"
            force-to-top = ["src"]
        "#
    )]
    force_to_top: Option<Vec<String>>,

    /// A list of modules to consider first-party, regardless of whether they
    /// can be identified as such via introspection of the local filesystem.
    ///
    /// Supports glob patterns. For more information on the glob syntax, refer
    /// to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).
    #[option(
        default = r#"[]"#,
        value_type = "list[str]",
        example = r#"
            known-first-party = ["src"]
        "#
    )]
    known_first_party: Option<Vec<String>>,

    /// A list of modules to consider third-party, regardless of whether they
    /// can be identified as such via introspection of the local filesystem.
    ///
    /// Supports glob patterns. For more information on the glob syntax, refer
    /// to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).
    #[option(
        default = r#"[]"#,
        value_type = "list[str]",
        example = r#"
            known-third-party = ["src"]
        "#
    )]
    known_third_party: Option<Vec<String>>,

    /// A list of modules to consider being a local folder.
    /// Generally, this is reserved for relative imports (`from . import module`).
    ///
    /// Supports glob patterns. For more information on the glob syntax, refer
    /// to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).
    #[option(
        default = r#"[]"#,
        value_type = "list[str]",
        example = r#"
            known-local-folder = ["src"]
        "#
    )]
    known_local_folder: Option<Vec<String>>,

    /// A list of modules to consider standard-library, in addition to those
    /// known to Ruff in advance.
    ///
    /// Supports glob patterns. For more information on the glob syntax, refer
    /// to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).
    #[option(
        default = r#"[]"#,
        value_type = "list[str]",
        example = r#"
            extra-standard-library = ["path"]
        "#
    )]
    extra_standard_library: Option<Vec<String>>,

    /// Whether to place "closer" imports (fewer `.` characters, most local)
    /// before "further" imports (more `.` characters, least local), or vice
    /// versa.
    ///
    /// The default ("furthest-to-closest") is equivalent to isort's
    /// [`reverse-relative`](https://pycqa.github.io/isort/docs/configuration/options.html#reverse-relative) default (`reverse-relative = false`); setting
    /// this to "closest-to-furthest" is equivalent to isort's
    /// `reverse-relative = true`.
    #[option(
        default = r#""furthest-to-closest""#,
        value_type = r#""furthest-to-closest" | "closest-to-furthest""#,
        example = r#"
            relative-imports-order = "closest-to-furthest"
        "#
    )]
    relative_imports_order: Option<RelativeImportsOrder>,

    /// Add the specified import line to all files.
    #[option(
        default = r#"[]"#,
        value_type = "list[str]",
        example = r#"
            required-imports = ["from __future__ import annotations"]
        "#
    )]
    required_imports: Option<Vec<NameImports>>,

    /// An override list of tokens to always recognize as a Class for
    /// [`order-by-type`](#lint_isort_order-by-type) regardless of casing.
    #[option(
        default = r#"[]"#,
        value_type = "list[str]",
        example = r#"
            classes = ["SVC"]
        "#
    )]
    classes: Option<Vec<String>>,

    /// An override list of tokens to always recognize as a CONSTANT
    /// for [`order-by-type`](#lint_isort_order-by-type) regardless of casing.
    #[option(
        default = r#"[]"#,
        value_type = "list[str]",
        example = r#"
            constants = ["constant"]
        "#
    )]
    constants: Option<Vec<String>>,

    /// An override list of tokens to always recognize as a var
    /// for [`order-by-type`](#lint_isort_order-by-type) regardless of casing.
    #[option(
        default = r#"[]"#,
        value_type = "list[str]",
        example = r#"
            variables = ["VAR"]
        "#
    )]
    variables: Option<Vec<String>>,

    /// A list of sections that should _not_ be delineated from the previous
    /// section via empty lines.
    #[option(
        default = r#"[]"#,
        value_type = r#"list["future" | "standard-library" | "third-party" | "first-party" | "local-folder" | str]"#,
        example = r#"
            no-lines-before = ["future", "standard-library"]
        "#
    )]
    no_lines_before: Option<Vec<ImportSection>>,

    /// A mapping from import section names to their heading comments.
    ///
    /// When set, a comment with the specified text will be added above imports
    /// in the corresponding section. If a heading comment already exists, it
    /// will be replaced.
    ///
    /// Compatible with isort's `import_heading_{section_name}` settings.
    #[option(
        default = r#"{}"#,
        value_type = r#"dict["future" | "standard-library" | "third-party" | "first-party" | "local-folder" | str, str]"#,
        scope = "import-heading",
        example = r#"
            future = "Future imports"
            standard-library = "Standard library imports"
            third-party = "Third party imports"
            first-party = "First party imports"
            local-folder = "Local folder imports"
        "#
    )]
    import_heading: Option<FxHashMap<ImportSection, String>>,

    /// The number of blank lines to place after imports.
    /// Use `-1` for automatic determination.
    ///
    /// Ruff uses at most one blank line after imports in typing stub files (files with `.pyi` extension) in accordance to
    /// the typing style recommendations ([source](https://typing.python.org/en/latest/guides/writing_stubs.html#blank-lines)).
    ///
    /// When using the formatter, only the values `-1`, `1`, and `2` are compatible because
    /// it enforces at least one empty and at most two empty lines after imports.
    #[option(
        default = r#"-1"#,
        value_type = "int",
        example = r#"
            # Use a single line after each import block.
            lines-after-imports = 1
        "#
    )]
    lines_after_imports: Option<isize>,

    /// The number of lines to place between "direct" and `import from` imports.
    ///
    /// When using the formatter, only the values `0` and `1` are compatible because
    /// it preserves up to one empty line after imports in nested blocks.
    #[option(
        default = r#"0"#,
        value_type = "int",
        example = r#"
            # Use a single line between direct and from import.
            lines-between-types = 1
        "#
    )]
    lines_between_types: Option<usize>,

    /// A list of modules to separate into auxiliary block(s) of imports,
    /// in the order specified.
    #[option(
        default = r#"[]"#,
        value_type = "list[str]",
        example = r#"
            forced-separate = ["tests"]
        "#
    )]
    forced_separate: Option<Vec<String>>,

    /// Override in which order the sections should be output. Can be used to move custom sections.
    #[option(
        default = r#"["future", "standard-library", "third-party", "first-party", "local-folder"]"#,
        value_type = r#"list["future" | "standard-library" | "third-party" | "first-party" | "local-folder" | str]"#,
        example = r#"
            section-order = ["future", "standard-library", "first-party", "local-folder", "third-party"]
        "#
    )]
    section_order: Option<Vec<ImportSection>>,

    /// Define a default section for any imports that don't fit into the specified [`section-order`](#lint_isort_section-order).
    #[option(
        default = r#""third-party""#,
        value_type = "str",
        example = r#"
            default-section = "first-party"
        "#
    )]
    default_section: Option<ImportSection>,

    /// Put all imports into the same section bucket.
    ///
    /// For example, rather than separating standard library and third-party imports, as in:
    /// ```python
    /// import os
    /// import sys
    ///
    /// import numpy
    /// import pandas
    /// ```
    ///
    /// Setting `no-sections = true` will instead group all imports into a single section:
    /// ```python
    /// import numpy
    /// import os
    /// import pandas
    /// import sys
    /// ```
    #[option(
        default = r#"false"#,
        value_type = "bool",
        example = r#"
            no-sections = true
        "#
    )]
    no_sections: Option<bool>,

    /// Whether to automatically mark imports from within the same package as first-party.
    /// For example, when `detect-same-package = true`, then when analyzing files within the
    /// `foo` package, any imports from within the `foo` package will be considered first-party.
    ///
    /// This heuristic is often unnecessary when `src` is configured to detect all first-party
    /// sources; however, if `src` is _not_ configured, this heuristic can be useful to detect
    /// first-party imports from _within_ (but not _across_) first-party packages.
    #[option(
        default = r#"true"#,
        value_type = "bool",
        example = r#"
            detect-same-package = false
        "#
    )]
    detect_same_package: Option<bool>,

    /// Whether to place `import from` imports before straight imports when sorting.
    ///
    /// For example, by default, imports will be sorted such that straight imports appear
    /// before `import from` imports, as in:
    /// ```python
    /// import os
    /// import sys
    /// from typing import List
    /// ```
    ///
    /// Setting `from-first = true` will instead sort such that `import from` imports appear
    /// before straight imports, as in:
    /// ```python
    /// from typing import List
    /// import os
    /// import sys
    /// ```
    #[option(
        default = r#"false"#,
        value_type = "bool",
        example = r#"
            from-first = true
        "#
    )]
    from_first: Option<bool>,

    /// Sort imports by their string length, such that shorter imports appear
    /// before longer imports. For example, by default, imports will be sorted
    /// alphabetically, as in:
    /// ```python
    /// import collections
    /// import os
    /// ```
    ///
    /// Setting `length-sort = true` will instead sort such that shorter imports
    /// appear before longer imports, as in:
    /// ```python
    /// import os
    /// import collections
    /// ```
    #[option(
        default = r#"false"#,
        value_type = "bool",
        example = r#"
            length-sort = true
        "#
    )]
    length_sort: Option<bool>,

    /// Sort straight imports by their string length. Similar to [`length-sort`](#lint_isort_length-sort),
    /// but applies only to straight imports and doesn't affect `from` imports.
    #[option(
        default = r#"false"#,
        value_type = "bool",
        example = r#"
            length-sort-straight = true
        "#
    )]
    length_sort_straight: Option<bool>,

    // Tables are required to go last.
    /// A list of mappings from section names to modules.
    ///
    /// By default, imports are categorized according to their type (e.g., `future`, `third-party`,
    /// and so on). This setting allows you to group modules into custom sections, to augment or
    /// override the built-in sections.
    ///
    /// For example, to group all testing utilities, you could create a `testing` section:
    /// ```toml
    /// testing = ["pytest", "hypothesis"]
    /// ```
    ///
    /// The values in the list are treated as glob patterns. For example, to match all packages in
    /// the LangChain ecosystem (`langchain-core`, `langchain-openai`, etc.):
    /// ```toml
    /// langchain = ["langchain-*"]
    /// ```
    ///
    /// Custom sections should typically be inserted into the [`section-order`](#lint_isort_section-order) list to ensure that
    /// they're displayed as a standalone group and in the intended order, as in:
    /// ```toml
    /// section-order = [
    ///   "future",
    ///   "standard-library",
    ///   "third-party",
    ///   "first-party",
    ///   "local-folder",
    ///   "testing"
    /// ]
    /// ```
    ///
    /// If a custom section is omitted from [`section-order`](#lint_isort_section-order), imports in that section will be
    /// assigned to the [`default-section`](#lint_isort_default-section) (which defaults to `third-party`).
    #[option(
        default = "{}",
        value_type = "dict[str, list[str]]",
        scope = "sections",
        example = r#"
            # Group all Django imports into a separate section.
            "django" = ["django"]
        "#
    )]
    sections: Option<FxHashMap<ImportSection, Vec<String>>>,
}

impl IsortOptions {
    pub(crate) fn try_into_settings(
        self,
    ) -> Result<isort::settings::Settings, isort::settings::SettingsError> {
        // Verify that if `no_sections` is set, then `section_order` is empty.
        let no_sections = self.no_sections.unwrap_or_default();
        if no_sections && self.section_order.is_some() {
            warn_user_once!("`section-order` is ignored when `no-sections` is set to `true`");
        }
        if no_sections && self.default_section.is_some() {
            warn_user_once!("`default-section` is ignored when `no-sections` is set to `true`");
        }
        if no_sections && self.sections.is_some() {
            warn_user_once!("`sections` is ignored when `no-sections` is set to `true`");
        }

        // Verify that if `force_sort_within_sections` is `True`, then `lines_between_types` is set to `0`.
        let force_sort_within_sections = self.force_sort_within_sections.unwrap_or_default();
        let lines_between_types = self.lines_between_types.unwrap_or_default();
        if force_sort_within_sections && lines_between_types != 0 {
            warn_user_once!(
                "`lines-between-types` is ignored when `force-sort-within-sections` \
                is set to `true`"
            );
        }

        // Extract any configuration options that deal with user-defined sections.
        let mut section_order: Vec<_> = self
            .section_order
            .unwrap_or_else(|| ImportType::iter().map(ImportSection::Known).collect());
        let default_section = self
            .default_section
            .unwrap_or(ImportSection::Known(ImportType::ThirdParty));

        let known_first_party = self
            .known_first_party
            .map(|names| {
                names
                    .into_iter()
                    .map(|name| IdentifierPattern::new(&name))
                    .collect()
            })
            .transpose()
            .map_err(isort::settings::SettingsError::InvalidKnownFirstParty)?
            .unwrap_or_default();
        let known_third_party = self
            .known_third_party
            .map(|names| {
                names
                    .into_iter()
                    .map(|name| IdentifierPattern::new(&name))
                    .collect()
            })
            .transpose()
            .map_err(isort::settings::SettingsError::InvalidKnownThirdParty)?
            .unwrap_or_default();
        let known_local_folder = self
            .known_local_folder
            .map(|names| {
                names
                    .into_iter()
                    .map(|name| IdentifierPattern::new(&name))
                    .collect()
            })
            .transpose()
            .map_err(isort::settings::SettingsError::InvalidKnownLocalFolder)?
            .unwrap_or_default();
        let extra_standard_library = self
            .extra_standard_library
            .map(|names| {
                names
                    .into_iter()
                    .map(|name| IdentifierPattern::new(&name))
                    .collect()
            })
            .transpose()
            .map_err(isort::settings::SettingsError::InvalidExtraStandardLibrary)?
            .unwrap_or_default();
        let no_lines_before = self.no_lines_before.unwrap_or_default();
        let from_first = self.from_first.unwrap_or_default();
        let sections = self.sections.unwrap_or_default();

        // Verify that `sections` doesn't contain any built-in sections.
        let sections: FxHashMap<String, Vec<IdentifierPattern>> = sections
            .into_iter()
            .filter_map(|(section, modules)| match section {
                ImportSection::Known(section) => {
                    warn_user_once!("`sections` contains built-in section: `{:?}`", section);
                    None
                }
                ImportSection::UserDefined(section) => Some((section, modules)),
            })
            .map(|(section, modules)| {
                let modules = modules
                    .into_iter()
                    .map(|module| {
                        IdentifierPattern::new(&module)
                            .map_err(isort::settings::SettingsError::InvalidUserDefinedSection)
                    })
                    .collect::<Result<Vec<_>, isort::settings::SettingsError>>()?;
                Ok((section, modules))
            })
            .collect::<Result<_, _>>()?;

        // Verify that `section_order` doesn't contain any duplicates.
        let mut seen = FxHashSet::with_capacity_and_hasher(section_order.len(), FxBuildHasher);
        for section in &section_order {
            if !seen.insert(section) {
                warn_user_once!(
                    "`section-order` contains duplicate section: `{:?}`",
                    section
                );
            }
        }

        // Verify that all sections listed in `section_order` are defined in `sections`.
        for section in &section_order {
            if let ImportSection::UserDefined(section_name) = section {
                if !sections.contains_key(section_name) {
                    warn_user_once!("`section-order` contains unknown section: `{:?}`", section,);
                }
            }
        }

        // Verify that all sections listed in `no_lines_before` are defined in `sections`.
        for section in &no_lines_before {
            if let ImportSection::UserDefined(section_name) = section {
                if !sections.contains_key(section_name) {
                    warn_user_once!(
                        "`no-lines-before` contains unknown section: `{:?}`",
                        section,
                    );
                }
            }
        }

        let import_heading = self.import_heading.unwrap_or_default();

        // Verify that all sections listed in `import_heading` are defined in `sections`.
        let mut import_heading_sections = import_heading.keys().collect::<Vec<_>>();
        import_heading_sections.sort_unstable();
        for section in import_heading_sections {
            if let ImportSection::UserDefined(section_name) = section {
                if !sections.contains_key(section_name) {
                    warn_user_once!("`import-heading` contains unknown section: `{:?}`", section,);
                }
            }
        }

        // Verify that `default_section` is in `section_order`.
        if !section_order.contains(&default_section) {
            warn_user_once!(
                "`section-order` must contain `default-section`: {:?}",
                default_section,
            );
            section_order.push(default_section.clone());
        }

        Ok(isort::settings::Settings {
            required_imports: self
                .required_imports
                .unwrap_or_default()
                .into_iter()
                .flat_map(NameImports::into_imports)
                .collect(),
            combine_as_imports: self.combine_as_imports.unwrap_or(false),
            force_single_line: self.force_single_line.unwrap_or(false),
            force_sort_within_sections,
            case_sensitive: self.case_sensitive.unwrap_or(false),
            force_wrap_aliases: self.force_wrap_aliases.unwrap_or(false),
            detect_same_package: self.detect_same_package.unwrap_or(true),
            force_to_top: FxHashSet::from_iter(self.force_to_top.unwrap_or_default()),
            known_modules: isort::categorize::KnownModules::new(
                known_first_party,
                known_third_party,
                known_local_folder,
                extra_standard_library,
                sections,
            ),
            order_by_type: self.order_by_type.unwrap_or(true),
            relative_imports_order: self.relative_imports_order.unwrap_or_default(),
            single_line_exclusions: FxHashSet::from_iter(
                self.single_line_exclusions.unwrap_or_default(),
            ),
            split_on_trailing_comma: self.split_on_trailing_comma.unwrap_or(true),
            classes: FxHashSet::from_iter(self.classes.unwrap_or_default()),
            constants: FxHashSet::from_iter(self.constants.unwrap_or_default()),
            variables: FxHashSet::from_iter(self.variables.unwrap_or_default()),
            no_lines_before: FxHashSet::from_iter(no_lines_before),
            import_headings: import_heading
                .into_iter()
                .map(|(section, heading)| (section, format!("# {heading}")))
                .collect(),
            lines_after_imports: self.lines_after_imports.unwrap_or(-1),
            lines_between_types,
            forced_separate: Vec::from_iter(self.forced_separate.unwrap_or_default()),
            section_order,
            default_section,
            no_sections,
            from_first,
            length_sort: self.length_sort.unwrap_or(false),
            length_sort_straight: self.length_sort_straight.unwrap_or(false),
        })
    }
}

/// Options for the `mccabe` plugin.
#[derive(
    Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize, OptionsMetadata, CombineOptions,
)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct McCabeOptions {
    /// The maximum McCabe complexity to allow before triggering `C901` errors.
    #[option(
        default = "10",
        value_type = "int",
        example = r#"
            # Flag errors (`C901`) whenever the complexity level exceeds 5.
            max-complexity = 5
        "#
    )]
    max_complexity: Option<usize>,
}

impl McCabeOptions {
    pub(crate) fn into_settings(self) -> mccabe::settings::Settings {
        mccabe::settings::Settings {
            max_complexity: self
                .max_complexity
                .unwrap_or(mccabe::settings::DEFAULT_MAX_COMPLEXITY),
        }
    }
}

/// Options for the `pep8-naming` plugin.
#[derive(
    Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize, OptionsMetadata, CombineOptions,
)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Pep8NamingOptions {
    /// A list of names (or patterns) to ignore when considering `pep8-naming` violations.
    ///
    /// Supports glob patterns. For example, to ignore all names starting with `test_`
    /// or ending with `_test`, you could use `ignore-names = ["test_*", "*_test"]`.
    /// For more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).
    #[option(
        default = r#"["setUp", "tearDown", "setUpClass", "tearDownClass", "setUpModule", "tearDownModule", "asyncSetUp", "asyncTearDown", "setUpTestData", "failureException", "longMessage", "maxDiff"]"#,
        value_type = "list[str]",
        example = r#"
            ignore-names = ["callMethod"]
        "#
    )]
    ignore_names: Option<Vec<String>>,

    /// Additional names (or patterns) to ignore when considering `pep8-naming` violations,
    /// in addition to those included in [`ignore-names`](#lint_pep8-naming_ignore-names).
    ///
    /// Supports glob patterns. For example, to ignore all names starting with `test_`
    /// or ending with `_test`, you could use `ignore-names = ["test_*", "*_test"]`.
    /// For more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).
    #[option(
        default = r#"[]"#,
        value_type = "list[str]",
        example = r#"extend-ignore-names = ["callMethod"]"#
    )]
    extend_ignore_names: Option<Vec<String>>,

    /// A list of decorators that, when applied to a method, indicate that the
    /// method should be treated as a class method (in addition to the builtin
    /// `@classmethod`).
    ///
    /// For example, Ruff will expect that any method decorated by a decorator
    /// in this list takes a `cls` argument as its first argument.
    ///
    /// Expects to receive a list of fully-qualified names (e.g., `pydantic.validator`,
    /// rather than `validator`) or alternatively a plain name which is then matched against
    /// the last segment in case the decorator itself consists of a dotted name.
    #[option(
        default = r#"[]"#,
        value_type = "list[str]",
        example = r#"
            classmethod-decorators = [
                # Allow Pydantic's `@validator` decorator to trigger class method treatment.
                "pydantic.validator",
                # Allow SQLAlchemy's dynamic decorators, like `@field.expression`, to trigger class method treatment.
                "declared_attr",
                "expression",
                "comparator",
            ]
        "#
    )]
    classmethod_decorators: Option<Vec<String>>,

    /// A list of decorators that, when applied to a method, indicate that the
    /// method should be treated as a static method (in addition to the builtin
    /// `@staticmethod`).
    ///
    /// For example, Ruff will expect that any method decorated by a decorator
    /// in this list has no `self` or `cls` argument.
    ///
    /// Expects to receive a list of fully-qualified names (e.g., `belay.Device.teardown`,
    /// rather than `teardown`) or alternatively a plain name which is then matched against
    /// the last segment in case the decorator itself consists of a dotted name.
    #[option(
        default = r#"[]"#,
        value_type = "list[str]",
        example = r#"
            # Allow Belay's `@Device.teardown` decorator to trigger static method treatment.
            staticmethod-decorators = ["belay.Device.teardown"]
        "#
    )]
    staticmethod_decorators: Option<Vec<String>>,
}

impl Pep8NamingOptions {
    pub(crate) fn try_into_settings(
        self,
    ) -> Result<pep8_naming::settings::Settings, pep8_naming::settings::SettingsError> {
        Ok(pep8_naming::settings::Settings {
            ignore_names: IgnoreNames::from_options(self.ignore_names, self.extend_ignore_names)?,
            classmethod_decorators: self.classmethod_decorators.unwrap_or_default(),
            staticmethod_decorators: self.staticmethod_decorators.unwrap_or_default(),
        })
    }
}

/// Options for the `pycodestyle` plugin.
#[derive(
    Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize, OptionsMetadata, CombineOptions,
)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct PycodestyleOptions {
    /// The maximum line length to allow for [`line-too-long`](https://docs.astral.sh/ruff/rules/line-too-long/) violations. By default,
    /// this is set to the value of the [`line-length`](#line-length) option.
    ///
    /// Use this option when you want to detect extra-long lines that the formatter can't automatically split by setting
    /// `pycodestyle.line-length` to a value larger than [`line-length`](#line-length).
    ///
    /// ```toml
    /// # The formatter wraps lines at a length of 88.
    /// line-length = 88
    ///
    /// [pycodestyle]
    /// # E501 reports lines that exceed the length of 100.
    /// max-line-length = 100
    /// ```
    ///
    /// The length is determined by the number of characters per line, except for lines containing East Asian characters or emojis.
    /// For these lines, the [unicode width](https://unicode.org/reports/tr11/) of each character is added up to determine the length.
    ///
    /// See the [`line-too-long`](https://docs.astral.sh/ruff/rules/line-too-long/) rule for more information.
    #[option(
        default = "null",
        value_type = "int",
        example = r#"
            max-line-length = 100
        "#
    )]
    pub max_line_length: Option<LineLength>,

    /// The maximum line length to allow for [`doc-line-too-long`](https://docs.astral.sh/ruff/rules/doc-line-too-long/) violations within
    /// documentation (`W505`), including standalone comments. By default,
    /// this is set to `null` which disables reporting violations.
    ///
    /// The length is determined by the number of characters per line, except for lines containing Asian characters or emojis.
    /// For these lines, the [unicode width](https://unicode.org/reports/tr11/) of each character is added up to determine the length.
    ///
    /// See the [`doc-line-too-long`](https://docs.astral.sh/ruff/rules/doc-line-too-long/) rule for more information.
    #[option(
        default = "null",
        value_type = "int",
        example = r#"
            max-doc-length = 88
        "#
    )]
    pub max_doc_length: Option<LineLength>,

    /// Whether line-length violations (`E501`) should be triggered for
    /// comments starting with [`task-tags`](#lint_task-tags) (by default: "TODO", "FIXME",
    /// and "XXX").
    #[option(
        default = "false",
        value_type = "bool",
        example = r#"
            ignore-overlong-task-comments = true
        "#
    )]
    pub ignore_overlong_task_comments: Option<bool>,
}

impl PycodestyleOptions {
    pub(crate) fn into_settings(
        self,
        global_line_length: LineLength,
    ) -> pycodestyle::settings::Settings {
        pycodestyle::settings::Settings {
            max_doc_length: self.max_doc_length,
            max_line_length: self.max_line_length.unwrap_or(global_line_length),
            ignore_overlong_task_comments: self.ignore_overlong_task_comments.unwrap_or_default(),
        }
    }
}

/// Options for the `pydocstyle` plugin.
#[derive(
    Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize, OptionsMetadata, CombineOptions,
)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct PydocstyleOptions {
    /// Whether to use Google-style, NumPy-style conventions, or the [PEP 257](https://peps.python.org/pep-0257/)
    /// defaults when analyzing docstring sections.
    ///
    /// Enabling a convention will disable all rules that are not included in
    /// the specified convention. As such, the intended workflow is to enable a
    /// convention and then selectively enable or disable any additional rules
    /// on top of it.
    ///
    /// For example, to use Google-style conventions but avoid requiring
    /// documentation for every function parameter:
    ///
    /// ```toml
    /// [tool.ruff.lint]
    /// # Enable all `pydocstyle` rules, limiting to those that adhere to the
    /// # Google convention via `convention = "google"`, below.
    /// select = ["D"]
    ///
    /// # On top of the Google convention, disable `D417`, which requires
    /// # documentation for every function parameter.
    /// ignore = ["D417"]
    ///
    /// [tool.ruff.lint.pydocstyle]
    /// convention = "google"
    /// ```
    ///
    /// The PEP 257 convention includes all `D` errors apart from:
    /// [`D203`](rules/incorrect-blank-line-before-class.md),
    /// [`D212`](rules/multi-line-summary-first-line.md),
    /// [`D213`](rules/multi-line-summary-second-line.md),
    /// [`D214`](rules/overindented-section.md),
    /// [`D215`](rules/overindented-section-underline.md),
    /// [`D404`](rules/docstring-starts-with-this.md),
    /// [`D405`](rules/non-capitalized-section-name.md),
    /// [`D406`](rules/missing-new-line-after-section-name.md),
    /// [`D407`](rules/missing-dashed-underline-after-section.md),
    /// [`D408`](rules/missing-section-underline-after-name.md),
    /// [`D409`](rules/mismatched-section-underline-length.md),
    /// [`D410`](rules/no-blank-line-after-section.md),
    /// [`D411`](rules/no-blank-line-before-section.md),
    /// [`D413`](rules/missing-blank-line-after-last-section.md),
    /// [`D415`](rules/missing-terminal-punctuation.md),
    /// [`D416`](rules/missing-section-name-colon.md),
    /// [`D417`](rules/undocumented-param.md), and
    /// [`D420`](rules/incorrect-section-order.md).
    ///
    /// The NumPy convention includes all `D` errors apart from:
    /// [`D107`](rules/undocumented-public-init.md),
    /// [`D203`](rules/incorrect-blank-line-before-class.md),
    /// [`D212`](rules/multi-line-summary-first-line.md),
    /// [`D213`](rules/multi-line-summary-second-line.md),
    /// [`D402`](rules/signature-in-docstring.md),
    /// [`D413`](rules/missing-blank-line-after-last-section.md),
    /// [`D415`](rules/missing-terminal-punctuation.md),
    /// [`D416`](rules/missing-section-name-colon.md), and
    /// [`D417`](rules/undocumented-param.md).
    ///
    /// The Google convention includes all `D` errors apart from:
    /// [`D203`](rules/incorrect-blank-line-before-class.md),
    /// [`D204`](rules/incorrect-blank-line-after-class.md),
    /// [`D213`](rules/multi-line-summary-second-line.md),
    /// [`D215`](rules/overindented-section-underline.md),
    /// [`D400`](rules/missing-trailing-period.md),
    /// [`D401`](rules/non-imperative-mood.md),
    /// [`D404`](rules/docstring-starts-with-this.md),
    /// [`D406`](rules/missing-new-line-after-section-name.md),
    /// [`D407`](rules/missing-dashed-underline-after-section.md),
    /// [`D408`](rules/missing-section-underline-after-name.md),
    /// [`D409`](rules/mismatched-section-underline-length.md), and
    /// [`D413`](rules/missing-blank-line-after-last-section.md).
    ///
    /// For more information see the [FAQ](faq.md#does-ruff-support-numpy-or-google-style-docstrings) entry.
    ///
    /// To enable an additional rule that's excluded from the convention,
    /// select the desired rule via its fully qualified rule code (e.g.,
    /// `D400` instead of `D4` or `D40`):
    ///
    /// ```toml
    /// [tool.ruff.lint]
    /// # Enable D400 on top of the Google convention.
    /// extend-select = ["D400"]
    ///
    /// [tool.ruff.lint.pydocstyle]
    /// convention = "google"
    /// ```
    #[option(
        default = r#"null"#,
        value_type = r#""google" | "numpy" | "pep257""#,
        example = r#"
            # Use Google-style docstrings.
            convention = "google"
        "#
    )]
    pub(crate) convention: Option<Convention>,

    /// Ignore docstrings for functions or methods decorated with the
    /// specified fully-qualified decorators.
    #[option(
        default = r#"[]"#,
        value_type = "list[str]",
        example = r#"
            ignore-decorators = ["typing.overload"]
        "#
    )]
    pub(crate) ignore_decorators: Option<Vec<String>>,

    /// A list of decorators that, when applied to a method, indicate that the
    /// method should be treated as a property (in addition to the builtin
    /// `@property` and standard-library `@functools.cached_property`).
    ///
    /// For example, Ruff will expect that any method decorated by a decorator
    /// in this list can use a non-imperative summary line.
    #[option(
        default = r#"[]"#,
        value_type = "list[str]",
        example = r#"
            property-decorators = ["gi.repository.GObject.Property"]
        "#
    )]
    pub(crate) property_decorators: Option<Vec<String>>,

    /// If set to `true`, ignore missing documentation for `*args` and `**kwargs` parameters.
    #[option(
        default = r#"false"#,
        value_type = "bool",
        example = r#"
            ignore-var-parameters = true
        "#
    )]
    pub(crate) ignore_var_parameters: Option<bool>,
}

impl PydocstyleOptions {
    pub(crate) fn into_settings(self) -> pydocstyle::settings::Settings {
        let PydocstyleOptions {
            convention,
            ignore_decorators,
            property_decorators,
            ignore_var_parameters: ignore_variadics,
        } = self;
        pydocstyle::settings::Settings {
            convention,
            ignore_decorators: BTreeSet::from_iter(ignore_decorators.unwrap_or_default()),
            property_decorators: BTreeSet::from_iter(property_decorators.unwrap_or_default()),
            ignore_var_parameters: ignore_variadics.unwrap_or_default(),
        }
    }
}

/// Options for the `pydoclint` plugin.
#[derive(
    Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize, OptionsMetadata, CombineOptions,
)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct PydoclintOptions {
    /// Skip docstrings which fit on a single line.
    ///
    /// Note: The corresponding setting in `pydoclint`
    /// is named `skip-checking-short-docstrings`.
    #[option(
        default = r#"false"#,
        value_type = "bool",
        example = r#"
            # Skip docstrings which fit on a single line.
            ignore-one-line-docstrings = true
        "#
    )]
    ignore_one_line_docstrings: Option<bool>,
}

impl PydoclintOptions {
    pub(crate) fn into_settings(self) -> pydoclint::settings::Settings {
        pydoclint::settings::Settings {
            ignore_one_line_docstrings: self.ignore_one_line_docstrings.unwrap_or_default(),
        }
    }
}

/// Options for the `pyflakes` plugin.
#[derive(
    Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize, OptionsMetadata, CombineOptions,
)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct PyflakesOptions {
    /// Additional functions or classes to consider generic, such that any
    /// subscripts should be treated as type annotation (e.g., `ForeignKey` in
    /// `django.db.models.ForeignKey["User"]`.
    ///
    /// Expects to receive a list of fully-qualified names (e.g., `django.db.models.ForeignKey`,
    /// rather than `ForeignKey`).
    #[option(
        default = r#"[]"#,
        value_type = "list[str]",
        example = "extend-generics = [\"django.db.models.ForeignKey\"]"
    )]
    extend_generics: Option<Vec<String>>,

    /// A list of modules to ignore when considering unused imports.
    ///
    /// Used to prevent violations for specific modules that are known to have side effects on
    /// import (e.g., `hvplot.pandas`).
    ///
    /// Modules in this list are expected to be fully-qualified names (e.g., `hvplot.pandas`). Any
    /// submodule of a given module will also be ignored (e.g., given `hvplot`, `hvplot.pandas`
    /// will also be ignored).
    #[option(
        default = r#"[]"#,
        value_type = "list[str]",
        example = r#"allowed-unused-imports = ["hvplot.pandas"]"#
    )]
    allowed_unused_imports: Option<Vec<String>>,
}

impl PyflakesOptions {
    pub(crate) fn into_settings(self) -> pyflakes::settings::Settings {
        pyflakes::settings::Settings {
            extend_generics: self.extend_generics.unwrap_or_default(),
            allowed_unused_imports: self.allowed_unused_imports.unwrap_or_default(),
        }
    }
}

/// Options for the `pylint` plugin.
#[derive(
    Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize, OptionsMetadata, CombineOptions,
)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct PylintOptions {
    /// Constant types to ignore when used as "magic values" (see `PLR2004`).
    #[option(
        default = r#"["str", "bytes"]"#,
        value_type = r#"list["str" | "bytes" | "complex" | "float" | "int"]"#,
        example = r#"
            allow-magic-value-types = ["int"]
        "#
    )]
    allow_magic_value_types: Option<Vec<ConstantType>>,

    /// Dunder methods name to allow, in addition to the default set from the
    /// Python standard library (see `PLW3201`).
    #[option(
        default = r#"[]"#,
        value_type = r#"list[str]"#,
        example = r#"
            allow-dunder-method-names = ["__tablename__", "__table_args__"]
        "#
    )]
    allow_dunder_method_names: Option<FxHashSet<String>>,

    /// Maximum number of branches allowed for a function or method body (see `PLR0912`).
    #[option(default = r"12", value_type = "int", example = r"max-branches = 15")]
    max_branches: Option<usize>,

    /// Maximum number of return statements allowed for a function or method
    /// body (see `PLR0911`)
    #[option(default = r"6", value_type = "int", example = r"max-returns = 10")]
    max_returns: Option<usize>,

    /// Maximum number of arguments allowed for a function or method definition
    /// (see `PLR0913`).
    #[option(default = r"5", value_type = "int", example = r"max-args = 10")]
    max_args: Option<usize>,

    /// Maximum number of positional arguments allowed for a function or method definition
    /// (see `PLR0917`).
    ///
    /// If not specified, defaults to the value of `max-args`.
    #[option(
        default = r"5", // Needs to be in sync with default of `max-args`.
        value_type = "int",
        example = r"max-positional-args = 3"
    )]
    max_positional_args: Option<usize>,

    /// Maximum number of local variables allowed for a function or method body (see `PLR0914`).
    #[option(default = r"15", value_type = "int", example = r"max-locals = 20")]
    max_locals: Option<usize>,

    /// Maximum number of statements allowed for a function or method body (see `PLR0915`).
    #[option(default = r"50", value_type = "int", example = r"max-statements = 75")]
    max_statements: Option<usize>,

    /// Maximum number of statements allowed for a try clause body (see `W0717`).
    #[option(
        default = r"5",
        value_type = "int",
        example = r"max-statements-in-try = 10"
    )]
    max_statements_in_try: Option<usize>,

    /// Maximum number of public methods allowed for a class (see `PLR0904`).
    #[option(
        default = r"20",
        value_type = "int",
        example = r"max-public-methods = 30"
    )]
    max_public_methods: Option<usize>,

    /// Maximum number of Boolean expressions allowed within a single `if` statement
    /// (see `PLR0916`).
    #[option(default = r"5", value_type = "int", example = r"max-bool-expr = 10")]
    max_bool_expr: Option<usize>,

    /// Maximum number of nested blocks allowed within a function or method body
    /// (see `PLR1702`).
    #[option(
        default = r"5",
        value_type = "int",
        example = r"max-nested-blocks = 10"
    )]
    max_nested_blocks: Option<usize>,
}

impl PylintOptions {
    pub(crate) fn into_settings(self) -> pylint::settings::Settings {
        let defaults = pylint::settings::Settings::default();
        pylint::settings::Settings {
            allow_magic_value_types: self
                .allow_magic_value_types
                .unwrap_or(defaults.allow_magic_value_types),
            allow_dunder_method_names: self.allow_dunder_method_names.unwrap_or_default(),
            max_args: self.max_args.unwrap_or(defaults.max_args),
            max_positional_args: self
                .max_positional_args
                .or(self.max_args)
                .unwrap_or(defaults.max_positional_args),
            max_bool_expr: self.max_bool_expr.unwrap_or(defaults.max_bool_expr),
            max_returns: self.max_returns.unwrap_or(defaults.max_returns),
            max_branches: self.max_branches.unwrap_or(defaults.max_branches),
            max_statements: self.max_statements.unwrap_or(defaults.max_statements),
            max_statements_in_try: self
                .max_statements_in_try
                .unwrap_or(defaults.max_statements_in_try),
            max_public_methods: self
                .max_public_methods
                .unwrap_or(defaults.max_public_methods),
            max_locals: self.max_locals.unwrap_or(defaults.max_locals),
            max_nested_blocks: self.max_nested_blocks.unwrap_or(defaults.max_nested_blocks),
        }
    }
}

/// Options for the `pyupgrade` plugin.
#[derive(
    Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize, OptionsMetadata, CombineOptions,
)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct PyUpgradeOptions {
    /// Whether to avoid [PEP 585](https://peps.python.org/pep-0585/) (`List[int]` -> `list[int]`) and [PEP 604](https://peps.python.org/pep-0604/)
    /// (`Union[str, int]` -> `str | int`) rewrites even if a file imports
    /// `from __future__ import annotations`.
    ///
    /// This setting is only applicable when the target Python version is below
    /// 3.9 and 3.10 respectively, and is most commonly used when working with
    /// libraries like Pydantic and FastAPI, which rely on the ability to parse
    /// type annotations at runtime. The use of `from __future__ import annotations`
    /// causes Python to treat the type annotations as strings, which typically
    /// allows for the use of language features that appear in later Python
    /// versions but are not yet supported by the current version (e.g., `str |
    /// int`). However, libraries that rely on runtime type annotations will
    /// break if the annotations are incompatible with the current Python
    /// version.
    ///
    /// For example, while the following is valid Python 3.8 code due to the
    /// presence of `from __future__ import annotations`, the use of `str | int`
    /// prior to Python 3.10 will cause Pydantic to raise a `TypeError` at
    /// runtime:
    ///
    /// ```python
    /// from __future__ import annotations
    ///
    /// import pydantic
    ///
    /// class Foo(pydantic.BaseModel):
    ///     bar: str | int
    /// ```
    ///
    ///
    #[option(
        default = r#"false"#,
        value_type = "bool",
        example = r#"
            # Preserve types, even if a file imports `from __future__ import annotations`.
            keep-runtime-typing = true
        "#
    )]
    keep_runtime_typing: Option<bool>,
}

impl PyUpgradeOptions {
    pub(crate) fn into_settings(self) -> pyupgrade::settings::Settings {
        pyupgrade::settings::Settings {
            keep_runtime_typing: self.keep_runtime_typing.unwrap_or_default(),
        }
    }
}

/// Options for the `ruff` plugin
#[derive(
    Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize, OptionsMetadata, CombineOptions,
)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct RuffOptions {
    /// Whether to prefer accessing items keyed by tuples with
    /// parentheses around the tuple (see `RUF031`).
    #[option(
        default = r#"false"#,
        value_type = "bool",
        example = r#"
        # Make it a violation to use a tuple in a subscript without parentheses.
        parenthesize-tuple-in-subscript = true
        "#
    )]
    parenthesize_tuple_in_subscript: Option<bool>,

    /// A list of additional callable names that behave like
    /// [`markupsafe.Markup`](https://markupsafe.palletsprojects.com/en/stable/escaping/#markupsafe.Markup).
    ///
    /// Expects to receive a list of fully-qualified names (e.g., `webhelpers.html.literal`, rather than
    /// `literal`).
    #[option(
        default = "[]",
        value_type = "list[str]",
        example = "extend-markup-names = [\"webhelpers.html.literal\", \"my_package.Markup\"]"
    )]
    #[deprecated(
        since = "0.10.0",
        note = "The `extend-markup-names` option has been moved to the `flake8-bandit` section of \
            the configuration."
    )]
    extend_markup_names: Option<Vec<String>>,

    /// A list of callable names, whose result may be safely passed into
    /// [`markupsafe.Markup`](https://markupsafe.palletsprojects.com/en/stable/escaping/#markupsafe.Markup).
    ///
    /// Expects to receive a list of fully-qualified names (e.g., `bleach.clean`, rather than `clean`).
    ///
    /// This setting helps you avoid false positives in code like:
    ///
    /// ```python
    /// from bleach import clean
    /// from markupsafe import Markup
    ///
    /// cleaned_markup = Markup(clean(some_user_input))
    /// ```
    ///
    /// Where the use of [`bleach.clean`](https://bleach.readthedocs.io/en/latest/clean.html)
    /// usually ensures that there's no XSS vulnerability.
    ///
    /// Although it is not recommended, you may also use this setting to whitelist other
    /// kinds of calls, e.g. calls to i18n translation functions, where how safe that is
    /// will depend on the implementation and how well the translations are audited.
    ///
    /// Another common use-case is to wrap the output of functions that generate markup
    /// like [`xml.etree.ElementTree.tostring`](https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.tostring)
    /// or template rendering engines where sanitization of potential user input is either
    /// already baked in or has to happen before rendering.
    #[option(
        default = "[]",
        value_type = "list[str]",
        example = "allowed-markup-calls = [\"bleach.clean\", \"my_package.sanitize\"]"
    )]
    #[deprecated(
        since = "0.10.0",
        note = "The `allowed-markup-names` option has been moved to the `flake8-bandit` section \
            of the configuration."
    )]
    allowed_markup_calls: Option<Vec<String>>,
    /// Whether to require `__init__.py` files to contain no code at all, including imports and
    /// docstrings (see `RUF067`).
    #[option(
        default = r#"false"#,
        value_type = "bool",
        example = r#"
        # Make it a violation to include any code, including imports and docstrings in `__init__.py`
        strictly-empty-init-modules = true
        "#
    )]
    strictly_empty_init_modules: Option<bool>,
}

impl RuffOptions {
    pub(crate) fn into_settings(self) -> ruff::settings::Settings {
        ruff::settings::Settings {
            parenthesize_tuple_in_subscript: self
                .parenthesize_tuple_in_subscript
                .unwrap_or_default(),
            strictly_empty_init_modules: self.strictly_empty_init_modules.unwrap_or_default(),
        }
    }
}

/// Configures the way Ruff formats your code.
#[derive(
    Clone, Debug, PartialEq, Eq, Default, Deserialize, Serialize, OptionsMetadata, CombineOptions,
)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct FormatOptions {
    /// A list of file patterns to exclude from formatting in addition to the files excluded globally (see [`exclude`](#exclude), and [`extend-exclude`](#extend-exclude)).
    ///
    /// Exclusions are based on globs, and can be either:
    ///
    /// - Single-path patterns, like `.mypy_cache` (to exclude any directory
    ///   named `.mypy_cache` in the tree), `foo.py` (to exclude any file named
    ///   `foo.py`), or `foo_*.py` (to exclude any file matching `foo_*.py` ).
    /// - Relative patterns, like `directory/foo.py` (to exclude that specific
    ///   file) or `directory/*.py` (to exclude any Python files in
    ///   `directory`). Note that these paths are relative to the project root
    ///   (e.g., the directory containing your `pyproject.toml`).
    ///
    /// For more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).
    #[option(
        default = r#"[]"#,
        value_type = "list[str]",
        example = r#"
            exclude = ["generated"]
        "#
    )]
    pub exclude: Option<Vec<String>>,

    /// Whether to enable the unstable preview style formatting.
    #[option(
        default = "false",
        value_type = "bool",
        example = r#"
            # Enable preview style formatting.
            preview = true
        "#
    )]
    pub preview: Option<bool>,

    /// Whether to use spaces or tabs for indentation.
    ///
    /// `indent-style = "space"` (default):
    ///
    /// ```python
    /// def f():
    ///     print("Hello") #  Spaces indent the `print` statement.
    /// ```
    ///
    /// `indent-style = "tab"`:
    ///
    /// ```python
    /// def f():
    ///     print("Hello") #  A tab `\t` indents the `print` statement.
    /// ```
    ///
    /// PEP 8 recommends using spaces for [indentation](https://peps.python.org/pep-0008/#indentation).
    /// We care about accessibility; if you do not need tabs for accessibility, we do not recommend you use them.
    ///
    /// See [`indent-width`](#indent-width) to configure the number of spaces per indentation and the tab width.
    #[option(
        default = r#""space""#,
        value_type = r#""space" | "tab""#,
        example = r#"
            # Use tabs instead of 4 space indentation.
            indent-style = "tab"
        "#
    )]
    pub indent_style: Option<IndentStyle>,

    /// Configures the preferred quote character for strings. The recommended options are
    ///
    /// * `double` (default): Use double quotes `"`
    /// * `single`: Use single quotes `'`
    ///
    /// In compliance with [PEP 8](https://peps.python.org/pep-0008/) and [PEP 257](https://peps.python.org/pep-0257/),
    /// Ruff prefers double quotes for triple quoted strings and docstrings even when using `quote-style = "single"`.
    ///
    /// Ruff deviates from using the configured quotes if doing so prevents the need for
    /// escaping quote characters inside the string:
    ///
    /// ```python
    /// a = "a string without any quotes"
    /// b = "It's monday morning"
    /// ```
    ///
    /// Ruff will change the quotes of the string assigned to `a` to single quotes when using `quote-style = "single"`.
    /// However, Ruff uses double quotes for the string assigned to `b` because using single quotes would require escaping the `'`,
    /// which leads to the less readable code: `'It\'s monday morning'`.
    ///
    /// In addition, Ruff supports the quote style `preserve` for projects that already use
    /// a mixture of single and double quotes and can't migrate to the `double` or `single` style.
    /// The quote style `preserve` leaves the quotes of all strings unchanged.
    #[option(
        default = r#""double""#,
        value_type = r#""double" | "single" | "preserve""#,
        example = r#"
            # Prefer single quotes over double quotes.
            quote-style = "single"
        "#
    )]
    pub quote_style: Option<QuoteStyle>,

    /// Controls the quote style for nested strings inside interpolated string expressions.
    ///
    /// - `alternating` (default): Use alternating quotes.
    /// - `preferred`: Use the configured [`quote-style`](#format_quote-style).
    ///
    /// ```python
    /// f"{data['key']}"  # alternating (default)
    /// f"{data["key"]}"  # preferred
    /// ```
    ///
    /// Note: This setting has no effect when targeting Python versions below 3.12.
    #[option(
        default = r#""alternating""#,
        value_type = r#""alternating" | "preferred""#,
        example = r#"
            # Use the configured quote style for nested strings (Python 3.12+ only).
            nested-string-quote-style = "preferred"
        "#
    )]
    pub nested_string_quote_style: Option<ruff_python_formatter::NestedStringQuoteStyle>,

    /// Ruff uses existing trailing commas as an indication that short lines should be left separate.
    /// If this option is set to `true`, the magic trailing comma is ignored.
    ///
    /// For example, Ruff leaves the arguments separate even though
    /// collapsing the arguments to a single line doesn't exceed the line length if `skip-magic-trailing-comma = false`:
    ///
    /// ```python
    /// # The arguments remain on separate lines because of the trailing comma after `b`
    /// def test(
    ///     a,
    ///     b,
    /// ): pass
    /// ```
    ///
    /// Setting `skip-magic-trailing-comma = true` changes the formatting to:
    ///
    /// ```python
    /// # The arguments are collapsed to a single line because the trailing comma is ignored
    /// def test(a, b):
    ///     pass
    /// ```
    #[option(
        default = r#"false"#,
        value_type = r#"bool"#,
        example = "skip-magic-trailing-comma = true"
    )]
    pub skip_magic_trailing_comma: Option<bool>,

    /// The character Ruff uses at the end of a line.
    ///
    /// * `auto`: The newline style is detected automatically on a file per file basis. Files with mixed line endings will be converted to the first detected line ending. Defaults to `\n` for files that contain no line endings.
    /// * `lf`: Line endings will be converted to `\n`. The default line ending on Unix.
    /// * `cr-lf`: Line endings will be converted to `\r\n`. The default line ending on Windows.
    /// * `native`: Line endings will be converted to `\n` on Unix and `\r\n` on Windows.
    #[option(
        default = r#""auto""#,
        value_type = r#""auto" | "lf" | "cr-lf" | "native""#,
        example = r#"
            # Use `\n` line endings for all files
            line-ending = "lf"
        "#
    )]
    pub line_ending: Option<LineEnding>,

    /// Whether to format code snippets in docstrings.
    ///
    /// When this is enabled, Python code examples within docstrings are
    /// automatically reformatted.
    ///
    /// For example, when this is enabled, the following code:
    ///
    /// ```python
    /// def f(x):
    ///     """
    ///     Something about `f`. And an example in doctest format:
    ///
    ///     >>> f(  x  )
    ///
    ///     Markdown is also supported:
    ///
    ///     ```py
    ///     f(  x  )
    ///     ```
    ///
    ///     As are reStructuredText literal blocks::
    ///
    ///         f(  x  )
    ///
    ///
    ///     And reStructuredText code blocks:
    ///
    ///     .. code-block:: python
    ///
    ///         f(  x  )
    ///     """
    ///     pass
    /// ```
    ///
    /// ... will be reformatted (assuming the rest of the options are set to
    /// their defaults) as:
    ///
    /// ```python
    /// def f(x):
    ///     """
    ///     Something about `f`. And an example in doctest format:
    ///
    ///     >>> f(x)
    ///
    ///     Markdown is also supported:
    ///
    ///     ```py
    ///     f(x)
    ///     ```
    ///
    ///     As are reStructuredText literal blocks::
    ///
    ///         f(x)
    ///
    ///
    ///     And reStructuredText code blocks:
    ///
    ///     .. code-block:: python
    ///
    ///         f(x)
    ///     """
    ///     pass
    /// ```
    ///
    /// If a code snippet in a docstring contains invalid Python code or if the
    /// formatter would otherwise write invalid Python code, then the code
    /// example is ignored by the formatter and kept as-is.
    ///
    /// Currently, doctest, Markdown, reStructuredText literal blocks, and
    /// reStructuredText code blocks are all supported and automatically
    /// recognized. In the case of unlabeled fenced code blocks in Markdown and
    /// reStructuredText literal blocks, the contents are assumed to be Python
    /// and reformatted. As with any other format, if the contents aren't valid
    /// Python, then the block is left untouched automatically.
    #[option(
        default = "false",
        value_type = "bool",
        example = r#"
            # Enable reformatting of code snippets in docstrings.
            docstring-code-format = true
        "#
    )]
    pub docstring_code_format: Option<bool>,

    /// Set the line length used when formatting code snippets in docstrings.
    ///
    /// This only has an effect when the `docstring-code-format` setting is
    /// enabled.
    ///
    /// The default value for this setting is `"dynamic"`, which has the effect
    /// of ensuring that any reformatted code examples in docstrings adhere to
    /// the global line length configuration that is used for the surrounding
    /// Python code. The point of this setting is that it takes the indentation
    /// of the docstring into account when reformatting code examples.
    ///
    /// Alternatively, this can be set to a fixed integer, which will result
    /// in the same line length limit being applied to all reformatted code
    /// examples in docstrings. When set to a fixed integer, the indent of the
    /// docstring is not taken into account. That is, this may result in lines
    /// in the reformatted code example that exceed the globally configured
    /// line length limit.
    ///
    /// For example, when this is set to `20` and [`docstring-code-format`](#docstring-code-format)
    /// is enabled, then this code:
    ///
    /// ```python
    /// def f(x):
    ///     '''
    ///     Something about `f`. And an example:
    ///
    ///     .. code-block:: python
    ///
    ///         foo, bar, quux = this_is_a_long_line(lion, hippo, lemur, bear)
    ///     '''
    ///     pass
    /// ```
    ///
    /// ... will be reformatted (assuming the rest of the options are set
    /// to their defaults) as:
    ///
    /// ```python
    /// def f(x):
    ///     """
    ///     Something about `f`. And an example:
    ///
    ///     .. code-block:: python
    ///
    ///         (
    ///             foo,
    ///             bar,
    ///             quux,
    ///         ) = this_is_a_long_line(
    ///             lion,
    ///             hippo,
    ///             lemur,
    ///             bear,
    ///         )
    ///     """
    ///     pass
    /// ```
    #[option(
        default = r#""dynamic""#,
        value_type = r#"int | "dynamic""#,
        example = r#"
            # Format all docstring code snippets with a line length of 60.
            docstring-code-line-length = 60
        "#
    )]
    pub docstring_code_line_length: Option<DocstringCodeLineWidth>,
}

/// Configures Ruff's `analyze` command.
#[derive(
    Clone, Debug, PartialEq, Eq, Default, Deserialize, Serialize, OptionsMetadata, CombineOptions,
)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct AnalyzeOptions {
    /// A list of file patterns to exclude from analysis in addition to the files excluded globally (see [`exclude`](#exclude), and [`extend-exclude`](#extend-exclude)).
    ///
    /// Exclusions are based on globs, and can be either:
    ///
    /// - Single-path patterns, like `.mypy_cache` (to exclude any directory
    ///   named `.mypy_cache` in the tree), `foo.py` (to exclude any file named
    ///   `foo.py`), or `foo_*.py` (to exclude any file matching `foo_*.py` ).
    /// - Relative patterns, like `directory/foo.py` (to exclude that specific
    ///   file) or `directory/*.py` (to exclude any Python files in
    ///   `directory`). Note that these paths are relative to the project root
    ///   (e.g., the directory containing your `pyproject.toml`).
    ///
    /// For more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).
    #[option(
        default = r#"[]"#,
        value_type = "list[str]",
        example = r#"
            exclude = ["generated"]
        "#
    )]
    pub(crate) exclude: Option<Vec<String>>,
    /// Whether to enable preview mode. When preview mode is enabled, Ruff will expose unstable
    /// commands.
    #[option(
        default = "false",
        value_type = "bool",
        example = r#"
            # Enable preview features.
            preview = true
        "#
    )]
    pub(crate) preview: Option<bool>,
    /// Whether to generate a map from file to files that it depends on (dependencies) or files that
    /// depend on it (dependents).
    #[option(
        default = r#""dependencies""#,
        value_type = r#""dependents" | "dependencies""#,
        example = r#"
            direction = "dependencies"
        "#
    )]
    pub(crate) direction: Option<Direction>,
    /// Whether to detect imports from string literals. When enabled, Ruff will search for string
    /// literals that "look like" import paths, and include them in the import map, if they resolve
    /// to valid Python modules.
    #[option(
        default = "false",
        value_type = "bool",
        example = r#"
            detect-string-imports = true
        "#
    )]
    pub(crate) detect_string_imports: Option<bool>,
    /// The minimum number of dots in a string to consider it a valid import.
    ///
    /// This setting is only relevant when [`detect-string-imports`](#detect-string-imports) is enabled.
    /// For example, if this is set to `2`, then only strings with at least two dots (e.g., `"path.to.module"`)
    /// would be considered valid imports.
    #[option(
        default = "2",
        value_type = "usize",
        example = r#"
            string-imports-min-dots = 2
        "#
    )]
    pub(crate) string_imports_min_dots: Option<usize>,
    /// A map from file path to the list of Python or non-Python file paths or globs that should be
    /// considered dependencies of that file, regardless of whether relevant imports are detected.
    #[option(
        default = "{}",
        scope = "include-dependencies",
        value_type = "dict[str, list[str]]",
        example = r#"
            "foo/bar.py" = ["foo/baz/*.py"]
            "foo/baz/reader.py" = ["configs/bar.json"]
        "#
    )]
    pub(crate) include_dependencies: Option<BTreeMap<PathBuf, Vec<String>>>,
    /// Whether to include imports that are only used for type checking (i.e., imports within `if TYPE_CHECKING:` blocks).
    /// When enabled (default), type-checking-only imports are included in the import graph.
    /// When disabled, they are excluded.
    #[option(
        default = "true",
        value_type = "bool",
        example = r#"
            # Exclude type-checking-only imports from the graph
            type-checking-imports = false
        "#
    )]
    pub(crate) type_checking_imports: Option<bool>,
}

/// Like [`LintCommonOptions`], but with any `#[serde(flatten)]` fields inlined. This leads to far,
/// far better error messages when deserializing.
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct LintOptionsWire {
    // common: LintCommonOptions
    allowed_confusables: Option<Vec<char>>,
    dummy_variable_rgx: Option<String>,
    extend_ignore: Option<Vec<UnresolvedRuleSelector>>,
    extend_select: Option<Vec<UnresolvedRuleSelector>>,
    extend_fixable: Option<Vec<UnresolvedRuleSelector>>,
    extend_unfixable: Option<Vec<UnresolvedRuleSelector>>,
    external: Option<Vec<String>>,
    fixable: Option<Vec<UnresolvedRuleSelector>>,
    ignore: Option<Vec<UnresolvedRuleSelector>>,
    extend_safe_fixes: Option<Vec<UnresolvedRuleSelector>>,
    extend_unsafe_fixes: Option<Vec<UnresolvedRuleSelector>>,
    ignore_init_module_imports: Option<bool>,
    logger_objects: Option<Vec<String>>,
    select: Option<Vec<UnresolvedRuleSelector>>,
    explicit_preview_rules: Option<bool>,
    task_tags: Option<Vec<String>>,
    typing_modules: Option<Vec<String>>,
    unfixable: Option<Vec<UnresolvedRuleSelector>>,
    flake8_annotations: Option<Flake8AnnotationsOptions>,
    flake8_bandit: Option<Flake8BanditOptions>,
    flake8_boolean_trap: Option<Flake8BooleanTrapOptions>,
    flake8_bugbear: Option<Flake8BugbearOptions>,
    flake8_builtins: Option<Flake8BuiltinsOptions>,
    flake8_comprehensions: Option<Flake8ComprehensionsOptions>,
    flake8_copyright: Option<Flake8CopyrightOptions>,
    flake8_errmsg: Option<Flake8ErrMsgOptions>,
    flake8_quotes: Option<Flake8QuotesOptions>,
    flake8_self: Option<Flake8SelfOptions>,
    flake8_tidy_imports: Option<Flake8TidyImportsOptions>,
    flake8_type_checking: Option<Flake8TypeCheckingOptions>,
    flake8_gettext: Option<Flake8GetTextOptions>,
    flake8_implicit_str_concat: Option<Flake8ImplicitStrConcatOptions>,
    flake8_import_conventions: Option<Flake8ImportConventionsOptions>,
    flake8_pytest_style: Option<Flake8PytestStyleOptions>,
    flake8_unused_arguments: Option<Flake8UnusedArgumentsOptions>,
    isort: Option<IsortOptions>,
    mccabe: Option<McCabeOptions>,
    pep8_naming: Option<Pep8NamingOptions>,
    pycodestyle: Option<PycodestyleOptions>,
    pydocstyle: Option<PydocstyleOptions>,
    pyflakes: Option<PyflakesOptions>,
    pylint: Option<PylintOptions>,
    pyupgrade: Option<PyUpgradeOptions>,
    per_file_ignores: Option<FxHashMap<String, Vec<UnresolvedRuleSelector>>>,
    extend_per_file_ignores: Option<FxHashMap<String, Vec<UnresolvedRuleSelector>>>,

    exclude: Option<Vec<String>>,
    pydoclint: Option<PydoclintOptions>,
    ruff: Option<RuffOptions>,
    preview: Option<bool>,
    typing_extensions: Option<bool>,
    future_annotations: Option<bool>,
}

impl From<LintOptionsWire> for LintOptions {
    fn from(value: LintOptionsWire) -> LintOptions {
        let LintOptionsWire {
            allowed_confusables,
            dummy_variable_rgx,
            extend_ignore,
            extend_select,
            extend_fixable,
            extend_unfixable,
            external,
            fixable,
            ignore,
            extend_safe_fixes,
            extend_unsafe_fixes,
            ignore_init_module_imports,
            logger_objects,
            select,
            explicit_preview_rules,
            task_tags,
            typing_modules,
            unfixable,
            flake8_annotations,
            flake8_bandit,
            flake8_boolean_trap,
            flake8_bugbear,
            flake8_builtins,
            flake8_comprehensions,
            flake8_copyright,
            flake8_errmsg,
            flake8_quotes,
            flake8_self,
            flake8_tidy_imports,
            flake8_type_checking,
            flake8_gettext,
            flake8_implicit_str_concat,
            flake8_import_conventions,
            flake8_pytest_style,
            flake8_unused_arguments,
            isort,
            mccabe,
            pep8_naming,
            pycodestyle,
            pydocstyle,
            pyflakes,
            pylint,
            pyupgrade,
            per_file_ignores,
            extend_per_file_ignores,
            exclude,
            pydoclint,
            ruff,
            preview,
            typing_extensions,
            future_annotations,
        } = value;

        LintOptions {
            #[expect(deprecated)]
            common: LintCommonOptions {
                allowed_confusables,
                dummy_variable_rgx,
                extend_ignore,
                extend_select,
                extend_fixable,
                extend_unfixable,
                external,
                fixable,
                ignore,
                extend_safe_fixes,
                extend_unsafe_fixes,
                ignore_init_module_imports,
                logger_objects,
                select,
                explicit_preview_rules,
                task_tags,
                typing_modules,
                unfixable,
                flake8_annotations,
                flake8_bandit,
                flake8_boolean_trap,
                flake8_bugbear,
                flake8_builtins,
                flake8_comprehensions,
                flake8_copyright,
                flake8_errmsg,
                flake8_quotes,
                flake8_self,
                flake8_tidy_imports,
                flake8_type_checking,
                flake8_gettext,
                flake8_implicit_str_concat,
                flake8_import_conventions,
                flake8_pytest_style,
                flake8_unused_arguments,
                isort,
                mccabe,
                pep8_naming,
                pycodestyle,
                pydocstyle,
                pyflakes,
                pylint,
                pyupgrade,
                per_file_ignores,
                extend_per_file_ignores,
            },
            exclude,
            pydoclint,
            ruff,
            preview,
            typing_extensions,
            future_annotations,
        }
    }
}

#[cfg(test)]
mod tests {
    use crate::options::{Flake8SelfOptions, Flake8TidyImportsOptions};
    use ruff_linter::rules::flake8_self;
    use ruff_linter::rules::flake8_tidy_imports::settings::{
        AllImports, ImportSelection, ImportSelector, ImportSelectorSettings,
    };
    use ruff_python_ast::name::Name;

    #[test]
    fn flake8_self_options() {
        let default_settings = flake8_self::settings::Settings::default();

        // Uses defaults if no options are specified.
        let options = Flake8SelfOptions {
            ignore_names: None,
            extend_ignore_names: None,
        };
        let settings = options.into_settings();
        assert_eq!(settings.ignore_names, default_settings.ignore_names);

        // Uses ignore_names if specified.
        let options = Flake8SelfOptions {
            ignore_names: Some(vec![Name::new_static("_foo")]),
            extend_ignore_names: None,
        };
        let settings = options.into_settings();
        assert_eq!(settings.ignore_names, vec![Name::new_static("_foo")]);

        // Appends extend_ignore_names to defaults if only extend_ignore_names is specified.
        let options = Flake8SelfOptions {
            ignore_names: None,
            extend_ignore_names: Some(vec![Name::new_static("_bar")]),
        };
        let settings = options.into_settings();
        assert_eq!(
            settings.ignore_names,
            default_settings
                .ignore_names
                .into_iter()
                .chain([Name::new_static("_bar")])
                .collect::<Vec<_>>()
        );

        // Appends extend_ignore_names to ignore_names if both are specified.
        let options = Flake8SelfOptions {
            ignore_names: Some(vec![Name::new_static("_foo")]),
            extend_ignore_names: Some(vec![Name::new_static("_bar")]),
        };
        let settings = options.into_settings();
        assert_eq!(
            settings.ignore_names,
            vec![Name::new_static("_foo"), Name::new_static("_bar")]
        );
    }

    #[test]
    fn flake8_tidy_imports_options_allow_disjoint_lazy_import_selectors() {
        let settings = Flake8TidyImportsOptions {
            require_lazy: Some(ImportSelector::Settings(ImportSelectorSettings {
                include: ImportSelection::All(AllImports::All),
                exclude: vec!["sitecustomize".to_string()],
            })),
            ban_lazy: Some(ImportSelector::Selection(ImportSelection::Imports(vec![
                "sitecustomize".to_string(),
            ]))),
            ..Default::default()
        }
        .try_into_settings()
        .unwrap();

        assert!(settings.require_lazy.includes_all());
        assert!(settings.ban_lazy.exclude().is_empty());
    }

    #[test]
    fn flake8_tidy_imports_options_reject_overlapping_lazy_import_selectors() {
        let error = Flake8TidyImportsOptions {
            require_lazy: Some(ImportSelector::Selection(ImportSelection::All(
                AllImports::All,
            ))),
            ban_lazy: Some(ImportSelector::Selection(ImportSelection::Imports(vec![
                "typing".to_string(),
            ]))),
            ..Default::default()
        }
        .try_into_settings()
        .unwrap_err();

        assert_eq!(
            error.to_string(),
            "`require-lazy` and `ban-lazy` must not overlap after applying exclusions"
        );
    }

    #[test]
    fn flake8_tidy_imports_options_reject_all_on_both_sides() {
        let error = Flake8TidyImportsOptions {
            require_lazy: Some(ImportSelector::Selection(ImportSelection::All(
                AllImports::All,
            ))),
            ban_lazy: Some(ImportSelector::Selection(ImportSelection::All(
                AllImports::All,
            ))),
            ..Default::default()
        }
        .try_into_settings()
        .unwrap_err();

        assert_eq!(
            error.to_string(),
            "`require-lazy` and `ban-lazy` must not overlap after applying exclusions"
        );
    }
}
