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

1"""Issue filtering for AI processing.""" 

2 

3from __future__ import annotations 

4 

5import fnmatch 

6import re 

7from pathlib import PurePosixPath 

8from typing import TYPE_CHECKING 

9 

10if TYPE_CHECKING: 

11 from lintro.ai.config import AIConfig 

12 from lintro.parsers.base_issue import BaseIssue 

13 

14 

15def _glob_match(path: str, pattern: str) -> bool: 

16 """Match a path against a glob pattern with recursive ``**`` support. 

17 

18 Uses ``PurePosixPath.full_match`` (Python 3.13+) when available, 

19 falling back to a regex-based converter for older versions. 

20 

21 ``**`` matches zero or more path segments (including separators). 

22 ``*`` matches any characters except ``/``. 

23 ``?`` matches any single character except ``/``. 

24 

25 Args: 

26 path: File path to test (forward-slash separated). 

27 pattern: Glob pattern, may contain ``**`` for recursive matching. 

28 

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)) 

35 

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)) 

60 

61 

62def should_process_issue(issue: BaseIssue, config: AIConfig) -> bool: 

63 """Check if an issue should be sent to AI based on path/rule filters. 

64 

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. 

70 

71 Path patterns support recursive ``**`` matching (e.g. ``src/**/*.py``). 

72 Rule patterns use standard ``fnmatch`` (e.g. ``E5*``, ``F401``). 

73 

74 Args: 

75 issue: The issue to evaluate. 

76 config: AI configuration containing filter patterns. 

77 

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 "" 

83 

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 

93 

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 ) 

103 

104 

105def filter_issues(issues: list[BaseIssue], config: AIConfig) -> list[BaseIssue]: 

106 """Filter a list of issues based on AI path/rule configuration. 

107 

108 Args: 

109 issues: List of issues to filter. 

110 config: AI configuration containing filter patterns. 

111 

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)]