Coverage for sources / vibelinter / rules / implementations / vbl201.py: 92%
106 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-07 04:34 +0000
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-07 04:34 +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''' VBL201: Import hub enforcement - no public imports in non-hub modules.
26 Category: Imports / Architecture
27 Subcategory: Namespace Management
29 This rule enforces the import hub pattern by detecting non-private imports
30 in modules that are not designated as import hubs. All imports must either:
31 1. Be from __future__
32 2. Result in private names (starting with _)
33 3. Be in a hub module (identified by configurable glob patterns)
35 This maintains architectural consistency, prevents namespace pollution,
36 and makes the codebase self-documenting.
37'''
40from . import __
43class VBL201( __.BaseRule ):
44 ''' Enforces import hub pattern for non-hub modules. '''
46 @property
47 def rule_id( self ) -> str:
48 return 'VBL201'
50 def __init__(
51 self,
52 filename: str,
53 wrapper: __.libcst.metadata.MetadataWrapper,
54 source_lines: tuple[ str, ... ],
55 hub_patterns: __.Absential[ tuple[ str, ... ] ] = __.absent,
56 ) -> None:
57 super( ).__init__( filename, wrapper, source_lines )
58 # Store hub patterns from configuration or use defaults
59 self._hub_patterns: tuple[ str, ... ] = (
60 hub_patterns if not __.is_absent( hub_patterns )
61 else ( '__init__.py', '__main__.py', '__.py', '__/imports.py' ) )
62 # Determine if this file is a hub module
63 self._is_hub_module: bool = self._is_import_hub_module( )
64 # Track function nesting depth (to allow local imports)
65 self._function_depth: int = 0
66 # Collections for violations
67 self._simple_imports: list[ __.libcst.Import ] = [ ]
68 self._from_imports: list[ __.libcst.ImportFrom ] = [ ]
70 def visit_FunctionDef( self, node: __.libcst.FunctionDef ) -> bool:
71 ''' Tracks entry into function definitions. '''
72 self._function_depth += 1
73 return True
75 def leave_FunctionDef(
76 self, original_node: __.libcst.FunctionDef
77 ) -> None:
78 ''' Tracks exit from function definitions. '''
79 self._function_depth -= 1
81 def visit_Import( self, node: __.libcst.Import ) -> bool:
82 ''' Collects module-level simple import statements (import foo). '''
83 if self._is_hub_module:
84 return True
85 # Allow imports inside function bodies (local imports)
86 if self._function_depth > 0:
87 return True
88 # Check if all imported names are private
89 if all( self._is_alias_private( alias ) for alias in node.names ):
90 return True
91 self._simple_imports.append( node )
92 return True
94 def visit_ImportFrom( self, node: __.libcst.ImportFrom ) -> bool:
95 ''' Collects module-level from imports (from foo import bar). '''
96 if self._is_hub_module:
97 return True
98 # Allow imports inside function bodies (local imports)
99 if self._function_depth > 0:
100 return True
101 if self._is_future_import( node ):
102 return True
103 if self._has_private_names( node ):
104 return True
105 self._from_imports.append( node )
106 return True
108 def _analyze_collections( self ) -> None:
109 ''' Analyzes collected imports and generates violations. '''
110 for node in self._simple_imports:
111 self._report_simple_import_violation( node )
112 for node in self._from_imports:
113 self._report_from_import_violation( node )
115 def _is_import_hub_module( self ) -> bool:
116 ''' Checks if current file matches any hub module pattern.
118 Uses glob patterns from configuration to identify hub modules.
119 Patterns are matched against both the filename and full path.
120 '''
121 file_path = __.pathlib.Path( self.filename )
122 for pattern in self._hub_patterns:
123 # Try matching against the file path
124 if file_path.match( pattern ):
125 return True
126 # Try matching with wildcard prefix for path-based patterns
127 if file_path.match( f'*/{pattern}' ): 127 ↛ 128line 127 didn't jump to line 128 because the condition on line 127 was never true
128 return True
129 return False
131 def _is_future_import( self, node: __.libcst.ImportFrom ) -> bool:
132 ''' Checks if import is from __future__. '''
133 module = node.module
134 if isinstance( module, __.libcst.Attribute ):
135 return False
136 if module is None:
137 return False
138 return module.value == '__future__'
140 def _has_private_names( self, node: __.libcst.ImportFrom ) -> bool:
141 ''' Checks if all imported names are private (start with _).
143 Examples of allowed imports:
144 - from . import __ (__ starts with _)
145 - from . import exceptions as _exceptions (alias starts with _)
146 - from json import loads as _json_loads (alias starts with _)
148 Examples of violations:
149 - from . import exceptions (exceptions doesn't start with _)
150 - from pathlib import Path (Path doesn't start with _)
151 - from pathlib import Path as P (P doesn't start with _)
152 '''
153 # Star imports are never private
154 if isinstance( node.names, __.libcst.ImportStar ):
155 return False
156 # Check each imported name
157 return all( self._is_alias_private( alias ) for alias in node.names )
159 def _is_alias_private( self, alias: __.libcst.ImportAlias ) -> bool:
160 ''' Checks if an import alias results in a private name. '''
161 if isinstance( alias.asname, __.libcst.AsName ):
162 alias_name = alias.asname.name
163 if isinstance( alias_name, __.libcst.Name ): 163 ↛ 165line 163 didn't jump to line 165 because the condition on line 163 was always true
164 return alias_name.value.startswith( '_' )
165 return False
166 node = alias.name
167 while isinstance( node, __.libcst.Attribute ): 167 ↛ 168line 167 didn't jump to line 168 because the condition on line 167 was never true
168 node = node.value
169 if isinstance( node, __.libcst.Name ): 169 ↛ 171line 169 didn't jump to line 171 because the condition on line 169 was always true
170 return node.value.startswith( '_' )
171 return False
173 def _report_simple_import_violation(
174 self, node: __.libcst.Import
175 ) -> None:
176 ''' Reports violation for simple import statement. '''
177 # Extract module name from import
178 if node.names: 178 ↛ 185line 178 didn't jump to line 185 because the condition on line 178 was always true
179 module_name = node.names[ 0 ].name.value
180 message = (
181 f"Direct import of '{module_name}'. "
182 f"Use import hub or private alias."
183 )
184 else:
185 message = (
186 "Direct import detected. Use import hub or private alias." )
187 self._produce_violation( node, message, severity = 'warning' )
189 def _report_from_import_violation(
190 self, node: __.libcst.ImportFrom
191 ) -> None:
192 ''' Reports violation for from import statement. '''
193 # Extract module name
194 if node.module is None:
195 module_name = "relative import"
196 elif isinstance( node.module, __.libcst.Attribute ):
197 module_name = self._extract_dotted_name( node.module )
198 else:
199 module_name = node.module.value
200 # Extract imported names
201 imported_names: list[ str ] = [ ]
202 if isinstance( node.names, __.libcst.ImportStar ):
203 imported_names = [ '*' ]
204 else:
205 for name in node.names:
206 name_node = name.name
207 if isinstance( name_node, __.libcst.Name ): 207 ↛ 205line 207 didn't jump to line 205 because the condition on line 207 was always true
208 imported_names.append( name_node.value )
209 names_str = ', '.join( imported_names )
210 message = (
211 f"Non-private import from '{module_name}': {names_str}. "
212 f"Use private names (starting with _)."
213 )
214 self._produce_violation( node, message, severity = 'warning' )
216 def _extract_dotted_name( self, attr: __.libcst.Attribute ) -> str:
217 ''' Extracts dotted module name from Attribute node. '''
218 parts: list[ str ] = [ ]
219 current: __.libcst.BaseExpression = attr
220 while isinstance( current, __.libcst.Attribute ):
221 parts.append( current.attr.value )
222 current = current.value
223 if isinstance( current, __.libcst.Name ): 223 ↛ 225line 223 didn't jump to line 225 because the condition on line 223 was always true
224 parts.append( current.value )
225 parts.reverse( )
226 return '.'.join( parts )
229# Self-register this rule
230__.RULE_DESCRIPTORS[ 'VBL201' ] = __.RuleDescriptor(
231 vbl_code = 'VBL201',
232 descriptive_name = 'import-hub-enforcement',
233 description = 'Enforces import hub pattern for non-hub modules.',
234 category = 'imports',
235 subcategory = 'architecture',
236 rule_class = VBL201,
237)