-
-
Save zverok/2f2a55c99f6093928103d58c1c149146 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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