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

74 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 

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 

140 for start_line, end_line, _func_node in function_nodes: 

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

142 body_start = start_line + 1 

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

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

145 line = self.source_lines[ line_num - 1 ] 

146 stripped = line.strip( ) 

147 # Report violation for blank lines between statements 

148 # Skip blank lines inside string literals 

149 # Skip blank lines immediately around nested definitions 

150 if ( 

151 not stripped 

152 and not self._is_in_string( line_num ) 

153 and not self._is_adjacent_to_definition( line_num ) 

154 ): 

155 self._report_blank_line( line_num ) 

156 

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

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

159 return any( 

160 start <= line_num <= end 

161 for start, end in self._string_ranges ) 

162 

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

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

165 return any( 

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

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

168 

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

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

171 if line_num in self._reported_lines: return 

172 self._reported_lines.add( line_num ) 

173 from .. import violations as _violations 

174 violation = _violations.Violation( 

175 rule_id = self.rule_id, 

176 filename = self.filename, 

177 line = line_num, 

178 column = 1, 

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

180 severity = 'warning' ) 

181 self._violations.append( violation ) 

182 

183 

184# Self-register this rule 

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

186 vbl_code = 'VBL101', 

187 descriptive_name = 'blank-line-elimination', 

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

189 category = 'readability', 

190 subcategory = 'compactness', 

191 rule_class = VBL101, 

192)