Coverage for sources / vibelinter / rules / base.py: 75%

38 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-12-04 00:00 +0000

1# vim: set filetype=python fileencoding=utf-8: 

2# -*- coding: utf-8 -*- 

3 

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#============================================================================# 

19 

20 

21''' Base rule framework with collection-then-analysis pattern. ''' 

22 

23 

24from . import __ 

25from . import violations as _violations 

26 

27 

28class BaseRule( __.libcst.CSTVisitor ): 

29 ''' Abstract base class for linting rules. 

30 

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. 

34 

35 Note: Cannot inherit from abc.ABC due to metaclass conflict with 

36 CSTVisitor. However, @abstractmethod decorators still enforce 

37 abstract method requirements. 

38 ''' 

39 

40 METADATA_DEPENDENCIES = ( 

41 __.libcst.metadata.PositionProvider, 

42 __.libcst.metadata.ScopeProvider, 

43 __.libcst.metadata.QualifiedNameProvider, 

44 ) 

45 

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 ] = [ ] 

65 

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. ''' 

72 

73 @property 

74 def violations( self ) -> tuple[ _violations.Violation, ... ]: 

75 ''' Returns violations generated by rule analysis. ''' 

76 return tuple( self._violations ) 

77 

78 def leave_Module( 

79 self, original_node: __.libcst.Module 

80 ) -> None: 

81 ''' Performs analysis after CST traversal completes. 

82 

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( ) 

88 

89 @__.abc.abstractmethod 

90 def _analyze_collections( self ) -> None: 

91 ''' Analyzes collected data and generates violations. 

92 

93 Called by leave_Module after traversal completes. 

94 Implementations should examine collected data and call 

95 _produce_violation for any violations discovered. 

96 ''' 

97 

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 ) 

123 

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( ) 

148 

149 def _position_from_node( 

150 self, node: __.libcst.CSTNode 

151 ) -> tuple[ int, int ]: 

152 ''' Extracts (line, column) position from CST node. 

153 

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 )