Skip to main content

A static analysis tool for detecting architectural code smells in Python

Project description

Vera Syntaxis

A static analysis tool for detecting architectural code smells in Python codebases.

Overview

Vera Syntaxis is a specialized linter that focuses on architectural issues rather than style or type safety:

  • Tight Coupling Detection: Identifies instances of tight coupling between classes
  • MVBC Architecture Linter: Enforces communication rules between Model, View, Business, and Controller layers

Features

  • AST-based Analysis: Uses Python's Abstract Syntax Tree for accurate code analysis
  • Call Graph Construction: Builds a comprehensive call graph of your codebase
  • Configurable Rules: Customize architectural rules via TOML configuration
  • Python API: Use as a library in your own tools
  • GUI Interface: Optional GUI plugin for Extensio Pulchra framework

Requirements

  • Python 3.8+
  • Extensive use of type hints in target codebase for accurate analysis

Installation

pip install vera-syntaxis

For development:

pip install vera-syntaxis[dev]

For GUI support:

pip install vera-syntaxis[gui]

Quick Start

Command Line

# Parse and analyze a project
vera-syntaxis analyze /path/to/project

# Parse only (for debugging)
vera-syntaxis parse /path/to/project

Python API

from vera_syntaxis import run_analysis

violations = run_analysis("/path/to/project")
for violation in violations:
    print(f"{violation.file_path}:{violation.line_number} - {violation.message}")

Configuration

Create a pyproject.toml in your project root:

[tool.vera_syntaxis]
model_paths = ["my_app/models/"]
view_paths = ["my_app/views/"]
business_paths = ["my_app/services/"]
controller_paths = ["my_app/controllers/"]
module_filter = ["my_app.*"]

[tool.vera_syntaxis.coupling]
max_inter_class_calls = 5
max_demeter_chain = 3

[tool.vera_syntaxis.circular]
allow_self_cycles = false
max_cycle_length = 10

[tool.vera_syntaxis.god_object]
max_methods = 20
max_attributes = 15
max_lines = 300

[tool.vera_syntaxis.feature_envy]
envy_threshold = 0.5
min_accesses = 3

[tool.vera_syntaxis.data_clump]
min_clump_size = 3
min_occurrences = 2

Architectural Rules

Tight Coupling Linter

TC001: Direct Instantiation

Flags direct class instantiations that create tight coupling.

Example Violation:

class UserService:
    def create_user(self):
        # Bad: Direct instantiation creates tight coupling
        db = Database()
        return db.save(User())

Fix: Use dependency injection instead.

TC002: Law of Demeter (Method Chaining)

Detects excessive method chaining that violates the Law of Demeter.

Configuration:

  • max_demeter_chain: Maximum allowed chain length (default: 5)

Example Violation:

# Bad: Chain length of 4 exceeds configured maximum of 3
country = user.address.city.postal_code.country

Fix: Break the chain using intermediate variables or add helper methods:

# Good: Use intermediate variables
address = user.address
city = address.city
postal_code = city.postal_code
country = postal_code.country

# Or: Add a helper method
country = user.get_country()

TC003: Excessive Interaction

Identifies classes with too many inter-class calls using call graph analysis.

Configuration:

  • max_inter_class_calls: Maximum allowed unique method calls to other classes (default: 10)

Example Violation:

class Client:
    def __init__(self):
        self.service_a = ServiceA()
        self.service_b = ServiceB()
        self.service_c = ServiceC()
    
    def do_work(self):
        # Bad: Calls 12 unique methods from other classes (exceeds limit of 10)
        self.service_a.method1()
        self.service_a.method2()
        self.service_a.method3()
        self.service_a.method4()
        self.service_b.method1()
        self.service_b.method2()
        self.service_b.method3()
        self.service_b.method4()
        self.service_c.method1()
        self.service_c.method2()
        self.service_c.method3()
        self.service_c.method4()

Fix: Refactor to reduce coupling:

# Good: Introduce a facade or coordinator
class ServiceCoordinator:
    def __init__(self):
        self.service_a = ServiceA()
        self.service_b = ServiceB()
        self.service_c = ServiceC()
    
    def execute_workflow(self):
        # Encapsulates the complex interactions
        self.service_a.do_work()
        self.service_b.do_work()
        self.service_c.do_work()

class Client:
    def __init__(self):
        self.coordinator = ServiceCoordinator()
    
    def do_work(self):
        # Now only 1 inter-class call
        self.coordinator.execute_workflow()

MVBC Linter

W2902: Illegal Layer Dependency

Enforces proper layer communication in Model-View-Business-Controller architecture.

Allowed Dependencies:

  • Model: Can be used by anyone (no dependencies on other layers)
  • View: Can use Model only
  • Business: Can use Model only
  • Controller: Can use View, Business, and Model

Forbidden Dependencies:

  • View → Business (must go through Controller)
  • Business → View (business logic shouldn't know about presentation)
  • View → Controller (creates circular dependency)
  • Business → Controller (creates circular dependency)

Configuration: Supports both single-level and nested directory patterns:

[tool.vera_syntaxis.mvbc]
model_paths = ["models/*.py"]          # Matches models/user.py and models/subdir/user.py
view_paths = ["views/**/*.py"]         # Explicit nested pattern
business_paths = ["business/*.py"]
controller_paths = ["controllers/*.py"]

Example Violations:

View → Business:

# In views/user_view.py
from business.user_logic import UserLogic  # Bad: View importing Business

class UserView:
    def __init__(self):
        self.logic = UserLogic()

Fix: Use a Controller to mediate between View and Business.

Business → View:

# In business/user_logic.py
from views.user_view import UserView  # Bad: Business importing View

class UserLogic:
    def __init__(self):
        self.view = UserView()

Fix: Keep business logic independent of presentation. Controllers should manage Views.

Circular Dependency Linter

CD001: Circular Dependency

Detects circular dependencies between modules.

Configuration:

  • allow_self_cycles: Allow methods to call themselves (default: false)
  • max_cycle_length: Maximum cycle length to report (default: 10)

Example Violation:

# module_a.py
from module_b import ClassB

class ClassA:
    def method(self):
        b = ClassB()

# module_b.py
from module_a import ClassA  # Circular dependency!

class ClassB:
    def method(self):
        a = ClassA()

Fix: Break the cycle using dependency inversion:

# interface.py
from abc import ABC, abstractmethod

class IProcessor(ABC):
    @abstractmethod
    def process(self): pass

# module_a.py
from interface import IProcessor

class ClassA(IProcessor):
    def process(self):
        pass

# module_b.py
from interface import IProcessor

class ClassB:
    def __init__(self, processor: IProcessor):
        self.processor = processor

God Object Linter

GO001: God Object

Detects classes with too many responsibilities (God Objects).

Configuration:

  • max_methods: Maximum number of methods allowed (default: 20)
  • max_attributes: Maximum number of attributes allowed (default: 15)
  • max_lines: Maximum number of lines allowed (default: 300)

Example Violation:

class UserManager:  # God Object!
    def __init__(self):
        self.db = Database()
        self.cache = Cache()
        self.logger = Logger()
        self.validator = Validator()
        self.emailer = Emailer()
        # ... 15+ attributes
    
    def create_user(self): pass
    def update_user(self): pass
    def delete_user(self): pass
    def validate_email(self): pass
    def send_welcome_email(self): pass
    def log_activity(self): pass
    # ... 20+ methods

Fix: Split into focused classes:

# Separate concerns into focused classes
class UserRepository:
    def __init__(self, db):
        self.db = db
    
    def create(self, user): pass
    def update(self, user): pass
    def delete(self, user_id): pass

class UserValidator:
    def validate_email(self, email): pass
    def validate_password(self, password): pass

class UserNotifier:
    def __init__(self, emailer):
        self.emailer = emailer
    
    def send_welcome_email(self, user): pass
    def send_password_reset(self, user): pass

class UserService:
    def __init__(self, repo, validator, notifier):
        self.repo = repo
        self.validator = validator
        self.notifier = notifier
    
    def create_user(self, user_data):
        if self.validator.validate_email(user_data['email']):
            user = self.repo.create(user_data)
            self.notifier.send_welcome_email(user)
            return user

Feature Envy Linter

FE001: Feature Envy

Detects methods that use more features from another class than their own.

Configuration:

  • envy_threshold: Ratio of external to total accesses that triggers envy (default: 0.5)
  • min_accesses: Minimum total accesses before checking (default: 3)

Example Violation:

class Order:
    def __init__(self):
        self.customer = Customer()
    
    def validate_customer_email(self):  # Feature Envy!
        # Uses customer features more than own features
        email = self.customer.email
        email = self.customer.email.strip()
        email = self.customer.email.lower()
        is_valid = '@' in self.customer.email
        return is_valid

Fix: Move the method to the appropriate class:

class Customer:
    def __init__(self):
        self.email = ""
    
    def validate_email(self):
        # Now uses own features
        email = self.email.strip().lower()
        return '@' in email

class Order:
    def __init__(self):
        self.customer = Customer()
    
    def process(self):
        # Delegate to the appropriate class
        if self.customer.validate_email():
            # Process order
            pass

Data Clump Linter

DC001: Data Clump

Detects groups of parameters that frequently appear together.

Configuration:

  • min_clump_size: Minimum number of parameters in a clump (default: 3)
  • min_occurrences: Minimum number of methods with the clump (default: 2)

Example Violation:

class OrderProcessor:
    def calculate_total(self, price, quantity, discount):  # Data Clump!
        return price * quantity * (1 - discount)
    
    def validate_order(self, price, quantity, discount):  # Same parameters
        return price > 0 and quantity > 0
    
    def format_order(self, price, quantity, discount):  # Same parameters
        return f"{quantity} items at ${price}"

Fix: Extract data clump into a class:

class OrderDetails:
    def __init__(self, price: float, quantity: int, discount: float):
        self.price = price
        self.quantity = quantity
        self.discount = discount
    
    def calculate_total(self) -> float:
        return self.price * self.quantity * (1 - self.discount)
    
    def validate(self) -> bool:
        return self.price > 0 and self.quantity > 0
    
    def format(self) -> str:
        return f"{self.quantity} items at ${self.price}"

class OrderProcessor:
    def process_order(self, details: OrderDetails):
        if details.validate():
            total = details.calculate_total()
            print(details.format())
            return total

Limitations

  • Requires extensive type hints for accurate analysis
  • Dynamic attributes (via __getattr__) may not be detected
  • Forward references must be properly annotated

Development Status

Version 0.1.0 - Alpha (Reporting Only)

License

MIT License

Contributing

Contributions welcome! Please see CONTRIBUTING.md for guidelines.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

vera_syntaxis-0.1.0.tar.gz (78.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

vera_syntaxis-0.1.0-py3-none-any.whl (74.7 kB view details)

Uploaded Python 3

File details

Details for the file vera_syntaxis-0.1.0.tar.gz.

File metadata

  • Download URL: vera_syntaxis-0.1.0.tar.gz
  • Upload date:
  • Size: 78.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.4

File hashes

Hashes for vera_syntaxis-0.1.0.tar.gz
Algorithm Hash digest
SHA256 aa2b25b8fbd67ccad8dfdc943f4e4efea6b8e3c41ca1601763128fc73e5aad8e
MD5 2a0681a6d75d8bd75103aaa4edf7dd02
BLAKE2b-256 6d8958406268bb24e4b8c7befd1e2451f3263449c9a7cff5e75a734173baf4fb

See more details on using hashes here.

File details

Details for the file vera_syntaxis-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: vera_syntaxis-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 74.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.4

File hashes

Hashes for vera_syntaxis-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f4b656399e50f18e68cea43b487fd6971ff9a6f8fd94860f1298775454817e19
MD5 3c8c67255b04b24f3850fd32466896d6
BLAKE2b-256 233d549f7989e8d807a9cf5abbbbff7cba2e76efdefb22899e1c16f966a2755a

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page