Coverage for src/pylint_sort_functions/utils/decorators.py: 100%

1"""Decorator analysis and exclusion logic for framework-aware sorting.

2

3 This module provides functionality to analyze function decorators and determine

4 whether functions should be excluded from sorting requirements based on their

5 decorators (e.g., framework-specific decorators that create ordering dependencies).

6"""

7

8 import re

9

10 from astroid import nodes # type: ignore[import-untyped]

11

12

13 def decorator_matches_pattern(decorator_str: str, pattern: str) -> bool:

14 """Check if a decorator string matches an ignore pattern.

15

16 Supports exact matches and simple wildcard patterns. This allows users to

17 exclude functions with specific decorators from sorting requirements when

18 the decorators create ordering dependencies.

19

20 Examples:

21 - "@app.route" matches both @app.route and @app.route("/path")

22 - "@*.command" matches @main.command(), @cli.command(), etc.

23

24 :param decorator_str: Decorator string to check (e.g., "@main.command()")

25 :type decorator_str: str

26 :param pattern: Pattern to match against (e.g., "@main.command", "@*.command")

27 :type pattern: str

28 :returns: True if decorator matches the pattern

29 :rtype: bool

30 """

31 # Normalize patterns by ensuring they start with @

32 if not pattern.startswith("@"):

33 pattern = f"@{pattern}"

34

35 # Exact match

36 if decorator_str == pattern:

37 return True

38

39 # Remove parentheses for pattern matching (treat @main.command() as @main.command)

40 decorator_base = decorator_str.rstrip("()")

41 pattern_base = pattern.rstrip("()")

42

43 if decorator_base == pattern_base:

44 return True

45

46 # Simple wildcard support: @*.command matches @main.command, @app.command, etc.

47 if "*" in pattern_base:

48 # Convert simple wildcard pattern to regex

49 # First escape the pattern, then replace escaped wildcards with regex

50 regex_pattern = re.escape(pattern_base)

51 regex_pattern = regex_pattern.replace(r"\*", r"[^.]+")

52 regex_pattern = f"^{regex_pattern}$"

53 if re.match(regex_pattern, decorator_base):

54 return True

55

56 return False

57

58

59 def function_has_excluded_decorator(

60 func: nodes.FunctionDef, ignore_decorators: list[str] | None

61) -> bool:

62 """Check if a function should be excluded from sorting due to its decorators.

63

64 Some decorators create dependencies that make alphabetical sorting inappropriate.

65 For example, Click commands or Flask routes may need specific ordering for proper

66 framework behavior.

67

68 :param func: Function definition node to check

69 :type func: nodes.FunctionDef

70 :param ignore_decorators: List of decorator patterns to match against

71 :type ignore_decorators: list[str] | None

72 :returns: True if function should be excluded from sorting requirements

73 :rtype: bool

74 """

75 if not ignore_decorators or not func.decorators:

76 return False

77

78 # Get string representations of all decorators on this function

79 function_decorators = get_decorator_strings(func)

80

81 # Check if any decorator matches any ignore pattern

82 for decorator_str in function_decorators:

83 for ignore_pattern in ignore_decorators:

84 if decorator_matches_pattern(decorator_str, ignore_pattern):

85 return True

86

87 return False

88

89

90 def get_decorator_strings(func: nodes.FunctionDef) -> list[str]:

91 """Extract string representations of all decorators on a function.

92

93 :param func: Function definition node

94 :type func: nodes.FunctionDef

95 :returns: List of decorator strings (e.g., ["@main.command()", "@app.route()"])

96 :rtype: list[str]

97 """

98 if not func.decorators:

99 return []

100

101 decorator_strings = []

102 for decorator in func.decorators.nodes:

103 decorator_str = _decorator_node_to_string(decorator)

104 if decorator_str:

105 decorator_strings.append(f"@{decorator_str}")

106

107 return decorator_strings

108

109

110 def _decorator_node_to_string(decorator: nodes.NodeNG) -> str:

111 """Convert a decorator AST node to its string representation.

112

113 :param decorator: Decorator AST node

114 :type decorator: nodes.NodeNG

115 :returns: String representation of the decorator (without @ prefix)

116 :rtype: str

117 """

118 if isinstance(decorator, nodes.Name):

119 # Simple decorator: @decorator_name

120 return str(decorator.name)

121

122 if isinstance(decorator, nodes.Attribute):

123 # Attribute decorator: @obj.method

124 if isinstance(decorator.expr, nodes.Name):

125 return f"{decorator.expr.name}.{decorator.attrname}"

126 # Handle nested attributes: @obj.nested.method

127 base = _decorator_node_to_string(decorator.expr)

128 if base:

129 return f"{base}.{decorator.attrname}"

130

131 if isinstance(decorator, nodes.Call):

132 # Function call decorator: @decorator() or @obj.method(args)

133 func_str = _decorator_node_to_string(decorator.func)

134 if func_str:

135 return f"{func_str}()"

136

137 # Fallback for complex decorators - return empty string to skip

138 return ""