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