Skip to content

Lazy Ignores Linter

AI Agent Context (click to expand)

Purpose: Complete guide to using the lazy-ignores linter for detecting and fixing unjustified linting suppressions

Scope: Configuration, usage, suppression declaration, and best practices for AI-generated code governance

Overview: Comprehensive documentation for the lazy-ignores linter that detects when AI agents add linting suppressions (# noqa, # type: ignore, # pylint: disable, @ts-ignore, etc.) without proper justification. Uses a header-based declaration model where all suppressions must be documented in the file header's Suppressions: section with human-approved justifications. This enforces that AI assistants cannot silently bypass quality gates. The linter also detects orphaned entries (declared but not used) and test skips without reasons.

Dependencies: tree-sitter (TypeScript parser), Python AST module, file header linter infrastructure

Exports: Usage documentation, configuration examples, suppression declaration patterns

Related: cli-reference.md for CLI commands, configuration.md for config format, FILE_HEADER_STANDARDS.md for Suppressions section format

Implementation: Header-based suppression declaration model with AST-based detection and orphaned entry validation

This follows the AI-Optimized Documentation Standard.


Why This Documentation Format?

This documentation follows the AI-Optimized Documentation Standard.

The structured header at the top of this page is intentional - it's designed to help AI coding assistants (Cursor, Copilot, Claude) understand and navigate the codebase more effectively. Research shows:

  • 2-3x more files scanned per context window with structured vs. prose headers
  • First 5 lines contain 80% of what AI needs for initial file assessment
  • Cleaner vector embeddings for semantic search (RAG systems)

See AI-Optimized Documentation Standard for the full technical justification with citations from Cursor, GitHub Copilot, Anthropic, and IBM Research.


Try It Now

pip install thailint
thailint lazy-ignores src/

Example output:

src/utils.py:45:20 - Unjustified suppression: # noqa: PLR0912
  To fix, add entry to file header Suppressions section:

      Suppressions:
          - PLR0912: [Your justification here]

  IMPORTANT: Suppression requires human approval. Do not add
  without explicit permission from a human reviewer.

Fix it: Add a Suppressions: section to the file header with a justification for each ignore.


Overview

The lazy-ignores linter enforces that all linting suppressions in code are justified and documented. It catches a common AI assistant anti-pattern: adding # noqa, # type: ignore, or # pylint: disable comments to make linting errors disappear without addressing the underlying issue.

The Problem

AI coding assistants frequently add suppression comments to bypass linting errors:

# BAD - AI added these to silence linters without explanation
result = complex_function()  # noqa: PLR0912
data = api_call()  # type: ignore[arg-type]
subprocess.run(cmd, shell=True)  # nosec B602

This behavior: - Hides real issues: The suppression may mask a genuine bug or security vulnerability - Bypasses quality gates: Linting rules exist for good reasons - Creates technical debt: No one knows why the ignore was added - Reduces code quality: AI assistants take shortcuts without human oversight

The Solution

The lazy-ignores linter requires all suppressions to be: 1. Documented in the file header's Suppressions: section 2. Justified with an explanation of why the ignore is legitimate 3. Human-approved before being added

"""
Purpose: Complex data processing utilities

Suppressions:
    - PLR0912: State machine implementation requires complex branching (reviewed 2024-01)
    - arg-type: Pydantic model coercion handles type conversion
    - B602: Subprocess call uses trusted, sanitized input from config
"""

result = complex_function()  # noqa: PLR0912
data = api_call()  # type: ignore[arg-type]
subprocess.run(cmd, shell=True)  # nosec B602

Benefits

  • AI Governance: AI assistants cannot silently bypass quality gates
  • Documentation: Every suppression has a recorded justification
  • Auditability: Easy to review and reconsider suppressions later
  • Code Quality: Encourages fixing issues rather than ignoring them
  • Human Oversight: Ensures human approval for all exceptions

How It Works

Detection Process

  1. Scan source files for ignore directive patterns:
  2. Python: # noqa, # type: ignore, # pylint: disable, # nosec, # pyright: ignore
  3. TypeScript/JavaScript: @ts-ignore, @ts-nocheck, @ts-expect-error, eslint-disable
  4. thai-lint: # thailint: ignore
  5. Test skips: @pytest.mark.skip without reason, it.skip(), describe.skip()

  6. Parse file headers to find Suppressions: section entries

  7. Match ignores to declarations by normalizing rule IDs:

  8. PLR0912 matches plr0912 (case-insensitive)
  9. type:ignore[arg-type] matches arg-type
  10. nosec B602 matches B602

  11. Report violations for:

  12. Unjustified ignores: Suppression in code without matching header entry
  13. Orphaned entries: Header entry without matching suppression in code

Supported Patterns

Python Patterns

Pattern Example Rule ID Extracted
# noqa # noqa: PLR0912, PLR0915 PLR0912, PLR0915
# type: ignore # type: ignore[arg-type] arg-type
# pylint: disable # pylint: disable=no-member no-member
# nosec # nosec B602 B602
# pyright: ignore # pyright: ignore[reportPrivateImportUsage] reportPrivateImportUsage
# thailint: ignore # thailint: ignore[nesting] nesting

TypeScript/JavaScript Patterns

Pattern Example Rule ID Extracted
@ts-ignore // @ts-ignore (no rule ID)
@ts-nocheck // @ts-nocheck (no rule ID)
@ts-expect-error // @ts-expect-error (no rule ID)
eslint-disable // eslint-disable-next-line no-console no-console
eslint-disable (block) /* eslint-disable react/prop-types */ react/prop-types

Test Skip Patterns

Pattern Example Violation Trigger
@pytest.mark.skip @pytest.mark.skip Skip without reason
@pytest.mark.skip() @pytest.mark.skip() Skip without reason
pytest.skip() pytest.skip() Skip without reason
it.skip() it.skip('test name', ...) Always (use it.todo() instead)
describe.skip() describe.skip('suite', ...) Always
test.skip() test.skip('test', ...) Always

Allowed Test Skip Patterns:

@pytest.mark.skip(reason="Flaky on CI, investigating #123")  # OK - has reason
@pytest.mark.skipif(sys.platform == 'win32', reason="Windows-specific")  # OK
pytest.skip("Database not available")  # OK - has reason string

Configuration

Basic Configuration

Add to .thailint.yaml:

lazy-ignores:
  enabled: true

Configuration Options

Option Type Default Description
enabled boolean true Enable/disable lazy-ignores linter
check_noqa boolean true Detect # noqa patterns
check_type_ignore boolean true Detect # type: ignore patterns
check_pylint_disable boolean true Detect # pylint: disable patterns
check_nosec boolean true Detect # nosec patterns
check_pyright_ignore boolean true Detect # pyright: ignore patterns
check_ts_ignore boolean true Detect @ts-ignore, @ts-nocheck, @ts-expect-error
check_eslint_disable boolean true Detect eslint-disable patterns
check_thailint_ignore boolean true Detect # thailint: ignore patterns
check_test_skips boolean true Detect test skips without reasons
check_orphaned boolean true Detect header entries without matching ignores
ignore_patterns list[str] ["tests/**"] File patterns to exclude

Full Configuration Example

lazy-ignores:
  enabled: true

  # Pattern detection toggles
  check_noqa: true
  check_type_ignore: true
  check_pylint_disable: true
  check_nosec: true
  check_pyright_ignore: true
  check_ts_ignore: true
  check_eslint_disable: true
  check_thailint_ignore: true
  check_test_skips: true

  # Orphaned detection
  check_orphaned: true

  # Files/directories to ignore
  ignore_patterns:
    - "tests/**"
    - "**/migrations/**"
    - "**/generated/**"

Strict (For New Projects)

lazy-ignores:
  enabled: true
  check_orphaned: true
  ignore_patterns: []  # Enforce everywhere, including tests

Standard (For Most Projects)

lazy-ignores:
  enabled: true
  check_orphaned: true
  ignore_patterns:
    - "tests/**"  # Don't enforce in test files

Lenient (For Legacy Code)

lazy-ignores:
  enabled: true
  check_orphaned: false  # Don't flag stale entries
  check_test_skips: false  # Allow lazy test skips
  ignore_patterns:
    - "tests/**"
    - "legacy/**"

JSON Configuration

{
  "lazy-ignores": {
    "enabled": true,
    "check_noqa": true,
    "check_type_ignore": true,
    "check_pylint_disable": true,
    "check_nosec": true,
    "check_pyright_ignore": true,
    "check_ts_ignore": true,
    "check_eslint_disable": true,
    "check_test_skips": true,
    "check_orphaned": true,
    "ignore_patterns": ["tests/**"]
  }
}

Usage

CLI Mode

Basic Usage

# Check current directory
thailint lazy-ignores

# Check specific directory
thailint lazy-ignores src/

# Check specific file
thailint lazy-ignores src/main.py

With Options

# Disable orphaned detection
thailint lazy-ignores --no-check-orphaned src/

# Skip test skip detection
thailint lazy-ignores --no-check-test-skips src/

# Check a specific minimum number of unjustified ignores
thailint lazy-ignores src/

With Config File

# Use config file
thailint lazy-ignores --config .thailint.yaml src/

# Auto-discover config (.thailint.yaml or .thailint.json)
thailint lazy-ignores src/

Output Formats

# Human-readable text (default)
thailint lazy-ignores src/

# JSON output for CI/CD
thailint lazy-ignores --format json src/

# SARIF for GitHub Code Scanning
thailint lazy-ignores --format sarif src/ > results.sarif

Docker Mode

# Run with default config
docker run --rm -v $(pwd):/workspace \
  washad/thailint lazy-ignores /workspace/src/

# With config file
docker run --rm \
  -v $(pwd):/workspace \
  -v $(pwd)/.thailint.yaml:/config/.thailint.yaml:ro \
  washad/thailint lazy-ignores --config /config/.thailint.yaml /workspace/src/

Suppression Declaration Format

Python Files

Add a Suppressions: section to your file header docstring:

"""
Purpose: Complex data processing utilities

Scope: Data transformation and validation

Overview: Provides utilities for processing complex data structures
    with type coercion and validation.

Suppressions:
    - PLR0912: State machine implementation requires complex branching
    - arg-type: Pydantic model coercion handles type conversion
    - B602: Subprocess call uses trusted, sanitized input from config
"""

def complex_state_machine():  # noqa: PLR0912
    # ... complex logic ...
    pass

def call_api(data):
    result = api.process(data)  # type: ignore[arg-type]
    return result

def run_trusted_command():
    subprocess.run(cmd, shell=True)  # nosec B602

TypeScript/JavaScript Files

Add a Suppressions: section to your JSDoc header:

/**
 * Purpose: Legacy API integration module
 *
 * Scope: Third-party API communication
 *
 * Overview: Handles communication with legacy API that uses
 *   dynamic typing and untyped responses.
 *
 * Suppressions:
 *   - ts-ignore: Legacy API returns untyped data, types added at runtime
 *   - no-console: Debug logging required for API tracing
 */

// @ts-ignore - Legacy API response
const data = await legacyApi.fetch();

console.log('API trace:', data);  // eslint-disable-line no-console

Format Rules

  1. Each suppression starts its own entry with - rule-id: justification
  2. Rule IDs should match what appears in the code:
  3. Use PLR0912 not plr0912 (convention is uppercase)
  4. Use arg-type not type:ignore[arg-type] (just the error code)
  5. Use B602 not nosec B602 (just the rule ID)
  6. Justifications explain WHY, not WHAT:
  7. Good: "State machine implementation requires complex branching"
  8. Bad: "Disables too-many-branches check"
  9. The rule ID ends at the first colon followed by whitespace, so justification prose may contain colons of its own
  10. Justification text may start with any character - backticks, quotes, digits, and parentheses are all fine
  11. Long justifications may wrap onto indented continuation lines:
"""
Suppressions:
    - S607: `git` is spelled without an absolute path; the argv is otherwise
      literal and the call only reads the tracked-file list.
"""

Inline Justifications

A suppression may be justified on its own line instead of in the header. Write a spaced dash after the directive, then the reason. An ASCII hyphen (-), em dash (), and en dash () all work, and the reason must be at least 10 characters:

subprocess.run(["git", "ls-files"])  # noqa: S607 - `git` is off PATH, argv is literal
result = api.process(data)  # type: ignore[arg-type] — upstream stubs are wrong

An inline justification takes precedence over the header, so a directive justified this way needs no Suppressions: entry.

Example Justifications

Rule ID Good Justification Bad Justification
PLR0912 "State machine requires complex branching for all states" "Too many branches"
arg-type "Pydantic model handles coercion at validation time" "Type mismatch"
B602 "Command from trusted config, user input sanitized" "Shell is needed"
no-member "Dynamic attribute added by metaclass decorator" "Pylint doesn't understand"
ts-ignore "Legacy API returns untyped data, runtime validation applied" "TypeScript error"

Violation Examples

Example 1: Unjustified Python Suppression

Code with violation:

"""
Purpose: User validation utilities
"""

def validate_user(user):
    result = check_complex_rules(user)  # noqa: PLR0912
    return result

Violation message:

src/validators.py:7:40 - Unjustified suppression: # noqa: PLR0912

  To fix, add entry to file header Suppressions section:

      Suppressions:
          - PLR0912: [Your justification here]

  IMPORTANT: Suppression requires human approval. Do not add
  without explicit permission from a human reviewer.

Fixed code:

"""
Purpose: User validation utilities

Suppressions:
    - PLR0912: Multi-factor validation requires checking all auth methods
"""

def validate_user(user):
    result = check_complex_rules(user)  # noqa: PLR0912
    return result

Example 2: Orphaned Header Entry

Code with violation:

"""
Purpose: Simple utility functions

Suppressions:
    - PLR0912: Complex branching required
"""

def simple_add(a, b):
    return a + b  # No PLR0912 ignore in code!

Violation message:

src/utils.py:5:4 - Orphaned suppression entry: PLR0912

  The header declares a suppression for 'PLR0912' but no matching
  ignore directive exists in the code.

  Either:
  1. Remove the orphaned entry from the Suppressions section
  2. Verify the rule ID matches exactly (case-sensitive)

Example 3: TypeScript Violation

Code with violation:

/**
 * Purpose: API client module
 */

// @ts-ignore
const response = await fetchData();

Violation message:

src/api.ts:6:0 - Unjustified suppression: // @ts-ignore

  To fix, add entry to file header Suppressions section:

      Suppressions:
        - ts-ignore: [Your justification here]

  IMPORTANT: Suppression requires human approval.

Example 4: Test Skip Without Reason

Code with violation:

import pytest

@pytest.mark.skip
def test_flaky_integration():
    # Test that sometimes fails
    pass

Violation message:

tests/test_integration.py:3:0 - Test skip without reason: @pytest.mark.skip

  Test skips should include a reason explaining why the test is skipped.

  Fix by adding a reason:
      @pytest.mark.skip(reason="Flaky on CI, tracking issue #123")

Fixed code:

import pytest

@pytest.mark.skip(reason="Flaky on CI, investigating intermittent timeout #123")
def test_flaky_integration():
    # Test that sometimes fails
    pass

CI/CD Integration

GitHub Actions

name: Lint

on: [push, pull_request]

jobs:
  lazy-ignores-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install thailint
        run: pip install thailint

      - name: Check for unjustified suppressions
        run: thailint lazy-ignores src/

      - name: Upload SARIF (optional)
        if: always()
        uses: github/codeql-action/upload-sarif@v2
        with:
          sarif_file: lazy-ignores.sarif
        continue-on-error: true

GitHub Actions with SARIF

name: Code Scanning

on: [push, pull_request]

jobs:
  lazy-ignores:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install thailint
        run: pip install thailint

      - name: Run lazy-ignores check
        run: thailint lazy-ignores --format sarif src/ > lazy-ignores.sarif

      - name: Upload SARIF
        uses: github/codeql-action/upload-sarif@v2
        with:
          sarif_file: lazy-ignores.sarif

Pre-commit Hook

# .pre-commit-config.yaml
repos:
  - repo: local
    hooks:
      - id: lazy-ignores
        name: Check unjustified suppressions
        entry: thailint lazy-ignores
        language: python
        types: [python]
        pass_filenames: false

Makefile Integration

lint-lazy-ignores:
    @echo "=== Checking for unjustified suppressions ==="
    @poetry run thailint lazy-ignores src/ || exit 1

lint-all: lint-lazy-ignores
    @echo "All checks passed"

Justfile Integration

# Check for unjustified suppressions
lint-lazy-ignores:
    @echo "=== Checking for unjustified suppressions ==="
    poetry run thailint lazy-ignores src/

# Full linting including lazy-ignores
lint-full: lint lint-lazy-ignores lint-complexity lint-solid

Language Support

Python Support

Fully Supported

Patterns detected: - # noqa with optional rule IDs (e.g., # noqa: PLR0912, PLR0915) - # type: ignore with optional error codes (e.g., # type: ignore[arg-type]) - # pylint: disable=rule-name (e.g., # pylint: disable=no-member) - # nosec with optional rule IDs (e.g., # nosec B602) - # pyright: ignore with optional codes (e.g., # pyright: ignore[reportPrivateImportUsage]) - # thailint: ignore[rule] (thai-lint's own ignore directive) - @pytest.mark.skip without reason - pytest.skip() without reason string

Header format: Python docstrings (triple-quoted strings)

TypeScript/JavaScript Support

Fully Supported

Patterns detected: - // @ts-ignore - // @ts-nocheck - // @ts-expect-error - // eslint-disable-next-line rule-name - // eslint-disable-line rule-name - /* eslint-disable rule-name */ (block) - it.skip(), describe.skip(), test.skip() (Jest/Mocha)

Header format: JSDoc comments (/** ... */)

Best Practices

1. Fix Rather Than Suppress

Before adding a suppression, try to fix the underlying issue:

# Instead of this:
def complex_function():  # noqa: PLR0912
    if a:
        if b:
            if c:
                # deeply nested logic

# Do this:
def complex_function():
    if not a:
        return
    if not b:
        return
    if not c:
        return
    # flattened logic

2. Be Specific with Rule IDs

# BAD - Suppresses all checks
result = foo()  # noqa

# GOOD - Suppresses only the specific rule
result = foo()  # noqa: PLR0912
"""
Suppressions:
    - PLR0912: State machine complexity (all methods in this module)
    - PLR0915: Related to PLR0912, same state machine logic
"""

4. Include Context in Justifications

"""
Suppressions:
    - B602: Shell command from trusted config file, input sanitized by ConfigValidator
    - arg-type: Pydantic v2 handles coercion, type error is false positive
"""

5. Review Suppressions Periodically

Set up a periodic review to check if suppressions are still needed: - Remove orphaned entries - Reconsider if fixes are now possible - Update justifications if context has changed

6. Never Auto-Add Suppressions

AI assistants should never add suppression comments without human approval:

"I'm encountering a MyPy error on line 45. I've tried several approaches
but cannot resolve it without a type: ignore. May I add a suppression
with the justification 'Dynamic attribute from decorator'?"

Troubleshooting

Issue: False Positives on Legitimate Patterns

Some patterns may be flagged incorrectly. Check:

  1. Rule ID normalization: Ensure the header entry matches the code
  2. Code: # noqa: PLR0912 → Header: PLR0912
  3. Code: # type: ignore[arg-type] → Header: arg-type

  4. File exclusion: Add to ignore_patterns if file should be excluded

lazy-ignores:
  ignore_patterns:
    - "src/legacy/**"
    - "**/vendor/**"

Issue: Orphaned Entries Not Detected

Orphaned detection requires exact rule ID matching:

# Header says:
#   - plr0912: justification

# Code has:
#   # noqa: PLR0912

# These DON'T match! Use consistent casing.

Issue: TypeScript Files Not Scanned

Ensure files have .ts or .tsx extension:

# Verify TypeScript parsing
thailint lazy-ignores --verbose src/file.ts

API Reference

Configuration Class

@dataclass
class LazyIgnoresConfig:
    check_noqa: bool = True
    check_type_ignore: bool = True
    check_pylint_disable: bool = True
    check_nosec: bool = True
    check_pyright_ignore: bool = True
    check_ts_ignore: bool = True
    check_eslint_disable: bool = True
    check_thailint_ignore: bool = True
    check_test_skips: bool = True
    check_orphaned: bool = True
    ignore_patterns: list[str] = field(default_factory=list)

Rule Class

class LazyIgnoresRule(BaseLintRule):
    rule_id: str = "lazy-ignores.unjustified"
    rule_name: str = "Unjustified Suppression"

    def check(self, context: BaseLintContext) -> list[Violation]:
        """Check file for unjustified suppressions."""

Violation Types

Rule ID Description
lazy-ignores.unjustified Suppression in code without header entry
lazy-ignores.orphaned Header entry without matching code suppression
lazy-ignores.test-skip-no-reason Test skip without reason parameter

Resources

  • CLI Reference: docs/cli-reference.md - Complete CLI documentation
  • Configuration Guide: docs/configuration.md - Config file reference
  • File Header Standards: .ai/docs/FILE_HEADER_STANDARDS.md - Suppressions section format
  • How to Fix: .ai/howtos/how-to-fix-lazy-ignores.md - AI agent fix guide

Contributing

Report issues or suggest improvements: - GitHub Issues: https://github.com/be-wise-be-kind/thai-lint/issues - Feature requests: Tag with enhancement - Bug reports: Tag with bug