Skip to content

Performance Linter

AI Agent Context (click to expand)

Purpose: Complete guide to using the performance linter for detecting and fixing performance anti-patterns in loops

Scope: Configuration, usage, refactoring patterns, and best practices for performance analysis

Overview: Comprehensive documentation for the performance linter that detects O(n^2) anti-patterns in Python and TypeScript code. Covers two key rules: string-concat-loop (detecting += string concatenation in loops) and regex-in-loop (detecting uncompiled regex calls in loops). Includes AST-based analysis, configuration options, CLI and library usage, common refactoring patterns, and CI/CD integration.

Dependencies: tree-sitter (Python parser), tree-sitter-typescript (TypeScript parser)

Exports: Usage documentation, configuration examples, refactoring patterns

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

Implementation: AST-based pattern detection with helpful violation messages and refactoring suggestions

This follows the AI-Optimized Documentation Standard.


Try It Now

pip install thailint
thailint perf src/

Example output:

src/utils.py:42:8
  [ERROR] performance.string-concat-loop: String concatenation in for loop: 'result +=' creates O(n^2) complexity

src/parser.py:78:11
  [ERROR] performance.regex-in-loop: Regex compilation in for loop: 're.match()' recompiles pattern on each iteration

Fix it: Use "".join() for string building, and re.compile() for regex patterns.


Overview

The performance linter detects common performance anti-patterns in loops that cause O(n^2) time complexity. These patterns are particularly common in AI-generated code and can cause significant slowdowns in production.

Rules

Rule Description Languages
string-concat-loop Detects += string concatenation in loops Python, TypeScript
regex-in-loop Detects uncompiled regex calls in loops Python

Why Performance Patterns Matter

String Concatenation in Loops: - Each += creates a new string object (strings are immutable) - Copying all previous characters each time = O(n^2) total operations - With 1000 iterations: ~500,000 character copies instead of ~1000

Regex in Loops: - re.match(pattern, text) compiles the regex pattern each call - Regex compilation is expensive (parsing, NFA construction) - Pre-compiling with re.compile() avoids repeated work

Real-World Impact

These patterns were found in production codebases: - FastAPI exceptions.py:197 - String concatenation in loop - FastAPI scripts/deploy_docs_status.py:83 - Regex in loop

Benefits

  • Faster execution: O(n) instead of O(n^2) for string operations
  • Lower memory: Avoid creating intermediate string objects
  • Reduced CPU: Compile regex once, not thousands of times
  • Scalability: Performance issues become critical at scale

How It Works

AST-Based Analysis

The linter uses Abstract Syntax Tree (AST) parsing to analyze code structure:

  1. Parse source code into AST using language-specific parsers:
  2. Python: Built-in ast module
  3. TypeScript: tree-sitter-typescript library

  4. Find all loops in the file (for, while, async for)

  5. Detect patterns within loops:

  6. String concatenation: += assignment with string operand
  7. Regex calls: re.match(), re.search(), etc.

  8. Report violations with line numbers and fix suggestions

Pattern Detection

String Concatenation Detection:

def build_message(items):
    result = ""           # String variable initialized
    for item in items:
        result += str(item)  # += detected in loop ← VIOLATION
    return result

Regex Detection:

def find_matches(items, pattern):
    for item in items:
        if re.match(pattern, item):  # re.match() in loop ← VIOLATION
            yield item

Smart Filtering

The linter avoids false positives by:

  • Ignoring numeric +=: count += 1 is fine (integer addition)
  • Ignoring list/dict +=: items += more is fine (list extend)
  • Ignoring compiled patterns: pattern.match() is fine (already compiled)
  • Tracking variable types: Uses heuristics to identify string variables

Configuration

Basic Configuration

Add to .thailint.yaml:

performance:
  enabled: true

  string-concat-loop:
    enabled: true
    report_each_concat: false  # One violation per loop

  regex-in-loop:
    enabled: true

Configuration Options

Option Type Default Description
enabled boolean true Enable/disable performance linter
string-concat-loop.enabled boolean true Enable string concat detection
string-concat-loop.report_each_concat boolean false Report each += separately
regex-in-loop.enabled boolean true Enable regex detection

Ignore Patterns

performance:
  enabled: true
  ignore:
    - "tests/**"
    - "scripts/**"

Usage

CLI Mode

Combined Command (All Rules)

# Check current directory
thailint perf

# Check specific directory
thailint perf src/

# Check specific file
thailint perf src/main.py

Individual Rules

# String concatenation only
thailint perf --rule string-concat src/

# Regex in loop only
thailint perf --rule regex-loop src/

# Or use individual commands
thailint string-concat-loop src/
thailint regex-in-loop src/

With Config File

# Use config file
thailint perf --config .thailint.yaml src/

Output Formats

# Human-readable text (default)
thailint perf src/

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

# SARIF output for GitHub Code Scanning
thailint perf --format sarif src/

Library Mode

from src import Linter

# Initialize with config file
linter = Linter(config_file='.thailint.yaml')

# Lint directory with performance rules
violations = linter.lint('src/', rules=['performance'])

# Process violations
if violations:
    for v in violations:
        print(f"{v.file_path}:{v.line} - {v.message}")

Docker Mode

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

# With specific rule
docker run --rm -v $(pwd):/workspace \
  washad/thailint perf --rule string-concat /workspace/src/

Violation Examples

Example 1: String Concatenation in Loop (Python)

Code with violation:

def build_html(items):
    html = ""
    for item in items:
        html += f"<li>{item.name}</li>"  # ← VIOLATION
    return f"<ul>{html}</ul>"

Violation message:

src/example.py:4:8
  [ERROR] performance.string-concat-loop: String concatenation in for loop: 'html +=' creates O(n^2) complexity

Example 2: Regex in Loop (Python)

Code with violation:

import re

def extract_emails(lines):
    emails = []
    for line in lines:
        match = re.search(r'[\w.]+@[\w.]+', line)  # ← VIOLATION
        if match:
            emails.append(match.group())
    return emails

Violation message:

src/parser.py:7:16
  [ERROR] performance.regex-in-loop: Regex compilation in for loop: 're.search()' recompiles pattern on each iteration

Example 3: TypeScript String Concatenation

Code with violation:

function buildMessage(items: string[]): string {
  let result = "";
  for (const item of items) {
    result += item;  // ← VIOLATION
  }
  return result;
}

Refactoring Patterns

Pattern 1: String Concatenation → join()

Before (O(n^2)):

def build_message(items):
    result = ""
    for item in items:
        result += str(item)
    return result

After (O(n)):

def build_message(items):
    return "".join(str(item) for item in items)

Benefits: Single string allocation, linear time complexity

Pattern 2: String Concatenation → List Append + Join

Before (O(n^2)):

def build_html(items):
    html = ""
    for item in items:
        html += f"<li>{item.name}</li>\n"
    return f"<ul>\n{html}</ul>"

After (O(n)):

def build_html(items):
    parts = [f"<li>{item.name}</li>" for item in items]
    return f"<ul>\n{chr(10).join(parts)}\n</ul>"

Benefits: Lists are mutable, no intermediate string copies

Pattern 3: Regex → Pre-compile

Before (slow):

def find_matches(lines, pattern):
    matches = []
    for line in lines:
        if re.match(pattern, line):
            matches.append(line)
    return matches

After (fast):

def find_matches(lines, pattern):
    compiled = re.compile(pattern)
    matches = []
    for line in lines:
        if compiled.match(line):
            matches.append(line)
    return matches

Benefits: Compile once, use many times

Pattern 4: Regex + Comprehension

Before:

def extract_numbers(lines):
    numbers = []
    for line in lines:
        match = re.search(r'\d+', line)
        if match:
            numbers.append(int(match.group()))
    return numbers

After:

def extract_numbers(lines):
    pattern = re.compile(r'\d+')
    return [int(m.group()) for line in lines if (m := pattern.search(line))]

Benefits: Concise, efficient, Pythonic

Pattern 5: TypeScript Array.join()

Before (O(n^2)):

function buildCSV(rows: string[][]): string {
  let csv = "";
  for (const row of rows) {
    csv += row.join(",") + "\n";
  }
  return csv;
}

After (O(n)):

function buildCSV(rows: string[][]): string {
  return rows.map(row => row.join(",")).join("\n");
}

Ignoring Violations

Line-Level Ignore

def legacy_builder(items):
    result = ""
    for item in items:
        result += item  # thailint: ignore performance.string-concat-loop
    return result

File-Level Ignore

# thailint: ignore-file performance

def function1():
    # All performance violations ignored in this file
    pass

Config-Level Ignore

performance:
  ignore:
    - "tests/**"
    - "scripts/**"
    - "**/legacy/**"

CI/CD Integration

GitHub Actions

name: Performance Check

on: [push, pull_request]

jobs:
  performance-lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Install thai-lint
        run: pip install thailint

      - name: Check performance patterns
        run: thailint perf src/

      - name: Upload SARIF
        if: always()
        uses: github/codeql-action/upload-sarif@v2
        with:
          sarif_file: performance-report.sarif

Pre-commit Hook

# .pre-commit-config.yaml
repos:
  - repo: local
    hooks:
      - id: performance-check
        name: Check performance patterns
        entry: thailint perf
        language: python
        types: [python]
        pass_filenames: false

Makefile Integration

lint-performance:
    @echo "=== Checking performance patterns ==="
    @thailint perf src/ || exit 1

lint-all: lint-performance
    @echo "All checks passed"

Language Support

Python Support

Fully Supported

String concat detection: - += with string literals - += with f-strings - += with str() calls - Variables named: result, output, html, text, msg, message, content

Regex detection: - re.match() - re.search() - re.sub() - re.findall() - re.split() - re.fullmatch()

Import variants: - import re - from re import match, search, ... - import re as regex

TypeScript Support

String Concat: Fully Supported

  • += string concatenation in for/while loops
  • Template literal concatenation
  • Type inference for string variables

Regex: Not Supported

TypeScript regex (/pattern/.test()) follows different patterns and is not detected.

Performance

The performance linter is designed for speed:

Operation Performance Target
Single file parse ~10-30ms <100ms
Single file analysis ~5-15ms <50ms
100 files ~500ms <2s
1000 files ~2-3s <10s

Troubleshooting

Common Issues

Issue: No violations shown but code has patterns

# Check config is loaded
thailint perf --verbose src/

# Verify rules are enabled
cat .thailint.yaml | grep -A5 performance

Issue: False positive on list +=

The linter uses heuristics to identify string variables. If you get false positives:

# Use explicit ignore
items += more_items  # thailint: ignore performance.string-concat-loop

Issue: Compiled regex flagged

The linter tracks re.compile() assignments. Ensure the pattern variable is used:

# Good - pattern.match() not flagged
pattern = re.compile(r'\d+')
for line in lines:
    pattern.match(line)

# Bad - different variable name
p = re.compile(r'\d+')
for line in lines:
    pattern.match(line)  # Might be flagged if 'pattern' not tracked

Best Practices

1. Always Use join() for String Building

# Always prefer
"".join(items)

# Over
result = ""
for item in items:
    result += item

2. Pre-compile Regex Patterns

# Module-level compilation
EMAIL_PATTERN = re.compile(r'[\w.]+@[\w.]+')

def find_emails(text):
    return EMAIL_PATTERN.findall(text)

3. Consider List Comprehensions

# Combine filter and transform
[process(x) for x in items if condition(x)]

4. Benchmark Critical Paths

import timeit

# Measure actual impact
timeit.timeit(lambda: build_string_v1(data), number=1000)
timeit.timeit(lambda: build_string_v2(data), number=1000)

5. Enforce in CI/CD

Make performance checks mandatory:

- name: Performance check
  run: thailint perf --format sarif src/ > perf.sarif

API Reference

Configuration Schema

@dataclass
class PerformanceConfig:
    enabled: bool = True
    string_concat_enabled: bool = True
    regex_in_loop_enabled: bool = True
    report_each_concat: bool = False

Rule IDs

Rule ID Description
performance.string-concat-loop String += in loop
performance.regex-in-loop re.method() in loop

CLI Commands

# Combined command
thailint perf [--rule RULE] [--format FORMAT] [PATHS...]

# Individual commands
thailint string-concat-loop [--format FORMAT] [PATHS...]
thailint regex-in-loop [--format FORMAT] [PATHS...]

Resources

  • CLI Reference: docs/cli-reference.md - Complete CLI documentation
  • Configuration Guide: docs/configuration.md - Config file reference
  • SARIF Output: docs/sarif-output.md - SARIF integration 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

Version History

  • v0.13.0: Performance linter release
  • String concatenation in loop detection (Python + TypeScript)
  • Regex in loop detection (Python)
  • Combined perf command with --rule filtering
  • 80 tests passing (100%)
  • SARIF output support