RuboCop::SocketrySourceRuboCopSocketryLayoutConsistentBlankLineIndentation

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