Back to Fastled

Lint Module

ci/lint/README.md

3.10.36.5 KB
Original Source

Lint Module

Python implementation of the FastLED comprehensive linting suite.

Overview

This module replaced the 589-line bash ./lint script with a Python implementation following the trampoline pattern (similar to ./testtest.py).

Architecture

Trampoline Pattern

The bash script ./lint is now a minimal 4-line trampoline:

bash
#!/bin/bash
set -e
cd "$(dirname "$0")"
uv run ci/lint.py "$@"

All logic is implemented in Python under ci/lint/.

Module Structure

ci/lint/
├── __init__.py              # Package initialization
├── args_parser.py           # CLI argument parsing
├── duration_tracker.py      # Timing and summary table generation
├── orchestrator.py          # Parallel execution engine
├── output_capture.py        # Thread-safe output buffering
├── stage_impls.py           # Stage implementation (C++, JS, Python)
└── stages.py                # LintStage dataclass definition

Core Classes

LintStage (stages.py):

  • Dataclass representing a single lint stage
  • Contains: name, display_name, run_fn, timeout, dependencies

DurationTracker (duration_tracker.py):

  • Tracks start/end times for each stage
  • Generates formatted summary table matching bash output

LintOrchestrator (orchestrator.py):

  • Manages parallel execution with ThreadPoolExecutor
  • Handles dependency-aware scheduling
  • Captures and replays stage output
  • Handles timeouts and Ctrl-C cleanup

LintArgs (args_parser.py):

  • Parsed command-line arguments
  • Flags: --js, --cpp, --no-fingerprint, --full, --iwyu, --strict

Stage Implementations

Python Pipeline (sequential within parallel):

  1. ruff: Linting and formatting
  2. ty: Fast type checking
  3. pyright: Strict type checking (only with --strict)

C++ Linting:

  1. Fingerprint cache check
  2. clang-format (currently disabled for most folders)
  3. Unified C++ checker (all linters + unity build + cpp_lint)
  4. IWYU analysis (only with --full or --iwyu)

JavaScript Linting:

  1. Auto-setup ESLint if needed
  2. Run fast JS linting (Node.js + ESLint)

Execution Modes

Parallel Mode (2+ stages)

When running multiple stages (e.g., bash lint):

  • All stages start simultaneously in separate threads
  • Python pipeline runs ruff → ty → pyright sequentially
  • C++ and JS run independently
  • Output captured and replayed after completion
  • Summary table shows all durations

Inline Mode (single stage)

When running a single stage (e.g., bash lint --js):

  • Stage runs in main thread
  • Output printed immediately
  • No parallelization overhead

Fingerprint Caching

Stages use fingerprint caching to skip unchanged files:

  • C++ linting: ci.cpp_lint_cache.py
  • Python linting (pyright): ci.python_lint_cache.py
  • JavaScript linting: ci.js_lint_cache.py

Cache behavior:

  • check_*_files_changed() → True if linting needed
  • mark_*_lint_success() → Mark cache success
  • invalidate_*_cache() → Invalidate on failure
  • --no-fingerprint bypasses all caches

Error Handling

KeyboardInterrupt

All exception handlers follow the KBI linter pattern:

python
except KeyboardInterrupt:
    _thread.interrupt_main()
    raise

Timeout Handling

Each stage has a timeout (default: 300s):

  • ThreadPoolExecutor enforces timeouts via future.result(timeout=...)
  • On timeout: stage marked as failed, execution continues

Cleanup Guarantees

Temp directories cleaned up via try/finally:

python
try:
    self.tmpdir = Path(tempfile.mkdtemp(prefix="fastled_lint_"))
    # ... run stages ...
finally:
    if self.tmpdir and self.tmpdir.exists():
        shutil.rmtree(self.tmpdir, ignore_errors=True)

Platform Compatibility

Windows Shell Handling

JavaScript linting uses shell=True on Windows to properly invoke bash scripts:

python
if sys.platform in ("win32", "cygwin", "msys"):
    result = subprocess.run("bash ci/lint-js-fast", shell=True, ...)
else:
    result = subprocess.run(["bash", "ci/lint-js-fast"], ...)

Cross-Platform Paths

All paths use pathlib.Path for cross-platform compatibility.

Testing

Test infrastructure prepared at tests/ci/lint/:

  • conftest.py - Shared fixtures
  • helpers/ - Mock cache, filesystem, output parser
  • Future: Dual-execution tests (bash vs Python parity)
  • Future: Flag combination tests
  • Future: Parallel safety tests

Migration from Bash

What Changed

Before (bash script):

  • 589 lines of complex bash
  • Manual background job management (&, wait $!)
  • Temp file parsing for metadata
  • Complex parallel coordination

After (Python implementation):

  • ~850 lines of well-structured Python across 7 modules
  • ThreadPoolExecutor for parallel execution
  • Proper exception handling
  • Type-safe with dataclasses

Preserved Behavior

All functionality preserved:

  • ✅ Exact CLI flags and help text
  • ✅ Parallel execution (3 stages: Python, C++, JS)
  • ✅ Fingerprint caching
  • ✅ Duration tracking and summary table
  • ✅ Exit codes (0=success, 1=failure, 130=interrupt)
  • ✅ Output format (headers, emojis, AI hints)

Performance

Comparable performance to bash:

  • Parallel mode: ~2s for all stages (with cache hits)
  • Python overhead: <100ms
  • ThreadPoolExecutor: efficient I/O-bound parallelism

Future Enhancements

Planned improvements (not in scope for initial refactor):

  • Comprehensive test suite (dual-execution parity tests)
  • Dependency-aware scheduling (currently all stages start immediately)
  • Real-time streaming output (currently buffered)
  • Progress bars for long-running stages
  • Configurable timeouts per stage
  • HTML/JSON output formats for CI integration

Maintenance

Adding a New Lint Stage

  1. Add function to stage_impls.py:

    python
    def run_my_linter() -> bool:
        result = subprocess.run([...], capture_output=False)
        return result.returncode == 0
    
  2. Create stage in lint.py:

    python
    stages.append(
        LintStage(
            name="my_linter",
            display_name="MY LINTER",
            run_fn=run_my_linter,
            timeout=120.0,
        )
    )
    

Modifying Cache Behavior

Cache modules are in ci/*_lint_cache.py:

  • Modify get_*_files() to change monitored files
  • Adjust cache key in TwoLayerFingerprintCache(..., "cache_name")

References

  • Original bash script: lint.backup (589 lines)
  • Entry point: ci/lint.py
  • Module: ci/lint/
  • Tests: tests/ci/lint/
  • Similar pattern: test.py (test suite trampoline)