Skip to content

Instantly share code, notes, and snippets.

@r4mbo7
Created June 19, 2026 08:30
Show Gist options
  • Select an option

  • Save r4mbo7/7c2a44d71bb5a356251f912dc33a0da8 to your computer and use it in GitHub Desktop.

Select an option

Save r4mbo7/7c2a44d71bb5a356251f912dc33a0da8 to your computer and use it in GitHub Desktop.
Cyclomatic complexity analyzer for Ruby methods.
#!/usr/bin/env ruby
# Cyclomatic complexity analyzer for Ruby methods.
#
# Counts the number of linearly independent paths through a method by
# starting at 1 and adding 1 for each decision point:
# - conditionals : if, elsif, unless (including postfix forms), ternary ?:
# - loops : while, until, for (including postfix forms)
# - case/rescue : each `when` clause, each `rescue` clause
# - boolean ops : &&, ||, and, or
# - safe nav : &. (each call may short-circuit, so it's a branch)
# - assignments : ||= and &&=
# - iter blocks : .each{}, .map{}, .select{} etc. (known Enumerable methods)
#
# No dependencies — uses only `ripper` from the Ruby stdlib.
#
# Usage:
# cyclomatic path/to/file.rb # score for the whole file
# cyclomatic path/to/file.rb --method foo # score for a single method
# echo "def foo; if x; 1; end; end" | cyclomatic
#
# Score interpretation:
# 1-4 low simple, easy to test
# 5-7 moderate manageable, consider splitting if growing
# 8-10 high hard to test exhaustively, refactor recommended
# 11+ very high strongly consider breaking the method up
require "ripper"
# AST nodes that introduce a new decision branch.
DECISION_NODES = %i[
if elsif unless if_mod unless_mod
while until while_mod until_mod
for when rescue ifop
and or
].freeze
# Subset of Enumerable/Array/Hash methods that imply iteration (mirrors RuboCop).
ITERATING_METHODS = %i[
all? any? collect collect_concat count cycle detect drop drop_while each
each_cons each_entry each_slice each_with_index each_with_object entries
filter filter_map find find_all find_index flat_map grep group_by inject
lazy map max max_by min min_by minmax minmax_by none? one? partition reduce
reject reverse_each select slice_after slice_before slice_when sort sort_by
sum take take_while tally to_h uniq zip
each_key each_pair each_value merge transform_keys transform_values
each_index keep_if map! reject! select!
].freeze
def iterating_block_method(sexp)
call = sexp[1]
call = call[1] if call.is_a?(Array) && call[0] == :method_add_arg
return nil unless call.is_a?(Array) && call[0] == :call
ident = call[3]
ident.is_a?(Array) && ident[0] == :@ident ? ident[1].to_sym : nil
end
def count_decisions(sexp)
return 0 unless sexp.is_a?(Array)
node_type = sexp[0]
count = 0
count += 1 if node_type == :binary && %i[&& ||].include?(sexp[2])
count += 1 if DECISION_NODES.include?(node_type)
count += 1 if node_type == :call && sexp[2].is_a?(Array) && sexp[2][1] == "&."
count += 1 if node_type == :opassign && sexp[2].is_a?(Array) && %w[||= &&=].include?(sexp[2][1])
count += 1 if node_type == :method_add_block && ITERATING_METHODS.include?(iterating_block_method(sexp))
sexp.each { |child| count += count_decisions(child) }
count
end
# Walk the full-file sexp and return the subtree for the named method.
def find_method_node(sexp, name)
return nil unless sexp.is_a?(Array)
if sexp[0] == :def
ident = sexp[1]
return sexp if ident.is_a?(Array) && ident[1].to_s == name.to_s
end
sexp.each do |child|
found = find_method_node(child, name)
return found if found
end
nil
end
def cyclomatic_complexity(sexp)
1 + count_decisions(sexp)
end
# --- Main ---
method_flag = ARGV.index("--method")
method_name = method_flag && ARGV[method_flag + 1]
file_arg = ARGV.find { |a| !a.start_with?("-") }
source = file_arg ? File.read(file_arg) : $stdin.read
sexp = Ripper.sexp(source)
abort "Parse error — invalid Ruby syntax." unless sexp
if method_name
node = find_method_node(sexp, method_name)
abort "Method '#{method_name}' not found." unless node
complexity = cyclomatic_complexity(node)
label = "'#{method_name}'"
else
complexity = cyclomatic_complexity(sexp)
label = file_arg ? File.basename(file_arg) : "input"
end
risk = case complexity
when 1..4 then "low"
when 5..7 then "moderate"
when 8..10 then "high"
else "very high"
end
puts "Cyclomatic complexity of #{label}: #{complexity} (#{risk})"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment