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
« 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''' VBL101: Detect blank lines between statements in function bodies.
26 Category: Readability
27 Subcategory: Compactness
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'''
36from . import __
39class VBL101( __.BaseRule ):
40 ''' Detects blank lines between statements in function bodies. '''
42 @property
43 def rule_id( self ) -> str:
44 return 'VBL101'
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( )
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
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
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
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
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
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 )
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 )
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 )
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 )
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)