Repository: astral-sh/ruff
File: crates/ruff_workspace/src/pyproject.rs (Rust)

Code before:
) -> Result<()> {
    let mut current = value;
    for key in table_path {
        let Some(next) = current.get(*key) else {
            return Ok(());
        };
        let toml::de::DeValue::Table(next) = next.get_ref() else {
            return Ok(());
        };
        current = next;
    }

    let required_version = current
        .get("required-version")
        .or_else(|| current.get("required_version"))
        .and_then(|value| match value.get_ref() {
            toml::de::DeValue::String(value) => Some(value.as_ref()),
            _ => None,
        });

    let Some(required_version) = required_version else {
        return Ok(());
    };

    let required_version = RequiredVersion::try_from(required_version.to_string())
        .with_context(|| format!("Failed to parse {}", path.display()))?;
    validate_required_version(&required_version)?;
    Ok(())
}

/// Derive target version from `required-version` in `pyproject.toml`, if
/// such a file exists in an ancestor directory.
pub fn find_fallback_target_version<P: AsRef<Path>>(path: P) -> Option<PythonVersion> {
    for directory in path.as_ref().ancestors() {
        if let Some(fallback) = get_fallback_target_version(directory) {
            return Some(fallback);
        }
    }
    None
}

/// Find the path to the user-specific `pyproject.toml` or `ruff.toml`, if it
/// exists.
#[cfg(not(target_arch = "wasm32"))]
pub fn find_user_settings_toml() -> Option<PathBuf> {
    use etcetera::BaseStrategy;

    let strategy = etcetera::base_strategy::choose_base_strategy().ok()?;
    let config_dir = strategy.config_dir().join("ruff");

    // Search for a user-specific `.ruff.toml`, then a `ruff.toml`, then a `pyproject.toml`.

Review comment (suggestion):
I suspect that we lose the span in the error message compared to when deserializing the value as part of the serde deserialization. I'm inclined to return `Ok` here and defer to the "full" deserialization in that case.

Code after:
            return Ok(());
        };
        let toml::de::DeValue::Table(next) = next.get_ref() else {
            return Ok(());
        };
        current = next;
    }

    let required_version = current
        .get("required-version")
        .and_then(|value| value.get_ref().as_str());

    let Some(required_version) = required_version else {
        return Ok(());
    };

    // If it doesn't parse, we just fall through to normal parsing; it will give a nicer error message.
    if let Ok(required_version) = required_version.parse::<RequiredVersion>() {
        validate_required_version(&required_version)?;
    }
    Ok(())
}

/// Derive target version from `required-version` in `pyproject.toml`, if
/// such a file exists in an ancestor directory.
pub fn find_fallback_target_version<P: AsRef<Path>>(path: P) -> Option<PythonVersion> {
    for directory in path.as_ref().ancestors() {
        if let Some(fallback) = get_fallback_target_version(directory) {
            return Some(fallback);
        }
    }
    None
}

/// Find the path to the user-specific `pyproject.toml` or `ruff.toml`, if it
/// exists.
#[cfg(not(target_arch = "wasm32"))]
pub fn find_user_settings_toml() -> Option<PathBuf> {
    use etcetera::BaseStrategy;

    let strategy = etcetera::base_strategy::choose_base_strategy().ok()?;
    let config_dir = strategy.config_dir().join("ruff");

    // Search for a user-specific `.ruff.toml`, then a `ruff.toml`, then a `pyproject.toml`.
    for filename in [".ruff.toml", "ruff.toml", "pyproject.toml"] {
        let path = config_dir.join(filename);
        if path.is_file() {
            return Some(path);
        }
    }