A tool for refurbishing and modernizing Python codebases
—
Complete framework for developing custom refurb checks with AST analysis utilities, type checking helpers, and visitor patterns.
The visitor system that enables custom checks to be seamlessly integrated into refurb's analysis pipeline.
class RefurbVisitor:
"""
Main visitor class that traverses AST nodes and runs applicable checks.
The visitor implements the standard visitor pattern for Mypy AST nodes,
dispatching to registered checks based on node type.
Attributes:
- checks: defaultdict[type[Node], list[Check]] - Checks organized by AST node type
- settings: Settings - Configuration controlling analysis behavior
- errors: list[Error] - Accumulated errors found during traversal
"""
def __init__(self, checks: defaultdict[type[Node], list[Check]], settings: Settings):
"""
Initialize visitor with checks and settings.
Parameters:
- checks: Dictionary mapping AST node types to lists of applicable checks
- settings: Configuration object controlling analysis behavior
"""
def run_check(self, node: Node, check: Check) -> None:
"""
Execute a single check on an AST node.
Handles both 2-parameter and 3-parameter check function signatures,
manages error collection, and provides error context.
Parameters:
- node: AST node to analyze
- check: Check function to execute
"""
def visit_call_expr(self, o: CallExpr) -> None:
"""
Visit function call expressions and run applicable checks.
Example of node-specific visitor method. Similar methods exist for
all supported AST node types.
Parameters:
- o: CallExpr AST node to analyze
"""
class TraverserVisitor:
"""
Base AST traversal visitor providing infrastructure for walking AST trees.
Provides the foundation for RefurbVisitor with automatic traversal
and node dispatch capabilities.
"""Functions that discover, load, and organize checks from built-in modules and plugins.
def load_checks(settings: Settings) -> defaultdict[type[Node], list[Check]]:
"""
Load and filter checks based on settings configuration.
Discovers checks from:
1. Built-in check modules in refurb.checks.*
2. Plugin entry points (refurb.plugins group)
3. Additional modules specified via --load
Applies enable/disable filtering based on settings.
Parameters:
- settings: Configuration specifying which checks to load and filter rules
Returns:
Dictionary mapping AST node types to lists of applicable check functions
"""
def get_modules(paths: list[str]) -> Generator[ModuleType, None, None]:
"""
Load Python modules from specified paths.
Searches for check modules in:
- Built-in refurb.checks package hierarchy
- Plugin entry points
- Custom paths specified via --load
Parameters:
- paths: List of module paths or directory paths to search
Yields:
Python module objects containing check definitions
"""
def get_error_class(module: ModuleType) -> type[Error] | None:
"""
Extract Error class from a check module.
Looks for classes inheriting from Error that define check metadata
like error codes, categories, and default enabled status.
Parameters:
- module: Python module to examine
Returns:
Error class if found, None if module doesn't define checks
"""
def should_load_check(settings: Settings, error: type[Error]) -> bool:
"""
Determine if a check should be enabled based on settings.
Applies filtering logic considering:
- Default enabled status of the check
- Explicit enable/disable settings
- enable_all/disable_all global flags
- Category-based filtering rules
Parameters:
- settings: Configuration containing enable/disable rules
- error: Error class representing the check
Returns:
True if check should be loaded and enabled, False otherwise
"""Refurb discovers plugins through Python entry points:
# setup.py or pyproject.toml entry point definition
[project.entry-points."refurb.plugins"]
my_plugin = "my_plugin.checks"
# Plugin module structure
# my_plugin/checks.py
from refurb.error import Error
class MyCustomError(Error):
enabled = True
name = "Use better pattern"
prefix = "PLUG" # Custom prefix
categories = {"readability"}
code = 1
def check(node: CallExpr, errors: list[Error]) -> None:
"""Check function that will be automatically discovered."""
if should_flag_pattern(node):
errors.append(MyCustomError(
line=node.line,
column=node.column,
msg="Consider using better_function() instead"
))Refurb supports multiple check function signatures for flexibility:
# Type definitions for check functions
Check = Callable[[Node, list[Error]], None] | Callable[[Node, list[Error], Settings], None]
# Two-parameter signature (most common)
def check_pattern(node: Node, errors: list[Error]) -> None:
"""Simple check function."""
# Three-parameter signature (access to settings)
def check_with_settings(node: Node, errors: list[Error], settings: Settings) -> None:
"""Check function with access to configuration."""
if settings.verbose:
# More detailed analysis in verbose mode
passComprehensive mapping of visitor methods to AST node types for precise targeting of checks.
METHOD_NODE_MAPPINGS: dict[str, type[Node]]
"""
Dictionary mapping visitor method names to AST node types.
Contains 89 different AST node type mappings, enabling checks to target
specific language constructs:
- visit_call_expr -> CallExpr (function calls)
- visit_name_expr -> NameExpr (variable references)
- visit_member_expr -> MemberExpr (attribute access)
- visit_op_expr -> OpExpr (binary operations)
- visit_comparison_expr -> ComparisonExpr (comparisons)
- visit_if_stmt -> IfStmt (if statements)
- visit_for_stmt -> ForStmt (for loops)
- visit_while_stmt -> WhileStmt (while loops)
- visit_try_stmt -> TryStmt (try/except blocks)
- visit_with_stmt -> WithStmt (with statements)
- ... and many more
"""# Example: Custom check for deprecated function usage
from mypy.nodes import CallExpr, MemberExpr, NameExpr
from refurb.error import Error
class DeprecatedFunctionError(Error):
"""Error for deprecated function usage."""
enabled = True
name = "Avoid deprecated function"
prefix = "PLUG"
categories = {"modernization"}
code = 100
def check_deprecated_calls(node: CallExpr, errors: list[Error]) -> None:
"""Check for calls to deprecated functions."""
if isinstance(node.callee, NameExpr):
if node.callee.name in {"deprecated_func", "old_api"}:
errors.append(DeprecatedFunctionError(
line=node.line,
column=node.column,
msg=f"Replace `{node.callee.name}()` with modern alternative"
))
# Example: Check with settings access
def check_with_config(node: Node, errors: list[Error], settings: Settings) -> None:
"""Check that can access configuration settings."""
if settings.python_version and settings.python_version >= (3, 10):
# Only apply this check for Python 3.10+
check_modern_syntax(node, errors)
# Example: Multi-node type check
def check_multiple_patterns(node: Node, errors: list[Error]) -> None:
"""Check that handles multiple AST node types."""
if isinstance(node, (CallExpr, MemberExpr)):
# Handle both function calls and attribute access
analyze_pattern(node, errors)Refurb provides utilities for generating boilerplate code for new checks, accessed via the gen subcommand.
def main() -> None:
"""
Interactive generator for creating new check boilerplate.
This function:
1. Prompts user to select AST node types to handle
2. Prompts for target file path and error prefix
3. Generates complete check file with proper imports and structure
4. Creates necessary __init__.py files in parent directories
5. Automatically assigns next available error ID
Accessed via: refurb gen
"""
def get_next_error_id(prefix: str) -> int:
"""
Find the next available error ID for a given prefix.
Parameters:
- prefix: Error code prefix (e.g., "FURB", "PLUG")
Returns:
Next unused error ID number for the prefix
"""
def build_imports(names: list[str]) -> str:
"""
Generate import statements for selected AST node types.
Parameters:
- names: List of AST node type names (e.g., ["CallExpr", "NameExpr"])
Returns:
Formatted import statements organized by module
"""# Test infrastructure for plugin development
from refurb.main import run_refurb
from refurb.settings import Settings
def test_my_check():
"""Test custom check behavior."""
settings = Settings(
files=["test_file.py"],
load=["my_plugin"] # Load custom plugin
)
errors = run_refurb(settings)
# Verify expected errors are found
assert any(error.prefix == "PLUG" and error.code == 100 for error in errors)# Plugin with custom categories
class AdvancedError(Error):
enabled = False # Disabled by default
name = "Advanced pattern check"
prefix = "ADV"
categories = {"performance", "security"} # Multiple categories
code = 1
# Plugin with path-specific behavior
def check_with_path_logic(node: Node, errors: list[Error], settings: Settings) -> None:
"""Check that behaves differently based on file path."""
if node.get_line() and "test" in (node.get_line().source_file or ""):
# Different behavior in test files
return
# Normal checking logic
standard_analysis(node, errors)
# Plugin integration with amendment rules
# In pyproject.toml:
# [[tool.refurb.amend]]
# path = "legacy/"
# ignore = ["ADV001"] # Ignore advanced checks in legacy codeRefurb supports checks for all major Python language constructs:
This comprehensive coverage enables plugins to analyze virtually any Python code pattern.
Install with Tessl CLI
npx tessl i tessl/pypi-refurb