Coverage for sources / vibelinter / rules / implementations / vbl101.py: 66%

74 statements  

« 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 -*- 

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 

22''' VBL101: Detect blank lines between statements in function bodies. 

23 

24 

25 

26 Category: Readability 

27 Subcategory: Compactness 

28 

29 This rule detects blank lines between statements within function or 

30 method bodies and suggests their elimination to improve vertical 

31 compactness per the project coding standards. Blank lines inside 

32 string literals are allowed. 

33''' 

34 

35 

36from . import __ 

37 

38 

39class VBL101( __.BaseRule ): 

40 ''' Detects blank lines between statements in function bodies. ''' 

41 

42 @property 

43 def rule_id( self ) -> str: 

44 return 'VBL101' 

45 

46 def __init__( 

47 self, 

48 filename: str, 

49 wrapper: __.libcst.metadata.MetadataWrapper, 

50 source_lines: tuple[ str, ... ], 

51 ) -> None: 

52 super( ).__init__( filename, wrapper, source_lines ) 

53 # Collection: store definition ranges (functions and classes) 

54 self._definition_ranges: list[ 

55 tuple[ int, int, __.libcst.CSTNode ] ] = [ ] 

56 # Collection: store triple-quoted string literal line ranges 

57 self._string_ranges: list[ tuple[ int, int ] ] = [ ] 

58 # State: track reported lines to prevent duplicates in nested scopes 

59 self._reported_lines: set[ int ] = set( ) 

60 

61 def visit_FunctionDef( self, node: __.libcst.FunctionDef ) -> bool: 

62 ''' Collects function definitions for later analysis. ''' 

63 self._collect_definition( node ) 

64 return True # Continue visiting children 

65 

66 def visit_ClassDef( self, node: __.libcst.ClassDef ) -> bool: 

67 ''' Collects class definitions for later analysis. ''' 

68 self._collect_definition( node ) 

69 return True # Continue visiting children 

70 

71 def _collect_definition( self, node: __.libcst.CSTNode ) -> None: 

72 ''' Helper to collect ranges for functions and classes. ''' 

73 try: 

74 position = self.wrapper.resolve( 

75 __.libcst.metadata.PositionProvider )[ node ] 

76 start_line = position.start.line 

77 end_line = position.end.line 

78 self._definition_ranges.append( ( start_line, end_line, node ) ) 

79 except KeyError: 

80 # Position not available, skip this definition 

81 pass 

82 

83 def visit_SimpleString( self, node: __.libcst.SimpleString ) -> bool: 

84 ''' Collects triple-quoted string literal ranges. ''' 

85 # Only track triple-quoted strings (docstrings and multiline strings) 

86 if node.quote in ( '"""', "'''" ): 

87 try: 

88 position = self.wrapper.resolve( 

89 __.libcst.metadata.PositionProvider )[ node ] 

90 start_line = position.start.line 

91 end_line = position.end.line 

92 self._string_ranges.append( ( start_line, end_line ) ) 

93 except KeyError: 

94 # Position not available, skip this string 

95 pass 

96 return True # Continue visiting children 

97 

98 def visit_ConcatenatedString( 

99 self, node: __.libcst.ConcatenatedString 

100 ) -> bool: 

101 ''' Collects concatenated string literal ranges. ''' 

102 # Check if any part is a triple-quoted string 

103 has_triple_quote = False 

104 for part in ( node.left, node.right ): 

105 if isinstance( part, __.libcst.SimpleString ): 

106 if part.quote in ( '"""', "'''" ): 

107 has_triple_quote = True 

108 break 

109 elif ( 

110 isinstance( part, __.libcst.FormattedString ) 

111 and part.start in ( '"""', "'''" ) 

112 ): 

113 # f-strings can also be triple-quoted 

114 has_triple_quote = True 

115 break 

116 if has_triple_quote: 

117 try: 

118 position = self.wrapper.resolve( 

119 __.libcst.metadata.PositionProvider )[ node ] 

120 start_line = position.start.line 

121 end_line = position.end.line 

122 self._string_ranges.append( ( start_line, end_line ) ) 

123 except KeyError: 

124 # Position not available, skip this string 

125 pass 

126 return True # Continue visiting children 

127 

128 def _analyze_collections( self ) -> None: 

129 ''' Analyzes collected functions for blank lines between statements. 

130 Blank lines inside string literals are allowed. 

131 Blank lines around nested definitions are allowed. 

132 ''' 

133 # Only analyze functions (skip classes as roots) 

134 # But we need all definitions for the adjacency check. 

135 function_nodes = [ 

136 ( s, e, n ) for s, e, n in self._definition_ranges 

137 if isinstance( n, __.libcst.FunctionDef ) 

138 ] 

139 for start_line, end_line, _func_node in function_nodes: 

140 # Get function body start (after the def line) 

141 body_start = start_line + 1 

142 for line_num in range( body_start, end_line + 1 ): 

143 if line_num - 1 >= len( self.source_lines ): break 143 ↛ 139line 143 didn't jump to line 139 because the break on line 143 wasn't executed

144 line = self.source_lines[ line_num - 1 ] 

145 stripped = line.strip( ) 

146 # Report violation for blank lines between statements 

147 # Skip blank lines inside string literals 

148 # Skip blank lines immediately around nested definitions 

149 if ( 

150 not stripped 

151 and not self._is_in_string( line_num ) 

152 and not self._is_adjacent_to_definition( line_num ) 

153 ): 

154 self._report_blank_line( line_num ) 

155 

156 def _is_in_string( self, line_num: int ) -> bool: 

157 ''' Checks if line is inside a triple-quoted string literal. ''' 

158 return any( 

159 start <= line_num <= end 

160 for start, end in self._string_ranges ) 

161 

162 def _is_adjacent_to_definition( self, line_num: int ) -> bool: 

163 ''' Checks if line is immediately before or after a definition. ''' 

164 return any( 

165 line_num == start - 1 or line_num == end + 1 

166 for start, end, _ in self._definition_ranges ) 

167 

168 def _report_blank_line( self, line_num: int ) -> None: 

169 ''' Reports a violation for a blank line in function body. ''' 

170 if line_num in self._reported_lines: return 

171 self._reported_lines.add( line_num ) 

172 from .. import violations as _violations 

173 violation = _violations.Violation( 

174 rule_id = self.rule_id, 

175 filename = self.filename, 

176 line = line_num, 

177 column = 1, 

178 message = "Blank line in function body.", 

179 severity = 'warning' ) 

180 self._violations.append( violation ) 

181 

182 

183# Self-register this rule 

184__.RULE_DESCRIPTORS[ 'VBL101' ] = __.RuleDescriptor( 

185 vbl_code = 'VBL101', 

186 descriptive_name = 'blank-line-elimination', 

187 description = 'Detects blank lines within function bodies.', 

188 category = 'readability', 

189 subcategory = 'compactness', 

190 rule_class = VBL101, 

191)