Coverage for lintro / ai / filters.py: 51%
41 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
1"""Issue filtering for AI processing."""
3from __future__ import annotations
5import fnmatch
6import re
7from pathlib import PurePosixPath
8from typing import TYPE_CHECKING
10if TYPE_CHECKING:
11 from lintro.ai.config import AIConfig
12 from lintro.parsers.base_issue import BaseIssue
15def _glob_match(path: str, pattern: str) -> bool:
16 """Match a path against a glob pattern with recursive ``**`` support.
18 Uses ``PurePosixPath.full_match`` (Python 3.13+) when available,
19 falling back to a regex-based converter for older versions.
21 ``**`` matches zero or more path segments (including separators).
22 ``*`` matches any characters except ``/``.
23 ``?`` matches any single character except ``/``.
25 Args:
26 path: File path to test (forward-slash separated).
27 pattern: Glob pattern, may contain ``**`` for recursive matching.
29 Returns:
30 True if the path matches the pattern.
31 """
32 p = PurePosixPath(path)
33 if hasattr(p, "full_match"):
34 return bool(p.full_match(pattern))
36 # Fallback for Python <3.13: convert glob to regex
37 i = 0
38 n = len(pattern)
39 regex = "^"
40 while i < n:
41 c = pattern[i]
42 if c == "*":
43 if i + 1 < n and pattern[i + 1] == "*":
44 # ** — match zero or more path segments
45 i += 2
46 if i < n and pattern[i] == "/":
47 i += 1
48 regex += "(?:.+/)?"
49 else:
50 regex += ".*"
51 continue
52 regex += "[^/]*"
53 elif c == "?":
54 regex += "[^/]"
55 else:
56 regex += re.escape(c)
57 i += 1
58 regex += "$"
59 return bool(re.match(regex, path))
62def should_process_issue(issue: BaseIssue, config: AIConfig) -> bool:
63 """Check if an issue should be sent to AI based on path/rule filters.
65 Evaluates include/exclude glob patterns for both file paths and rule
66 codes. Include patterns act as an allowlist (only matching items are
67 processed). Exclude patterns act as a denylist (matching items are
68 skipped). When both include and exclude are set, include is checked
69 first.
71 Path patterns support recursive ``**`` matching (e.g. ``src/**/*.py``).
72 Rule patterns use standard ``fnmatch`` (e.g. ``E5*``, ``F401``).
74 Args:
75 issue: The issue to evaluate.
76 config: AI configuration containing filter patterns.
78 Returns:
79 True if the issue should be processed by AI, False otherwise.
80 """
81 file_path = getattr(issue, "file", "") or ""
82 code = getattr(issue, "code", "") or ""
84 # Path filtering — _glob_match handles both * and ** correctly
85 if config.include_paths and not any(
86 _glob_match(file_path, p) for p in config.include_paths
87 ):
88 return False
89 if config.exclude_paths and any(
90 _glob_match(file_path, p) for p in config.exclude_paths
91 ):
92 return False
94 # Rule filtering (fnmatch is fine for error codes)
95 if config.include_rules and not any(
96 fnmatch.fnmatch(code, r) for r in config.include_rules
97 ):
98 return False
99 return not (
100 config.exclude_rules
101 and any(fnmatch.fnmatch(code, r) for r in config.exclude_rules)
102 )
105def filter_issues(issues: list[BaseIssue], config: AIConfig) -> list[BaseIssue]:
106 """Filter a list of issues based on AI path/rule configuration.
108 Args:
109 issues: List of issues to filter.
110 config: AI configuration containing filter patterns.
112 Returns:
113 Filtered list containing only issues that pass all filters.
114 """
115 return [i for i in issues if should_process_issue(i, config)]