Coverage for sources / vibelinter / rules / implementations / vbl202.py: 97%
57 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-01 02:35 +0000
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-01 02:35 +0000
1# vim: set filetype=python fileencoding=utf-8:
2# -*- coding: utf-8 -*-
4#============================================================================#
5# #
6# Licensed under the Apache License, Version 2.0 (the "License"); #
7# you may not use this file except in compliance with the License. #
8# You may obtain a copy of the License at #
9# #
10# http://www.apache.org/licenses/LICENSE-2.0 #
11# #
12# Unless required by applicable law or agreed to in writing, software #
13# distributed under the License is distributed on an "AS IS" BASIS, #
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
15# See the License for the specific language governing permissions and #
16# limitations under the License. #
17# #
18#============================================================================#
22''' VBL202: Import spaghetti detection - prevents excessive relative imports.
26 Category: Imports / Architecture
27 Subcategory: Module Coupling
29 This rule prevents "import spaghetti" by restricting the depth of relative
30 imports. Specifically:
32 1. Relative imports with more than 2 parent levels (e.g., `...`, `....`)
33 are never allowed anywhere.
35 2. Relative imports with exactly 2 parent levels (e.g., `from .. import`)
36 are only allowed in private re-export hub modules (configurable,
37 defaults to `__.py`).
39 3. Relative imports with exactly 1 level (e.g., `from . import`) are not
40 allowed in re-export hub modules (`__.py`). This prevents backward
41 imports since siblings expect to do `from . import __`.
43 This maintains low coupling between packages and prevents complex
44 dependency chains that make code hard to understand and refactor.
45'''
48from . import __
51# Maximum allowed relative import depth
52_MAX_RELATIVE_IMPORT_DEPTH = 2
55class VBL202( __.BaseRule ):
56 ''' Enforces restrictions on relative import depth. '''
58 @property
59 def rule_id( self ) -> str:
60 return 'VBL202'
62 def __init__(
63 self,
64 filename: str,
65 wrapper: __.libcst.metadata.MetadataWrapper,
66 source_lines: tuple[ str, ... ],
67 reexport_hub_patterns: __.Absential[ tuple[ str, ... ] ] = __.absent,
68 ) -> None:
69 super( ).__init__( filename, wrapper, source_lines )
70 # Store re-export hub patterns from configuration or use defaults
71 self._reexport_hub_patterns: tuple[ str, ... ] = (
72 reexport_hub_patterns if not __.is_absent( reexport_hub_patterns )
73 else ( '__.py', ) )
74 # Determine if this file is a re-export hub module
75 self._is_reexport_hub: bool = self._is_reexport_hub_module( )
76 # Collections for violations
77 self._excessive_depth_imports: list[ __.libcst.ImportFrom ] = [ ]
78 self._two_level_imports: list[ __.libcst.ImportFrom ] = [ ]
79 self._one_level_imports_in_hub: list[ __.libcst.ImportFrom ] = [ ]
81 def visit_ImportFrom( self, node: __.libcst.ImportFrom ) -> bool:
82 ''' Collects relative import statements with parent references. '''
83 # Calculate the relative import depth
84 depth = self._calculate_relative_depth( node )
85 if depth == 0:
86 # Not a relative import, no violation
87 return True
88 if depth > _MAX_RELATIVE_IMPORT_DEPTH:
89 # More than 2 levels is always a violation
90 self._excessive_depth_imports.append( node )
91 elif depth == _MAX_RELATIVE_IMPORT_DEPTH and not self._is_reexport_hub:
92 # Exactly 2 levels is only allowed in re-export hubs
93 self._two_level_imports.append( node )
94 elif depth == 1 and self._is_reexport_hub:
95 # Single-level imports are not allowed in re-export hubs
96 self._one_level_imports_in_hub.append( node )
97 return True
99 def _analyze_collections( self ) -> None:
100 ''' Analyzes collected imports and generates violations. '''
101 for node in self._excessive_depth_imports:
102 self._report_excessive_depth_violation( node )
103 for node in self._two_level_imports:
104 self._report_two_level_violation( node )
105 for node in self._one_level_imports_in_hub:
106 self._report_one_level_in_hub_violation( node )
108 def _is_reexport_hub_module( self ) -> bool:
109 ''' Checks if current file matches any re-export hub pattern.
111 Uses glob patterns from configuration to identify re-export hubs.
112 Patterns are matched against both the filename and full path.
113 '''
114 file_path = __.pathlib.Path( self.filename )
115 for pattern in self._reexport_hub_patterns:
116 # Try matching against the file path
117 if file_path.match( pattern ):
118 return True
119 # Try matching with wildcard prefix for path-based patterns
120 if file_path.match( f'*/{pattern}' ): 120 ↛ 121line 120 didn't jump to line 121 because the condition on line 120 was never true
121 return True
122 return False
124 def _calculate_relative_depth( self, node: __.libcst.ImportFrom ) -> int:
125 ''' Calculates the depth of relative import (number of parent levels).
127 Examples:
128 - from . import foo -> depth 1
129 - from .. import foo -> depth 2
130 - from ... import foo -> depth 3
131 - from .... import foo -> depth 4
132 - from foo import bar -> depth 0 (absolute import)
133 '''
134 # Check if this is a relative import
135 if node.relative:
136 # Count the dots
137 # node.relative is a sequence of Dot objects
138 return len( node.relative )
139 return 0
141 def _report_excessive_depth_violation(
142 self, node: __.libcst.ImportFrom
143 ) -> None:
144 ''' Reports violation for import with more than 2 parent levels. '''
145 depth = self._calculate_relative_depth( node )
146 dots = '.' * depth
147 message = (
148 f"Excessive relative import depth ({depth} levels): '{dots}'. "
149 f"Maximum allowed depth is {_MAX_RELATIVE_IMPORT_DEPTH} levels."
150 )
151 self._produce_violation( node, message, severity = 'error' )
153 def _report_two_level_violation(
154 self, node: __.libcst.ImportFrom
155 ) -> None:
156 ''' Reports violation for 2-level import outside re-export hub. '''
157 patterns_str = ', '.join( self._reexport_hub_patterns )
158 message = (
159 "Two-level relative import ('from .. import') is only allowed "
160 f"in re-export hub modules ({patterns_str}). "
161 "Move this import to a re-export hub or reduce import depth."
162 )
163 self._produce_violation( node, message, severity = 'warning' )
165 def _report_one_level_in_hub_violation(
166 self, node: __.libcst.ImportFrom
167 ) -> None:
168 ''' Reports violation for single-level import in re-export hub. '''
169 patterns_str = ', '.join( self._reexport_hub_patterns )
170 message = (
171 "Single-level relative import ('from . import') is not allowed "
172 f"in re-export hub modules ({patterns_str}). This creates "
173 "backward imports since siblings expect to import the hub "
174 "(e.g., 'from . import __'). Import from parent package "
175 "instead using 'from .. import'."
176 )
177 self._produce_violation( node, message, severity = 'warning' )
180# Self-register this rule
181__.RULE_DESCRIPTORS[ 'VBL202' ] = __.RuleDescriptor(
182 vbl_code = 'VBL202',
183 descriptive_name = 'import-spaghetti-detection',
184 description = 'Prevents excessive relative import depth.',
185 category = 'imports',
186 subcategory = 'architecture',
187 rule_class = VBL202,
188)