Coverage for src/pylint_sort_functions/utils/decorators.py: 100%
50 statements
« prev ^ index » next coverage.py v7.10.1, created at 2025-08-12 16:06 +0200
« prev ^ index » next coverage.py v7.10.1, created at 2025-08-12 16:06 +0200
1"""Decorator analysis and exclusion logic for framework-aware sorting.
3This module provides functionality to analyze function decorators and determine
4whether functions should be excluded from sorting requirements based on their
5decorators (e.g., framework-specific decorators that create ordering dependencies).
6"""
8import re
10from astroid import nodes # type: ignore[import-untyped]
13def decorator_matches_pattern(decorator_str: str, pattern: str) -> bool:
14 """Check if a decorator string matches an ignore pattern.
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.
20 Examples:
21 - "@app.route" matches both @app.route and @app.route("/path")
22 - "@*.command" matches @main.command(), @cli.command(), etc.
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}"
35 # Exact match
36 if decorator_str == pattern:
37 return True
39 # Remove parentheses for pattern matching (treat @main.command() as @main.command)
40 decorator_base = decorator_str.rstrip("()")
41 pattern_base = pattern.rstrip("()")
43 if decorator_base == pattern_base:
44 return True
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
56 return False
59def 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.
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.
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
78 # Get string representations of all decorators on this function
79 function_decorators = get_decorator_strings(func)
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
87 return False
90def get_decorator_strings(func: nodes.FunctionDef) -> list[str]:
91 """Extract string representations of all decorators on a function.
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 []
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}")
107 return decorator_strings
110def _decorator_node_to_string(decorator: nodes.NodeNG) -> str:
111 """Convert a decorator AST node to its string representation.
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)
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}"
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}()"
137 # Fallback for complex decorators - return empty string to skip
138 return ""