dissolve-python 0.3.0

A tool to dissolve deprecated calls in Python codebases
Documentation
// Copyright (C) 2024 Jelmer Vernooij <jelmer@samba.org>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Domain-specific types for better type safety and clarity

use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter, Result as FmtResult};
use std::str::FromStr;

/// Represents a Python module name (e.g., "mypackage.submodule")
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ModuleName(String);

impl ModuleName {
    pub fn new(name: impl Into<String>) -> Self {
        Self(name.into())
    }

    pub fn as_str(&self) -> &str {
        &self.0
    }

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

    /// Check if this module is a parent of another module
    pub fn is_parent_of(&self, other: &ModuleName) -> bool {
        other.0.starts_with(&self.0)
            && other.0.len() > self.0.len()
            && other.0.chars().nth(self.0.len()) == Some('.')
    }

    /// Get the parent module name (returns None for root modules)
    pub fn parent(&self) -> Option<ModuleName> {
        self.0
            .rfind('.')
            .map(|pos| ModuleName(self.0[..pos].to_string()))
    }
}

impl Display for ModuleName {
    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
        write!(f, "{}", self.0)
    }
}

impl FromStr for ModuleName {
    type Err = &'static str;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        if s.is_empty() {
            Err("Module name cannot be empty")
        } else {
            Ok(ModuleName(s.to_string()))
        }
    }
}

/// Represents a Python function name (e.g., "calculate_total")
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct FunctionName(String);

impl FunctionName {
    pub fn new(name: impl Into<String>) -> Self {
        Self(name.into())
    }

    pub fn as_str(&self) -> &str {
        &self.0
    }

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

impl Display for FunctionName {
    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
        write!(f, "{}", self.0)
    }
}

impl FromStr for FunctionName {
    type Err = &'static str;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        if s.is_empty() {
            Err("Function name cannot be empty")
        } else {
            Ok(FunctionName(s.to_string()))
        }
    }
}

/// Represents a fully qualified function name (e.g., "mymodule.MyClass.method")
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct QualifiedName {
    pub module: ModuleName,
    pub function: FunctionName,
}

impl QualifiedName {
    pub fn new(module: ModuleName, function: FunctionName) -> Self {
        Self { module, function }
    }

    pub fn from_string(qualified_name: &str) -> Result<Self, &'static str> {
        if let Some(last_dot) = qualified_name.rfind('.') {
            let module_part = &qualified_name[..last_dot];
            let function_part = &qualified_name[last_dot + 1..];

            Ok(QualifiedName {
                module: ModuleName::new(module_part),
                function: FunctionName::new(function_part),
            })
        } else {
            Err("Qualified name must contain at least one dot")
        }
    }

    pub fn to_string(&self) -> String {
        format!("{}.{}", self.module, self.function)
    }
}

impl Display for QualifiedName {
    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
        write!(f, "{}.{}", self.module, self.function)
    }
}

/// Represents a source file path in a type-safe way
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SourcePath(std::path::PathBuf);

impl SourcePath {
    pub fn new(path: impl Into<std::path::PathBuf>) -> Self {
        Self(path.into())
    }

    pub fn as_path(&self) -> &std::path::Path {
        &self.0
    }

    pub fn into_path_buf(self) -> std::path::PathBuf {
        self.0
    }

    pub fn to_string_lossy(&self) -> std::borrow::Cow<str> {
        self.0.to_string_lossy()
    }
}

impl Display for SourcePath {
    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
        write!(f, "{}", self.0.display())
    }
}

// Re-export Version from core::types to avoid duplication
pub use crate::core::types::Version;

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_module_name_parent() {
        let module = ModuleName::new("parent.child.grandchild");
        let parent = module.parent().unwrap();
        assert_eq!(parent.as_str(), "parent.child");

        let grandparent = parent.parent().unwrap();
        assert_eq!(grandparent.as_str(), "parent");

        assert!(grandparent.parent().is_none());
    }

    #[test]
    fn test_module_is_parent_of() {
        let parent = ModuleName::new("parent");
        let child = ModuleName::new("parent.child");
        let unrelated = ModuleName::new("other");

        assert!(parent.is_parent_of(&child));
        assert!(!parent.is_parent_of(&unrelated));
        assert!(!child.is_parent_of(&parent));
    }

    #[test]
    fn test_qualified_name_from_string() {
        let qname = QualifiedName::from_string("module.submodule.function").unwrap();
        assert_eq!(qname.module.as_str(), "module.submodule");
        assert_eq!(qname.function.as_str(), "function");

        assert!(QualifiedName::from_string("nomodule").is_err());
    }
}