Coverage for sources / vibelinter / rules / base.py: 75%
38 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#============================================================================#
21''' Base rule framework with collection-then-analysis pattern. '''
24from . import __
25from . import violations as _violations
28class BaseRule( __.libcst.CSTVisitor ):
29 ''' Abstract base class for linting rules.
31 Implements collection-then-analysis pattern where rules collect data
32 during CST traversal and perform analysis in leave_Module to generate
33 violations. Supports complex rules requiring complete file information.
35 Note: Cannot inherit from abc.ABC due to metaclass conflict with
36 CSTVisitor. However, @abstractmethod decorators still enforce
37 abstract method requirements.
38 '''
40 METADATA_DEPENDENCIES = (
41 __.libcst.metadata.PositionProvider,
42 __.libcst.metadata.ScopeProvider,
43 __.libcst.metadata.QualifiedNameProvider,
44 )
46 def __init__(
47 self,
48 filename: __.typx.Annotated[
49 str,
50 __.ddoc.Doc( 'Path to source file being analyzed.' ) ],
51 wrapper: __.typx.Annotated[
52 __.libcst.metadata.MetadataWrapper,
53 __.ddoc.Doc(
54 'LibCST metadata wrapper providing position and scope.'
55 ) ],
56 source_lines: __.typx.Annotated[
57 tuple[ str, ... ],
58 __.ddoc.Doc( 'Source file lines for context extraction.' ) ],
59 ) -> None:
60 super( ).__init__( )
61 self.filename = filename
62 self.wrapper = wrapper
63 self.source_lines = source_lines
64 self._violations: list[ _violations.Violation ] = [ ]
66 @property
67 @__.abc.abstractmethod
68 def rule_id( self ) -> __.typx.Annotated[
69 str,
70 __.ddoc.Doc( 'Unique identifier for rule (VBL code).' ) ]:
71 ''' Returns the VBL code for this rule. '''
73 @property
74 def violations( self ) -> tuple[ _violations.Violation, ... ]:
75 ''' Returns violations generated by rule analysis. '''
76 return tuple( self._violations )
78 def leave_Module(
79 self, original_node: __.libcst.Module
80 ) -> None:
81 ''' Performs analysis after CST traversal completes.
83 Subclasses must override _analyze_collections to implement
84 rule-specific analysis logic using collected data.
85 '''
86 _ = original_node # Required by LibCST interface
87 self._analyze_collections( )
89 @__.abc.abstractmethod
90 def _analyze_collections( self ) -> None:
91 ''' Analyzes collected data and generates violations.
93 Called by leave_Module after traversal completes.
94 Implementations should examine collected data and call
95 _produce_violation for any violations discovered.
96 '''
98 def _produce_violation(
99 self,
100 node: __.typx.Annotated[
101 __.libcst.CSTNode,
102 __.ddoc.Doc( 'CST node where violation occurred.' ) ],
103 message: __.typx.Annotated[
104 str,
105 __.ddoc.Doc( 'Human-readable violation description.' ) ],
106 severity: __.typx.Annotated[
107 str,
108 __.ddoc.Doc(
109 "Severity level: 'error', 'warning', or 'info'."
110 ) ] = 'error',
111 ) -> None:
112 ''' Creates violation from CST node with precise positioning. '''
113 line, column = self._position_from_node( node )
114 violation = _violations.Violation(
115 rule_id = self.rule_id,
116 filename = self.filename,
117 line = line,
118 column = column,
119 message = message,
120 severity = severity,
121 )
122 self._violations.append( violation )
124 def _extract_context(
125 self,
126 line: __.typx.Annotated[
127 int,
128 __.ddoc.Doc( 'One-indexed line number.' ) ],
129 context_size: __.typx.Annotated[
130 int,
131 __.ddoc.Doc(
132 'Number of lines to show before and after violation.'
133 ) ] = 2,
134 ) -> _violations.ViolationContext:
135 ''' Extracts source code context around violation. '''
136 start_line = max( 1, line - context_size )
137 end_line = min( len( self.source_lines ), line + context_size )
138 context_lines = tuple(
139 self.source_lines[ i ]
140 for i in range( start_line - 1, end_line ) )
141 if self._violations:
142 violation = self._violations[ -1 ]
143 return _violations.ViolationContext(
144 violation = violation,
145 context_lines = context_lines,
146 context_start_line = start_line )
147 raise __.immut.exceptions.Omnierror( )
149 def _position_from_node(
150 self, node: __.libcst.CSTNode
151 ) -> tuple[ int, int ]:
152 ''' Extracts (line, column) position from CST node.
154 Returns one-indexed line and column numbers for consistency.
155 '''
156 try:
157 position = self.wrapper.resolve(
158 __.libcst.metadata.PositionProvider )[ node ]
159 return ( position.start.line, position.start.column + 1 )
160 except KeyError: return ( 1, 1 )