Ruby Software Complexity Metrics (Part Two: Calculations — Pseudocode and Examples)

Cyclomatic Complexity, Perceived Complexity, and ABC Metric

Abhimanyu Singh
10 min readSep 29, 2019

The story has three parts:

Goal

In Part One: Prerequisites, we have identified the required nodes to calculate the cyclomatic complexity, perceived complexity, and, ABC metric. In this part, we will go through the definitions of complexity metrics and pseudocode for the implementation of the calculations. Also, some code examples to get the feel of how to calculate the values without building control flow graphs.

Cyclomatic Complexity

Cyclomatic Complexity or McCabe complexity is a quantitive measure of linearly independent paths through a method. The pseudocode for the calculation:

NODES = %i[if and or when for while until rescue].freezecyclomatic_complexity = 1for node in nodes
next unless NODES.include?(node.type)
# each decision point node contributes one
cyclomatic_complexity += 1
end

The cyclomatic complexity metric helps:

  • Determine the number of independent path executions.
  • Evaluate the risk associated.
  • Improve code coverage.
# Problem Statement
# Given two positive integers a and b.
# - Print 'best', when a is even and b is odd.
# - Print 'better', when a is odd or b is odd.
# - Print 'good', otherwise.
def qux_v1(a, b)
return :best if a.even? && b.odd?
return :better if a.odd? || b.odd?
:good
end

# def qux_v1(a, b)
# return :best
# if -- if node, decision points = 1
# a.even? -- if condition `a.even? && b.odd?`
# && -- and node, decision points = 2
# b.odd?
#
# return :better
# if -- if node, decision points = 3
# a.odd? -- if condition `a.odd? || b.odd?`
# || -- or node, decision points = 4
# b.odd?
#
# :good
# end
#
# cyclomatic complexity = 1 + decision points = 1 + 4 = 5
--------------------------------------------------------------------def qux_v2(a, b)
return :better if a.odd?
return :best if b.odd?
:good
end

# def qux_v2(a, b)
# return :better
# if -- if node, decision points = 1
# a.odd? -- send node
#
# return :best
# if -- if node, decision points = 2
# b.odd? -- send node
#
# :good
# end
#
# cyclomatic complexity = 1 + decision points = 1 + 2 = 3

Perceived Complexity

RuboCop produces a score for the code readability, the perceived complexity. The pseudocode for the calculation:

NODES = %i[if and or case for while until rescue].freezeperceived_complexity = 1for node in nodes
next unless NODES.include?(node.type)
perceived_complexity +=
case node.type
# when case node
when :case
if node.condition.present?
# when case condition is present
# score = 0.8 + 0.2 * the total number of when nodes
0.8 + 0.2 * node.when_nodes.size
else
# when no case condition then
# case...when is treated as if...elsif...else
# score = the total number of when nodes
node.when_nodes.size
end
# when `if` node
when :if
if else_branch?(node)
# when if node has an else branch
# if foo
# :foo
# elsif bar
# :bar
# else
# :baz
# end
#
# score = 2
2
else
# when if node has no else branch
# if foo
# :foo
# elsif bar
# :bar
# end
#
# score = 1
1
end
else
# All the other qualified nodes except for the if and case
# score = 1
1
end
end

Whenever it makes sense, the perceived complexity could be improved by preferring:

  • case…when over if…elsif…else.
  • if-modifier and unless-modifier over if…else and unless…else.
# Problem Statement
# Given a non-zero integer a.
# - Print 'positive', when a is positive
# - Print 'negative', otherwise
def qux_v1(a)
if a.positive?
:positive
else
:negative
end
end

# def qux_v1(a)
# if -- if node has else, score = 2
# a.positive? -- if condition `a.positive?`
# :positive -- if body `:positive`
# else
# :negative -- else body `:negative`
# end
# end
#
# perceived complexity = 1 + score = 1 + 2 = 3
# cyclomatic complexity = 2
--------------------------------------------------------------------def qux_v2(a)
return :positive if a.positive?
:negative
end

# def qux_v2(a)
# return :positive
# if -- if node has no else, score = 1
# a.positive? -- if condition `a.positive?`
#
# :negative
# end
#
# perceived complexity = 1 + score = 1 + 1 = 2
# cyclomatic complexity = 2
====================================================================# Problem Statement
# Given a positive integer a, describe divisibility by four:
# - Print 'best', when already divisible
# - Print 'better', when divisible after adding one
# - Print 'good', when divisible after adding two
# - Print 'bad', when divisible after adding three
def qux_v1(a)
if (a % 4).zero?
:best
elsif ((a + 1) % 4).zero?
:better
elsif ((a + 2) % 4).zero?
:good
else
:bad
end
end

# def qux_v1(a)
# if -- if node has else, score = 2
# (a % 4).zero? -- if condition `(a % 4).zero?`
# :best -- if body `:best`
# else
# if -- if node has no else, score = 3
# ((a + 1) % 4).zero? -- if condition `((a + 1) % 4).zero?`
# :better -- if body `:better`
# else
# if -- if node has no else, score = 4
# ((a + 2) % 4).zero? -- if condition `((a + 2) % 4).zero?`
# :good -- if body `:good`
# else
# :bad -- else body `:bad`
# end
# end
# end
# end
#
# perceived complexity = 1 + score = 1 + 4 = 5
# cyclomatic complexity = 4
--------------------------------------------------------------------def qux_v2(a)
case a % 4
when 1
:bad
when 2
:good
when 3
:better
else
:best
end
end

# def qux_v2(a)
# case -- case node
# a % 4 -- case condition `a % 4`, score = 0.8
# when 1 -- when `1`, score = 0.8 + 0.2 = 1.0
# :bad -- when body `:bad`
# when 2 -- when `2`, score = 1.0 + 0.2 = 1.2
# :good -- when body `:good`
# when 3 -- when `3`, score = 1.2 + 0.2 = 1.4
# :better -- when body `:better`
# else
# :best -- else body `:best`
# end
# end
#
# perceived complexity = 1 + score = 1 + 1.4 = 2.4
# cyclomatic complexity = 4

ABC Metric

ABC metric is a size metric, not a complexity metric, which describes three fundamental operations:

  • Assignment: An explicit transfer of data into variables.
  • Branch: An explicit out-of-scope forward branch, i.e., method invocation.
  • Condition: A boolean test, i.e., decision points and comparison operators.

Let A be the total number of assignments, B be the total number of branches, and, C be the total number of conditions, then the ABC metric is the magnitude of the vector <A, B, C>, i.e., sqrt(A² + B² + C²), where sqrt denotes square root. The pseudocode for the calculation:

ASSIGNMENT_NODES = %i[
lvasgn gvasgn ivasgn cvasgn casgn
masgn op-asgn and-asgn or-asgn
].freeze
METHOD_INVOCATION_NODES = %i[send csend].freeze
COMPARISON_OPERATORS = %i[== === != < <= > >=].freeze
DECISION_POINT_NODES = %i[
if and or when for
while until rescue
].freeze
assignments = 0
branches = 0
conditions = 0
for node in nodes
if ASSIGNMENT_NODES.include?(node.type)
# when assignment node
assignments += 1
elsif METHOD_INVOCATION_NODES.include?(node.type)
# when method invocation node
if COMPARISON_OPERATORS.include?(node.method)
# when method is comparison operator
conditions += 1
else
branches += 1
end
elsif DECISION_POINT_NODES.include?(node.type)
# when decision point node
conditions +=
if else_branch?(node)
# when if node has an else branch
2
else
# when if node has no else branch
1
end
end
end
abc_metric = (assignments**2 + branches**2 + conditions**2)**0.5

The ABC vectors are linear so, it makes it easy to compute the ABC metric for the modules, classes, and, project. The final ABC vector for the modules and classes is the sum of individual ABC vectors of the methods defined. Similarly, the final ABC vector for the entire project is the sum of individual ABC vectors of the modules and classes. Also, the ABC magnitude is non-linear; therefore, the ABC metric without ABC vector does not provide much value.

# Problem Statement
# Given three positive integers a, b, c. Let d be:
# - a**2 + b**2 + c**2, when all negative
# - a**3 + b**3 + c**3, when all positive
# - a**4 + b**4 + c**4, otherwise
# - Print d + d**2 + d**3 + d**4
def qux_v1(a, b, c)
d =
if a < 0 && b < 0 && c < 0
a**2 + b**2 + c**2
elsif a > 0 && b > 0 && c > 0
a**3 + b**3 + c**3
else
a**4 + b**4 + c**4
end
d + d**2 + d**3 + d**4
end

# def qux_v1(a, b, c)
# d =
# -- lvasgn node, assignments = 1
# if
# -- if node has else, conditions = 2
# a < 0 && b < 0 && c < 0
# -- if condition `a < 0 && b < 0 && c < 0`
# -- comparison `a < 0`, conditions = 3
# -- and node `&&`, conditions = 4
# -- comparison `b < 0`, conditions = 5
# -- and node `&&`, conditions = 6
# -- comparison `c < 0`, conditions = 7
#
# a**2 + b**2 + c**2
# -- if body `a**2 + b**2 + c**2`
# -- a.**(2) send node, branches = 1
# -- .+ send node, branches = 2
# -- (b.**(2)) send node, branches = 3
# -- .+ send node, branches = 4
# -- (c.**(2)) send node, branches = 5
# else
# if
# -- if node has no else, conditions = 8
# a > 0 && b > 0 && c > 0
# -- if condition `a > 0 && b > 0 && c > 0`
# -- comparison `a > 0`, conditions = 9
# -- and node `&&`, conditions = 10
# -- comparison `b > 0`, conditions = 11
# -- and node `&&`, conditions = 12
# -- comparison `c > 0`, conditions = 13
#
# a**3 + b**3 + c**3
# -- if body `a**3 + b**3 + c**3`
# -- a.**(3) send node, branches = 6
# -- .+ send node, branches = 7
# -- (b.**(3)) send node, branches = 8
# -- .+ send node, branches = 9
# -- (c.**(3)) send node, branches = 10
# else
# a**4 + b**4 + c**4
# -- else body `a**4 + b**4 + c**4`
# -- a.**(4) send node, branches = 11
# -- .+ send node, branches = 12
# -- (b.**(4)) send node, branches = 13
# -- .+ send node, branches = 14
# -- (c.**(4)) send node, branches = 15
# end
# end
#
# d + d**2 + d**3 + d**4
# -- expression `d + d**2 + d**3 + d**4`
# -- d
# -- .+ send node, branches = 16
# -- (d.**(2)) send node, branches = 17
# -- .+ send node, branches = 18
# -- (d.**(3)) send node, branches = 19
# -- .+ send node, branches = 20
# -- (d.**(4)) send node, branches = 21
# end
#
# ABC metric = <1, 21, 13> [24.72]
# cyclomatic complexity = 7
# perceived complexity = 8
--------------------------------------------------------------------def qux_v2(a, b, c)
m = [a, b, c]
d =
if m.all?(&:negative?)
m.sum { |e| e**2 }
elsif m.all?(&:positive?)
m.sum { |e| e**3 }
else
m.sum { |e| e**4 }
end
(1 + d**2) * (d + d**2)
end

# def qux_v2(a, b, c)
# m = [a, b, c]
# -- lvasgn node, assignments = 1
#
# d =
# -- lvasgn node, assignments = 2
# if
# -- if node has else, conditions = 2
# m.all?(&:negative?)
# -- if condition `m.all?(&:negative?)`
# -- send node, branches = 1
#
# m.sum { |e| e**2 }
# -- if body `m.sum { |e| e**2 }`
# -- m.sum do |e| send node, branches = 2
# -- e.**(2) send node, branches = 3
# -- end
# else
# if
# -- if node has no else, conditions = 3
# m.all?(&:positive?)
# -- if condition `m.all?(&:positive?)`
# -- send node, branches = 4
#
# m.sum { |e| e**3 }
# -- if body `m.sum { |e| e**3 }`
# -- m.sum do |e| send node, branches = 5
# -- e.**(3) send node, branches = 6
# -- end
# else
# m.sum { |e| e**4 }
# -- else body `m.sum { |e| e**4 }`
# -- m.sum do |e| send node, branches = 7
# -- e.**(4) send node, branches = 8
# -- end
# end
# end
#
# (1 + d**2) * (d + d**2)
# -- expression `(1 + d**2) * (d + d**2)`
# -- (1
# -- .+ send node, branches = 9
# -- (d.**(2))) send node, branches = 10
# -- .*(d send node, branches = 11
# -- .+ send node, branches = 12
# -- (d.**(2))) send node, branches = 13
# end
#
# ABC metric = <2, 13, 3> = 13.49
# cyclomatic complexity = 3
# perceived complexity = 4
--------------------------------------------------------------------def qux_v3(a, b, c)
m = [a, b, c]
d = m.sum { |e| e**qux_v3_helper(*m.minmax) }
(1..4).sum { |n| d**n }
end
def qux_v3_helper(p, q)
return 2 if q.negative?
return 3 if p.positive?
4
end

# def qux_v3(a, b, c)
# m = [a, b, c]
# -- lvasgn node, assignments = 1
#
# d = m.sum { |e| e**qux_v3_helper(*m.minmax) }
# -- lvasgn node, assignments = 2
# -- m.sum do |e| send node, branches = 1
# -- e.**( send node, branches = 2
# -- qux_v3_helper( send node, branches = 3
# -- *m.minmax send node, branches = 4
# -- )
# -- )
#
# (1..4).sum { |n| d**n }
# -- (1..4).sum do |n| send node, branches = 5
# -- d.**(n) send node, branches = 6
# -- end
# end
#
# ABC metric = <2, 6, 0> = 6.32
# cyclomatic complexity = 1
# perceived complexity = 1
#
# def qux_v3_helper(p, q)
# return 2
# if
# -- if node has no else, conditions = 1
# q.negative?
# -- if condition `q.negative?`
# -- send node, branches = 1
#
# return 3
# if
# -- if node has no else, conditions = 2
# p.positive?
# -- if condition `q.positive?`
# -- send node, branches = 2
#
# 4
# end
#
# ABC metric = <0, 2, 2> = 2.83
# cyclomatic complexity = 3
# perceived complexity = 3
Total ABC metric = <2, 6, 0> + <0, 2, 2> = <2, 8, 2> = 8.49

Summary

We solved four problems with at least two different solutions to demonstrate complexity calculations and improvements:

  • For cyclomatic complexity, the improved version reduces the value from 5 to 3
  • For perceived complexity, the improved version reduces the value from 5 to 2.4
  • For the ABC metric, the improved version reduces the value from <1, 21, 13> [24.72] to <2, 8, 2> [8.49]

As we understand calculation implementation, we continue to the third and last part — the most interesting one, to mathematically analyze the relationships amongst the complexity metrics and producing a logical explanation for what maximum configured values make sense for the RuboCop analysis.

📝 Read this story later in Journal.

👩‍💻 Wake up every Sunday morning to the week’s most noteworthy stories in Tech waiting in your inbox. Read the Noteworthy in Tech newsletter.

--

--