Skip to content

Commit

Permalink
Optimize code length calculation
Browse files Browse the repository at this point in the history
  • Loading branch information
fatkodima authored and bbatsov committed Jul 18, 2020
1 parent cb57295 commit 5b7c51c
Show file tree
Hide file tree
Showing 9 changed files with 99 additions and 89 deletions.
1 change: 0 additions & 1 deletion lib/rubocop.rb
Expand Up @@ -110,7 +110,6 @@
require_relative 'rubocop/cop/mixin/string_help'
require_relative 'rubocop/cop/mixin/string_literals_help'
require_relative 'rubocop/cop/mixin/target_ruby_version'
require_relative 'rubocop/cop/mixin/too_many_lines'
require_relative 'rubocop/cop/mixin/trailing_body'
require_relative 'rubocop/cop/mixin/trailing_comma'
require_relative 'rubocop/cop/mixin/uncommunicative_name'
Expand Down
2 changes: 1 addition & 1 deletion lib/rubocop/cop/metrics/block_length.rb
Expand Up @@ -30,7 +30,7 @@ module Metrics
# HEREDOC
# end # 5 points
class BlockLength < Cop
include TooManyLines
include CodeLength

LABEL = 'Block'

Expand Down
2 changes: 1 addition & 1 deletion lib/rubocop/cop/metrics/class_length.rb
Expand Up @@ -30,7 +30,7 @@ module Metrics
# end # 5 points
#
class ClassLength < Cop
include TooManyLines
include CodeLength

def on_class(node)
check_code_length(node)
Expand Down
2 changes: 1 addition & 1 deletion lib/rubocop/cop/metrics/method_length.rb
Expand Up @@ -30,7 +30,7 @@ module Metrics
# end # 5 points
#
class MethodLength < Cop
include TooManyLines
include CodeLength

LABEL = 'Method'

Expand Down
2 changes: 1 addition & 1 deletion lib/rubocop/cop/metrics/module_length.rb
Expand Up @@ -30,7 +30,7 @@ module Metrics
# end # 5 points
#
class ModuleLength < Cop
include TooManyLines
include CodeLength

def on_module(node)
check_code_length(node)
Expand Down
78 changes: 54 additions & 24 deletions lib/rubocop/cop/metrics/utils/code_length_calculator.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require 'set'

module RuboCop
module Cop
module Metrics
Expand All @@ -10,24 +12,23 @@ class CodeLengthCalculator
include Util

FOLDABLE_TYPES = %i[array hash heredoc].freeze
CLASSISH_TYPES = %i[class module].freeze
CLASSLIKE_TYPES = %i[class module].freeze

def initialize(node, count_comments: false, foldable_types: [])
def initialize(node, processed_source, count_comments: false, foldable_types: [])
@node = node
@processed_source = processed_source
@count_comments = count_comments
@foldable_checks = build_foldable_checks(foldable_types)
@foldable_types = normalize_foldable_types(foldable_types)
end

def calculate
length = code_length(@node)
return length if @foldable_types.empty?

each_top_level_descendant(@node, *@foldable_types, *CLASSISH_TYPES) do |descendant|
descendant_length = code_length(descendant)

if classlike_node?(descendant)
length -= (descendant_length + 2)
elsif foldable_node?(descendant)
each_top_level_descendant(@node, @foldable_types) do |descendant|
if foldable_node?(descendant)
descendant_length = code_length(descendant)
length = length - descendant_length + 1
end
end
Expand All @@ -37,14 +38,6 @@ def calculate

private

def_node_matcher :class_definition?, <<~PATTERN
(casgn nil? _ (block (send (const nil? :Class) :new) ...))
PATTERN

def_node_matcher :module_definition?, <<~PATTERN
(casgn nil? _ (block (send (const nil? :Module) :new) ...))
PATTERN

def build_foldable_checks(types)
types.map do |type|
case type
Expand All @@ -67,35 +60,72 @@ def normalize_foldable_types(types)
end

def code_length(node)
return heredoc_length(node) if heredoc_node?(node)
if classlike_node?(node)
classlike_code_length(node)
elsif heredoc_node?(node)
heredoc_length(node)
else
body = extract_body(node)
return 0 unless body

body = extract_body(node)
lines = body&.source&.lines || []
lines.count { |line| !irrelevant_line?(line) }
body.source.each_line.count { |line| !irrelevant_line?(line) }
end
end

def heredoc_node?(node)
node.respond_to?(:heredoc?) && node.heredoc?
end

def classlike_code_length(node)
return 0 if namespace_module?(node)

body_line_numbers = line_range(node).to_a[1...-1]

target_line_numbers = body_line_numbers -
line_numbers_of_inner_nodes(node, :module, :class)

target_line_numbers.reduce(0) do |length, line_number|
source_line = @processed_source[line_number]
next length if irrelevant_line?(source_line)

length + 1
end
end

def namespace_module?(node)
classlike_node?(node.body)
end

def line_numbers_of_inner_nodes(node, *types)
line_numbers = Set.new

node.each_descendant(*types) do |inner_node|
line_range = line_range(inner_node)
line_numbers.merge(line_range)
end

line_numbers.to_a
end

def heredoc_length(node)
lines = node.loc.heredoc_body.source.lines
lines.count { |line| !irrelevant_line?(line) } + 2
end

def each_top_level_descendant(node, *types, &block)
def each_top_level_descendant(node, types, &block)
node.each_child_node do |child|
next if classlike_node?(child)

if types.include?(child.type)
yield child
else
each_top_level_descendant(child, *types, &block)
each_top_level_descendant(child, types, &block)
end
end
end

def classlike_node?(node)
CLASSISH_TYPES.include?(node.type) ||
(node.casgn_type? && (class_definition?(node) || module_definition?(node)))
CLASSLIKE_TYPES.include?(node.type)
end

def foldable_node?(node)
Expand Down
20 changes: 13 additions & 7 deletions lib/rubocop/cop/mixin/code_length.rb
Expand Up @@ -6,8 +6,14 @@ module Cop
module CodeLength
include ConfigurableMax

MSG = '%<label>s has too many lines. [%<length>d/%<max>d]'

private

def message(length, max_length)
format(MSG, label: cop_label, length: length, max: max_length)
end

def max_length
cop_config['Max']
end
Expand All @@ -21,9 +27,14 @@ def count_as_one
end

def check_code_length(node)
length = code_length(node)
# Skip costly calculation when definitely not needed
return if node.line_count <= max_length

return unless length > max_length
calculator = Metrics::Utils::CodeLengthCalculator.new(node, processed_source,
count_comments: count_comments?,
foldable_types: count_as_one)
length = calculator.calculate
return if length <= max_length

location = node.casgn_type? ? :name : :expression

Expand All @@ -32,11 +43,6 @@ def check_code_length(node)
self.max = length
end
end

# Returns true for lines that shall not be included in the count.
def irrelevant_line(source_line)
source_line.blank? || !count_comments? && comment_line?(source_line)
end
end
end
end
25 changes: 0 additions & 25 deletions lib/rubocop/cop/mixin/too_many_lines.rb

This file was deleted.

0 comments on commit 5b7c51c

Please sign in to comment.