Skip to the content.

The Pre-Flight Linter: Catching Design Smells Early

Ever wonder why your CI pipeline passed, but your service failed in production due to a circular import or a prohibited database access pattern? You likely have a runtime-first architecture.

The “midnight deployment spike” often occurs not because of load, but because a developer bypassed a module boundary that wasn’t strictly enforced. By the time the code reaches the test suite, the architectural violation is already deeply woven into your dependency graph. A Pre-Flight Linter enforces design constraints at the static analysis stage—before code is even compiled or interpreted.

The Strategy: Shift Left on Architecture

Static Analysis is the practice of inspecting code without running it. While standard linters (like Flake8 or ESLint) check for style, a Pre-Flight Linter checks for Intent.

Glossary for Beginners

Simple Implementation: The Import Checker

This script parses a Python file to find prohibited imports, preventing developers from accessing the internal database layer.

import ast

# Define forbidden modules
FORBIDDEN = {'internal_db_driver'}

def check_imports(file_path):
    with open(file_path, "r") as f:
        tree = ast.parse(f.read())
    
    for node in ast.walk(tree):
        if isinstance(node, ast.Import):
            for name in node.names:
                if name.name in FORBIDDEN:
                    return f"SECURITY VIOLATION: {name.name} is forbidden."
    return "Check Passed"

Complex Implementation: Production-Grade Enforcement

For larger systems, we need to traverse directories and ignore specific non-critical paths.

import ast
import os

class ArchitectureLinter:
    def __init__(self, rules):
        self.rules = rules # Mapping of file path patterns to forbidden modules

    def lint_project(self, root_dir):
        for root, _, files in os.walk(root_dir):
            for file in files:
                if file.endswith(".py"):
                    self._lint_file(os.path.join(root, file))

    def _lint_file(self, path):
        with open(path, "r") as f:
            tree = ast.parse(f.read())
            # Logic to verify node relationships against self.rules
            # Includes logging and exit-code management for CI/CD
            pass

# Usage: Ensure 'domain_layer' never imports from 'infrastructure'
rules = {'domain_layer': ['infrastructure']}
linter = ArchitectureLinter(rules)

Quick Reference: Linter vs. Test Suite

Feature Static Analysis (Linter) Unit Testing
Stage Pre-Commit / Pre-Build Post-Build
Focus Structural Integrity / Standards Functional Correctness
Execution Reads code as text/AST Executes code
Speed Near instantaneous Can take minutes/hours

Why We Choose Static Analysis over Runtime Checks

Runtime checks (like if statements inside your functions) add overhead to every request and are often forgotten. Static Analysis is immutable—if the code is written in a way that violates the architecture, it simply won’t build. It treats your design constraints as code.

Developer Checklist

Takeaways