-
-
Save zverok/5e858fac96a2e9f5282bbeab8fec2d39 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(&: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