Skip to content

Instantly share code, notes, and snippets.

@zverok
Created August 8, 2020 12:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zverok/5e858fac96a2e9f5282bbeab8fec2d39 to your computer and use it in GitHub Desktop.
Save zverok/5e858fac96a2e9f5282bbeab8fec2d39 to your computer and use it in GitHub Desktop.
require 'rubocop'
# Usage: `complications.rb foo/bar/baz.rb:123`
# ...to analyze ONE method starting at line 123
location = ARGV.shift or fail "Provide location to method to analyze"
class Runner
include RuboCop::AST::Traversal
def initialize(path, line)
# ruby_version = @config_store.for_file(file).target_ruby_version
ruby_version = 2.6
@source = RuboCop::ProcessedSource.from_file(path, ruby_version)
@line = line.to_i
end
def call
@methods = []
walk(@source.ast)
method = @methods.find { |m| m.loc.expression.line == @line } or
fail "No methods defined at #{@line}"
abc, (a, b, c), increases = AbcSizeExplainer.call(method)
puts "Outcome: sqrt(#{a} assignments² + #{b} branches² + #{c} conditions²) = #{abc}"
puts
increases.group_by(&:first).each do |node, incs|
location = node.loc.expression
puts(location.source_line)
column_length = if location.first_line == location.last_line
location.column_range.count
else
location.source_line.length - location.column
end
highlighted_area = Parser::Source::Range.new(location.source_line,
location.column,
location.column + column_length)
print("##{' ' * (highlighted_area.begin_pos-1)}#{'^' * highlighted_area.size}")
puts(" " + incs.map { |_, type, explanation|
"+#{type}" + (explanation ? " (#{explanation})" : '')
}.join(', '))
end
end
private
def on_def(node)
@methods << node
end
end
# See RuboCop::Cop::Metrics::Utils::AbcSizeCalculator. The code is copied from there, and then
# "explanatory" parts added.
class AbcSizeExplainer
include RuboCop::Cop::Metrics::Utils::IteratingBlock
include RuboCop::Cop::Metrics::Utils::RepeatedCsendDiscount
# > Branch -- an explicit forward program branch out of scope -- a
# > function call, class method call ..
# > http://c2.com/cgi/wiki?AbcMetric
BRANCH_NODES = %i[send csend yield].freeze
# > Condition -- a logical/Boolean test, == != <= >= < > else case
# > default try catch ? and unary conditionals.
# > http://c2.com/cgi/wiki?AbcMetric
CONDITION_NODES = RuboCop::Cop::Metrics::CyclomaticComplexity::COUNTED_NODES.freeze
def self.call(node)
new(node).call
end
# TODO: move to rubocop-ast
ARGUMENT_TYPES = %i[arg optarg restarg kwarg kwoptarg kwrestarg blockarg].freeze
def initialize(node)
@assignment = 0
@branch = 0
@condition = 0
@node = node
@increments = []
reset_repeated_csend
end
def call
@node.each_node do |child|
increment!(child, :assignment) if assignment?(child)
if branch?(child)
evaluate_branch_nodes(child)
elsif condition?(child)
evaluate_condition_node(child)
end
end
[
Math.sqrt(@assignment**2 + @branch**2 + @condition**2).round(2),
[@assignment, @branch, @condition],
@increments
]
end
def evaluate_branch_nodes(node)
if node.comparison_method?
increment!(node, :condition, 'comparison')
else
increment!(node, :branch)
increment!(node, :condition) if node.csend_type? && !discount_for_repeated_csend?(node)
end
end
def evaluate_condition_node(node)
increment!(node, :condition, 'due to else') if else_branch?(node)
increment!(node, :condition)
end
def else_branch?(node)
%i[case if].include?(node.type) &&
node.else? &&
node.loc.else.is?('else')
end
private
# methods added by Explainer, instead of just @assignment/@branch/@condition += 1
def increment!(node, type, explanation = nil)
instance_variable_set("@#{type}", instance_variable_get("@#{type}") + 1)
@increments << [node, type, explanation]
end
def assignment?(node)
node.for_type? ||
node.op_asgn_type? ||
(node.respond_to?(:setter_method?) && node.setter_method?) ||
(simple_assignment?(node) && capturing_variable?(node.children.first))
end
def simple_assignment?(node)
return false if node.masgn_type?
if node.equals_asgn?
reset_on_lvasgn(node) if node.lvasgn_type?
return true
end
argument?(node)
end
def capturing_variable?(name)
name && !/^_/.match?(name)
end
# Returns true for nodes which otherwise would be counted
# as one too many assignment
def assignment_doubled_in_ast?(node)
node.masgn_type? || node.or_asgn_type? || node.and_asgn_type?
end
def branch?(node)
BRANCH_NODES.include?(node.type)
end
# TODO: move to rubocop-ast
def argument?(node)
ARGUMENT_TYPES.include?(node.type)
end
def condition?(node)
return false if iterating_block?(node) == false
CONDITION_NODES.include?(node.type)
end
end
Runner.new(*location.split(':', 2)).call
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment