Coverage for patterns/python.py: 86%

1"""Pattern detection for Python source files."""

2

3 from __future__ import annotations

4

5 from models import PatternViolation

6 from patterns.common import create_query, run_captures

7 from patterns.shared import check_patterns as check_patterns_shared

8

9 _PY_NESTED_IMPORT_QUERY = """

10(block (import_statement) @nested_import)

11(block (import_from_statement) @nested_import)

12"""

13

14 _PY_WILDCARD_IMPORT_QUERY = "(import_from_statement (wildcard_import) @wildcard)"

15

16

17 def _is_type_checking_import(node) -> bool:

18 """Check if import is inside a 'if TYPE_CHECKING:' block."""

19 parent = node.parent

20 while parent:

21 if parent.type == "if_statement":

22 for child in parent.children:

23 if child.type in ("identifier", "expression_statement"):

24 text = (

25 child.text

26 if child.type == "identifier"

27 else child.text.decode()

28 if child.text

29 else ""

30 )

31 if text == b"TYPE_CHECKING" or "TYPE_CHECKING" in str(text):

32 return True

33 if parent.type == "module":

34 break

35 parent = parent.parent

36 return False

37

38

39 def check_nested_imports(root) -> list[PatternViolation]:

40 """Detect import statements not at module level (inside functions/classes)."""

41 from parsers.python import PY_LANGUAGE

42

43 query = create_query(PY_LANGUAGE, _PY_NESTED_IMPORT_QUERY)

44 captures = run_captures(query, root)

45

46 violations = []

47 for node, capture_name in captures:

48 if _is_type_checking_import(node):

49 continue

50 line = node.start_point[0] + 1

51 violations.append(

52 PatternViolation(

53 type="nested_import",

54 line=line,

55 description="Import statement inside function/class body instead of module level",

56 severity="warning",

57 )

58 )

59

60 return violations

61

62

63 def check_wildcard_imports(root) -> list[PatternViolation]:

64 """Detect wildcard imports like 'from module import *'."""

65 from parsers.python import PY_LANGUAGE

66

67 query = create_query(PY_LANGUAGE, _PY_WILDCARD_IMPORT_QUERY)

68 captures = run_captures(query, root)

69

70 violations = []

71 for node, capture_name in captures:

72 if capture_name == "wildcard":

73 line = node.start_point[0] + 1

74 violations.append(

75 PatternViolation(

76 type="wildcard_import",

77 line=line,

78 description="Wildcard import 'from module import *' - explicit imports preferred",

79 severity="warning",

80 )

81 )

82

83 return violations

84

85

86 def check_patterns(root, patterns: list[str] | None = None) -> list[PatternViolation]:

87 """Run pattern checks for Python files."""

88 return check_patterns_shared("python", root, patterns)