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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
aa2b25b8fbd67ccad8dfdc943f4e4efea6b8e3c41ca1601763128fc73e5aad8e
|
|
| MD5 |
2a0681a6d75d8bd75103aaa4edf7dd02
|
|
| BLAKE2b-256 |
6d8958406268bb24e4b8c7befd1e2451f3263449c9a7cff5e75a734173baf4fb
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f4b656399e50f18e68cea43b487fd6971ff9a6f8fd94860f1298775454817e19
|
|
| MD5 |
3c8c67255b04b24f3850fd32466896d6
|
|
| BLAKE2b-256 |
233d549f7989e8d807a9cf5abbbbff7cba2e76efdefb22899e1c16f966a2755a
|