Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ A tool for finding Ruff rules that are not yet configured, and can be added to y
- Rules that can be automatically fixed by Ruff 🪄
- Rules violated in the repository, sorted by ascending violation count 🔎

**✨ New:** When all rules in a category (e.g., RUF, ASYNC) belong to the same situation, adopt-ruff highlights these categories with ready-to-use configuration snippets!

The output is a markdown report, easy to check as a Github action summary and CSV files with relevant Rule information per category.

_See example at the bottom of this page_
Expand Down Expand Up @@ -98,15 +100,28 @@ Run `adopt-ruff --help` for more information.\
## Respected Ruff rules

374 Ruff rules are already respected in the repo - they can be added right away 🚀

### ✅ Categories with ALL rules respected:
- **flake8-async (ASYNC)**: All 12 rules 🎉
- **flake8-builtins (A)**: All 3 rules 🎉

💡 **Quick add to your config:**
```toml
[tool.ruff.lint]
select = ["A", "ASYNC"]
```

<details>
<summary>Details</summary>

| Code | Name | Fixable | Preview | Linter |
|----------|----------------------------------------------|-----------|-----------|----------------------------|
| A001 | builtin-variable-shadowing | No | False | flake8-builtins |
| A002 | builtin-argument-shadowing | No | False | flake8-builtins |
| A003 | builtin-attribute-shadowing | No | False | flake8-builtins |
| AIR001 | airflow-variable-name-task-id-mismatch | No | False | Airflow |
| ASYNC100 | blocking-http-call-in-async-function | No | False | flake8-async |
| ASYNC101 | open-sleep-or-subprocess-in-async-function | No | False | Perflint |
| ASYNC101 | open-sleep-or-subprocess-in-async-function | No | False | flake8-async |
| PERF403 | manual-dict-comprehension | No | True | Perflint |

(table truncated for example purposes)
Expand All @@ -116,6 +131,17 @@ Run `adopt-ruff --help` for more information.\
## Autofixable Ruff rules

65 Ruff rules are violated in the repo, but can be auto-fixed 🪄

### 🎯 Categories with ALL rules autofixable:
- **Ruff-specific rules (RUF)**: All 5 rules ✨
- **flake8-comprehensions (C4)**: All 8 rules ✨

💡 **Quick add to your config:**
```toml
[tool.ruff.lint]
select = ["C4", "RUF"]
```

<details>
<summary>Details</summary>

Expand All @@ -124,6 +150,10 @@ Run `adopt-ruff --help` for more information.\
| B010 | set-attr-with-constant | Always | False | flake8-bugbear |
| B011 | assert-false | Always | False | flake8-bugbear |
| C401 | unnecessary-generator-set | Always | False | flake8-comprehensions |
| C402 | unnecessary-generator-dict | Always | False | flake8-comprehensions |
| RUF001 | ambiguous-unicode-character-string | Always | False | Ruff-specific rules |
| RUF002 | ambiguous-unicode-character-docstring | Always | False | Ruff-specific rules |
| RUF003 | ambiguous-unicode-character-comment | Always | False | Ruff-specific rules |

(table truncated for example purposes)

Expand All @@ -132,6 +162,13 @@ Run `adopt-ruff --help` for more information.\
## Applicable Rules

194 other Ruff rules are not yet configured in the repository

### 📋 Categories with ALL rules violated:
- **pydocstyle (D)**: All 58 rules 🔍
- **flake8-use-pathlib (PTH)**: All 27 rules 🔍

💡 **Tip:** These categories need attention across the entire codebase

<details>
<summary>Details</summary>

Expand Down
59 changes: 58 additions & 1 deletion adopt_ruff/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,14 @@
from adopt_ruff.models.ruff_config import RuffConfig
from adopt_ruff.models.ruff_output import Violation
from adopt_ruff.models.rule import FixAvailability, Rule
from adopt_ruff.utils import ARTIFACTS_PATH, logger, output_table, search_config_file
from adopt_ruff.utils import (
ARTIFACTS_PATH,
find_complete_categories,
generate_pyproject_suggestion,
logger,
output_table,
search_config_file,
)


def run_ruff(path: Path) -> tuple[set[Rule], tuple[Violation, ...], Version]:
Expand Down Expand Up @@ -99,6 +106,24 @@ def run(
f"{len(respected)} Ruff rules are already respected in the repo - "
"they can be added right away 🚀"
)

# Check for complete categories
complete_cats = find_complete_categories(respected, rules)
if complete_cats:
md.new_header(3, "✅ Categories with ALL rules respected:")
for prefix in sorted(complete_cats.keys()):
linter_name, count = complete_cats[prefix]
md.new_line(f"- **{linter_name} ({prefix})**: All {count} rules 🎉")

# Add configuration suggestion
md.new_line(
"\n💡 **Quick add to your config:**"
)
md.new_line(
generate_pyproject_suggestion(list(complete_cats.keys()), "select")
)
md.new_line("") # Add spacing

output_table(
items=([r.as_dict() for r in respected]),
path=ARTIFACTS_PATH / "respected.csv",
Expand All @@ -120,6 +145,24 @@ def run(
md.new_line(
f"{len(autofixable)} Ruff rules are violated in the repo, but can{always_status} be auto-fixed 🪄"
)

# Check for complete categories
complete_cats = find_complete_categories(autofixable, rules)
if complete_cats:
md.new_header(3, "🎯 Categories with ALL rules autofixable:")
for prefix in sorted(complete_cats.keys()):
linter_name, count = complete_cats[prefix]
md.new_line(f"- **{linter_name} ({prefix})**: All {count} rules ✨")

# Add configuration suggestion
md.new_line(
"\n💡 **Quick add to your config:**"
)
md.new_line(
generate_pyproject_suggestion(list(complete_cats.keys()), "select")
)
md.new_line("") # Add spacing

output_table(
items=([r.as_dict() for r in autofixable]),
path=ARTIFACTS_PATH / "autofixable.csv",
Expand Down Expand Up @@ -153,6 +196,20 @@ def run(
md.new_line(
f"{len(applicable_rules)} other Ruff rules are not yet configured in the repository"
)

# Check for complete categories
complete_cats = find_complete_categories(applicable_rules, rules)
if complete_cats:
md.new_header(3, "📋 Categories with ALL rules violated:")
for prefix in sorted(complete_cats.keys()):
linter_name, count = complete_cats[prefix]
md.new_line(f"- **{linter_name} ({prefix})**: All {count} rules 🔍")

md.new_line(
"\n💡 **Tip:** These categories need attention across the entire codebase"
)
md.new_line("") # Add spacing

output_table(
items=(
[
Expand Down
71 changes: 71 additions & 0 deletions adopt_ruff/utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import csv
import re
from collections import defaultdict
from collections.abc import Iterable
from pathlib import Path

Expand Down Expand Up @@ -80,3 +82,72 @@ def search_config_file(path: Path) -> Path | None:
logger.info(f"found config file at {file_path.resolve()!s}")
return file_path
return None


def extract_category_prefix(code: str) -> str:
"""
Extracts the category prefix from a rule code.
Examples: RUF001 -> RUF, B010 -> B, ASYNC100 -> ASYNC
"""
match = re.match(r"^([A-Z]+)", code)
return match.group(1) if match else code


def find_complete_categories(rules: Iterable, all_rules: set) -> dict[str, tuple[str, int]]:
"""
Finds categories where ALL rules from that category are in the given set.

Args:
rules: The rules in the current situation (e.g., autofixable, respected)
all_rules: All available rules

Returns:
Dict mapping category prefix to (linter_name, count)
"""
from adopt_ruff.models.rule import Rule

rules_list = list(rules) if not isinstance(rules, list) else rules

# Group all rules by category
all_by_category: dict[str, list[Rule]] = defaultdict(list)
for rule in all_rules:
prefix = extract_category_prefix(rule.code)
all_by_category[prefix].append(rule)

# Group current situation rules by category
current_by_category: dict[str, list[Rule]] = defaultdict(list)
for rule in rules_list:
prefix = extract_category_prefix(rule.code)
current_by_category[prefix].append(rule)

# Find complete categories (all rules from category are in current situation)
complete_categories = {}
for prefix, current_rules in current_by_category.items():
all_in_category = all_by_category[prefix]
if len(current_rules) == len(all_in_category) and len(current_rules) > 0:
# All rules from this category are in the current situation
linter_name = current_rules[0].linter
complete_categories[prefix] = (linter_name, len(current_rules))

return complete_categories


def generate_pyproject_suggestion(category_prefixes: list[str], section: str) -> str:
"""
Generates a pyproject.toml configuration suggestion.

Args:
category_prefixes: List of category prefixes (e.g., ["RUF", "ASYNC"])
section: Either "select" for respected/autofixable or "ignore" for applicable

Returns:
Formatted configuration suggestion
"""
if not category_prefixes:
return ""

codes = ", ".join(f'"{prefix}"' for prefix in sorted(category_prefixes))
return f"""```toml
[tool.ruff.lint]
{section} = [{codes}]
```"""
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ build-backend = "hatchling.build"
dev = [
"mypy>=1.14.0",
"pre-commit>=4.0.1",
"pytest>=8.3.4",
"ruff>=0.8.4",
]
typing = [
Expand Down
1 change: 1 addition & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Test package for adopt-ruff
Loading
Loading