Skip to content

Instantly share code, notes, and snippets.

@zverok
Created August 8, 2020 13:35
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/2f2a55c99f6093928103d58c1c149146 to your computer and use it in GitHub Desktop.
Save zverok/2f2a55c99f6093928103d58c1c149146 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(&:source_line).each do |line, line_incs|
puts(line)
line_incs.sort_by(&:column).reverse.each do |inc|
print('#' + "#{' ' * (inc.highlight.begin_pos-1)}#{'^' * inc.highlight.size}".ljust(40))
puts " +#{inc.type}" + (inc.explanation ? " (#{inc.explanation})" : '')
end
end
end
private
def on_def(node)
@methods << node
end
end
class Increment
extend Forwardable
def_delegators :@location, :first_line, :last_line, :source_line, :column, :column_range
attr_reader :type, :explanation
def initialize(node, type, explanation)
@node = node
@type = type
@explanation = explanation
@location = node.loc.expression
end
def highlight
highlight_till = first_line == last_line ? column_range.count : source_line.length - column
Parser::Source::Range.new(source_line, column, column + highlight_till)
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 << Increment.new(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