#!/usr/bin/env python3 """ Check and fix naming conventions across the codebase. This script analyzes Python files for naming convention compliance: - Files: snake_case - Classes: PascalCase - Functions/methods: snake_case - Constants: UPPER_CASE - Variables: snake_case """ import ast import os import re from pathlib import Path from typing import Dict, List, Tuple, Set from dataclasses import dataclass @dataclass class NamingIssue: """Represents a naming convention issue""" file_path: str line_number: int issue_type: str current_name: str suggested_name: str description: str class NamingConventionChecker: """Checks and reports naming convention issues""" def __init__(self): self.issues: List[NamingIssue] = [] def is_snake_case(self, name: str) -> bool: """Check if name follows snake_case convention""" # Allow leading underscores for private functions/variables return re.match(r'^_*[a-z][a-z0-9_]*$', name) is not None def is_pascal_case(self, name: str) -> bool: """Check if name follows PascalCase convention""" return re.match(r'^[A-Z][a-zA-Z0-9]*$', name) is not None def is_upper_case(self, name: str) -> bool: """Check if name follows UPPER_CASE convention""" return re.match(r'^[A-Z][A-Z0-9_]*$', name) is not None def to_snake_case(self, name: str) -> str: """Convert name to snake_case""" # Handle PascalCase to snake_case s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() def to_pascal_case(self, name: str) -> str: """Convert name to PascalCase""" return ''.join(word.capitalize() for word in name.split('_')) def to_upper_case(self, name: str) -> str: """Convert name to UPPER_CASE""" return name.upper() def analyze_file(self, file_path: str) -> List[NamingIssue]: """Analyze a Python file for naming convention issues""" issues = [] try: with open(file_path, 'r', encoding='utf-8') as f: content = f.read() tree = ast.parse(content) except (SyntaxError, UnicodeDecodeError) as e: return issues for node in ast.walk(tree): # Check class names (should be PascalCase) if isinstance(node, ast.ClassDef): if not self.is_pascal_case(node.name): issues.append(NamingIssue( file_path=file_path, line_number=node.lineno, issue_type="class_name", current_name=node.name, suggested_name=self.to_pascal_case(node.name), description=f"Class '{node.name}' should use PascalCase" )) # Check function/method names (should be snake_case) elif isinstance(node, ast.FunctionDef) or isinstance(node, ast.AsyncFunctionDef): # Skip special methods like __init__, __str__, etc. if not (node.name.startswith('__') and node.name.endswith('__')): if not self.is_snake_case(node.name): issues.append(NamingIssue( file_path=file_path, line_number=node.lineno, issue_type="function_name", current_name=node.name, suggested_name=self.to_snake_case(node.name), description=f"Function '{node.name}' should use snake_case" )) # Check variable assignments for constants (UPPER_CASE) elif isinstance(node, ast.Assign): for target in node.targets: if isinstance(target, ast.Name): name = target.id # Check if it looks like a constant (all caps or starts with caps) if (name.isupper() or (len(name) > 1 and name[0].isupper())) and '_' in name: if not self.is_upper_case(name): issues.append(NamingIssue( file_path=file_path, line_number=node.lineno, issue_type="constant_name", current_name=name, suggested_name=self.to_upper_case(name), description=f"Constant '{name}' should use UPPER_CASE" )) return issues def check_file_names(self, directory: str) -> List[NamingIssue]: """Check if Python file names follow snake_case convention""" issues = [] for root, dirs, files in os.walk(directory): # Skip certain directories dirs[:] = [d for d in dirs if d not in ['__pycache__', '.git', 'venv', 'env', '.pytest_cache', 'backups', 'atlas_env', 'node_modules', '.vscode']] for file in files: if file.endswith('.py') and file != '__init__.py': file_path = os.path.join(root, file) file_name = file[:-3] # Remove .py extension if not self.is_snake_case(file_name): suggested_name = self.to_snake_case(file_name) + '.py' issues.append(NamingIssue( file_path=file_path, line_number=0, issue_type="file_name", current_name=file, suggested_name=suggested_name, description=f"File '{file}' should use snake_case naming" )) return issues def analyze_directory(self, directory: str) -> List[NamingIssue]: """Analyze all Python files in a directory for naming issues""" all_issues = [] # Check file names all_issues.extend(self.check_file_names(directory)) # Check code content for root, dirs, files in os.walk(directory): # Skip certain directories dirs[:] = [d for d in dirs if d not in ['__pycache__', '.git', 'venv', 'env', '.pytest_cache', 'backups', 'atlas_env', 'node_modules', '.vscode']] for file in files: if file.endswith('.py'): file_path = os.path.join(root, file) file_issues = self.analyze_file(file_path) all_issues.extend(file_issues) return all_issues def main(): """Main function to check naming conventions across the codebase""" print("šŸ” Checking naming conventions...") checker = NamingConventionChecker() project_root = Path(__file__).parent.parent.parent # Directories to check directories_to_check = [ str(project_root), # Root level str(project_root / 'analytics'), str(project_root / 'tests'), str(project_root / 'scripts'), str(project_root / 'archive') ] all_issues = [] for directory in directories_to_check: if os.path.exists(directory): print(f"\nšŸ“ Checking directory: {directory}") issues = checker.analyze_directory(directory) all_issues.extend(issues) print(f" Found {len(issues)} naming issues") # Group issues by type issues_by_type = {} for issue in all_issues: if issue.issue_type not in issues_by_type: issues_by_type[issue.issue_type] = [] issues_by_type[issue.issue_type].append(issue) # Report results print(f"\nšŸ“Š Naming Convention Analysis Results:") print(f"Total issues found: {len(all_issues)}") for issue_type, issues in issues_by_type.items(): print(f"\n{issue_type.replace('_', ' ').title()} Issues ({len(issues)}):") for issue in issues[:10]: # Show first 10 of each type rel_path = os.path.relpath(issue.file_path, project_root) if issue.line_number > 0: print(f" šŸ“ {rel_path}:{issue.line_number} - {issue.current_name} → {issue.suggested_name}") else: print(f" šŸ“ {rel_path} - {issue.current_name} → {issue.suggested_name}") if len(issues) > 10: print(f" ... and {len(issues) - 10} more") # Provide recommendations print(f"\nšŸ’” Recommendations:") if issues_by_type.get('file_name'): print(" • Rename files to use snake_case (e.g., MyFile.py → my_file.py)") if issues_by_type.get('class_name'): print(" • Rename classes to use PascalCase (e.g., my_class → MyClass)") if issues_by_type.get('function_name'): print(" • Rename functions to use snake_case (e.g., myFunction → my_function)") if issues_by_type.get('constant_name'): print(" • Rename constants to use UPPER_CASE (e.g., myConstant → MY_CONSTANT)") if not all_issues: print("āœ… All naming conventions are properly followed!") if __name__ == "__main__": main()