class ConsistentBlankLineIndentation
A RuboCop cop that enforces consistent blank line indentation based on AST structure. This cop ensures that blank lines are indented correctly according to their context in the code, using a two-pass algorithm that analyzes the AST to determine proper indentation levels.
Definitions
MESSAGE = "Blank lines must have the correct indentation."
Signature
-
attribute
String
The message displayed when a blank line has incorrect indentation.
def configured_indentation_width
Get the configured indentation width from cop configuration or fallback to default.
Signature
-
returns
Integer
The number of spaces or tabs to use for each indentation level.
Implementation
def configured_indentation_width
cop_config["IndentationWidth"] || config.for_cop("Layout/IndentationWidth")["Width"] || 1
end
def configured_indentation_style
Get the configured indentation style from cop configuration or fallback to default.
Signature
-
returns
String
The indentation style, either "tab" or "space".
Implementation
def configured_indentation_style
cop_config["IndentationStyle"] || config.for_cop("Layout/IndentationStyle")["Style"] || "tab"
end
def indentation(width)
Generate indentation string based on the current level and configured style.
Signature
-
parameter
width
Integer
The number of indentation levels to apply.
-
returns
String
The indentation string (tabs or spaces).
Implementation
def indentation(width)
case configured_indentation_style
when "tab"
"\t" * (width * configured_indentation_width)
when "space"
" " * (width * configured_indentation_width)
end
end
def on_new_investigation
Main investigation method that processes the source code and checks blank line indentation. This method implements a two-pass algorithm: first building indentation deltas from the AST, then processing each line to check blank lines against expected indentation.
Implementation
def on_new_investigation
indentation_deltas = build_indentation_deltas
current_level = 0
processed_source.lines.each_with_index do |line, index|
line_number = index + 1
unless delta = indentation_deltas[line_number]
# Skip this line (e.g., non-squiggly heredoc content):
next
end
# Check blank lines for correct indentation:
if line.strip.empty?
expected_indentation = indentation(current_level)
if line != expected_indentation
add_offense(
source_range(processed_source.buffer, line_number, 0, line.length),
message: MESSAGE
) do |corrector|
corrector.replace(
source_range(processed_source.buffer, line_number, 0, line.length),
expected_indentation
)
end
end
end
current_level += delta
end
end
def build_indentation_deltas
Build a hash mapping line numbers to indentation deltas (+1 for indent, -1 for dedent). This method walks the AST to identify where indentation should increase or decrease.
Signature
-
returns
Hash(Integer, Integer)
A hash where keys are line numbers and values are deltas.
Implementation
def build_indentation_deltas
deltas = Hash.new(0)
walk_ast_for_indentation(processed_source.ast, deltas)
deltas
end
def walk_ast_for_indentation(node, deltas)
Recursively walk the AST to build indentation deltas for block structures. This method identifies nodes that should affect indentation and records the deltas.
Signature
-
parameter
node
Parser::AST::Node
The current AST node to process.
-
parameter
deltas
Hash(Integer, Integer)
The deltas hash to populate.
Implementation
def walk_ast_for_indentation(node, deltas)
return unless node.is_a?(Parser::AST::Node)
case node.type
when :block, :hash, :array, :class, :module, :sclass, :def, :defs, :case, :while, :until, :for, :kwbegin
if location = node.location
deltas[location.line] += 1
deltas[location.last_line] -= 1
end
when :if
# We don't want to add deltas for elsif, because it's handled by the if node:
if node.keyword == "if"
if location = node.location
deltas[location.line] += 1
deltas[location.last_line] -= 1
end
end
when :dstr
if location = node.location
if location.is_a?(Parser::Source::Map::Heredoc) and body = location.heredoc_body
# Don't touch the indentation of heredoc bodies:
(body.line..body.last_line).each do |line|
deltas[line] = nil
end
end
end
end
node.children.each do |child|
walk_ast_for_indentation(child, deltas)
end
end
def source_range(buffer, line, column, length)
Create a source range for a specific line and column position.
Signature
-
parameter
buffer
Parser::Source::Buffer
The source buffer.
-
parameter
line
Integer
The line number (1-indexed).
-
parameter
column
Integer
The column position.
-
parameter
length
Integer
The length of the range.
-
returns
Parser::Source::Range
The source range object.
Implementation
def source_range(buffer, line, column, length)
Parser::Source::Range.new(buffer, buffer.line_range(line).begin_pos + column, buffer.line_range(line).begin_pos + column + length)
end