Skip to content
This repository has been archived by the owner on May 4, 2024. It is now read-only.

Commit

Permalink
Add complexity metric
Browse files Browse the repository at this point in the history
  • Loading branch information
floriandejonckheere committed Apr 19, 2024
1 parent b8e6794 commit 744f207
Show file tree
Hide file tree
Showing 7 changed files with 229 additions and 1 deletion.
66 changes: 66 additions & 0 deletions lib/mosaik/metrics/complexity.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# frozen_string_literal: true

module MOSAIK
module Metrics
##
# Cyclomatic complexity (T.J. McCabe, 1976)
#
class Complexity < Metric
def evaluate
# Total complexity
complexity = 0.0

# Iterate over each cluster
graph.clusters.each_value do |cluster|
# Find all vertices in the cluster
vertices_in_cluster = cluster.vertices

# Calculate complexity for the cluster
complexity_c = 0.0

# Iterate over all vertices in the cluster
vertices_in_cluster.each do |v|
# Resolve the constant name to a file
file = resolver.resolve_constant!(v.id)

# Parse file to extract complexities
complexities = Parser
.new
.parse(file)

# Calculate the complexity for the vertex
complexity_v = (complexities.values.sum / complexities.size).round(2)

# Store complexity value in the vertex
v.attributes[:complexity] = complexity_v

# Store complexity value in the cluster
complexity_c += complexity_v
end

# Store complexity value in the cluster
cluster.attributes[:complexity] = complexity_c

# Calculate complexity contribution from this cluster
complexity += complexity_c
end

# Store complexity value in the graph
graph.attributes[:complexity] = complexity

# Return the total complexity
complexity
end

private

def resolver
@resolver ||= Resolver.new(
options[:directory],
MOSAIK.configuration.load_paths,
MOSAIK.configuration.overrides,
)
end
end
end
end
30 changes: 30 additions & 0 deletions lib/mosaik/metrics/complexity/parser.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# frozen_string_literal: true

module MOSAIK
module Metrics
class Complexity
##
# Parser for Ruby code
#
class Parser
def parse(file)
debug "Parsing file: #{file}"

# Parse Abstract Syntax Tree
source = RuboCop::AST::ProcessedSource
.new(File.read(file), 3.3)

# Process AST to extract complexity values from methods
processor = Processor.new

source.ast.each_node do |node|
processor
.process(node)
end

processor.complexities
end
end
end
end
end
49 changes: 49 additions & 0 deletions lib/mosaik/metrics/complexity/processor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# frozen_string_literal: true

module MOSAIK
module Metrics
class Complexity
##
# Abstract Syntax Tree parser for Ruby code
#
class Processor < AST::Processor
attr_reader :complexities

def initialize
super

# Initialize complexities
@complexities = {}
end

# Instance methods
def on_def(node)
method_name = node.children[0].to_s

# Calculate complexity for the method
_, abc = RuboCop::Cop::Metrics::Utils::AbcSizeCalculator.calculate(node)

# Extract complexity from <A, B, C> triplet
complexity = abc.split(",")[1].to_f

# Store complexity for the method
complexities[method_name] = complexity
end

# Class methods
def on_defs(node)
method_name = "self.#{node.children[1]}"

# Calculate complexity for the method
_, abc = RuboCop::Cop::Metrics::Utils::AbcSizeCalculator.calculate(node)

# Extract complexity from <A, B, C> triplet
complexity = abc.split(",")[1].to_i

# Store complexity for the method
complexities[method_name] = complexity
end
end
end
end
end
2 changes: 1 addition & 1 deletion spec/mosaik/metrics/abc_size_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
# Cluster 1: 1.0 / 1 = 1.0
# Cluster 2: 15.04 / 2 + 5.0 / 3 = 9.19 (rounded)
#
# ABC size = 10.19
# ABC size = 10.19 (rounded)
#

it "sets the ABC size values for each vertex, cluster, and for the graph" do
Expand Down
37 changes: 37 additions & 0 deletions spec/mosaik/metrics/complexity/parser_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# frozen_string_literal: true
# typed: true

RSpec.describe MOSAIK::Metrics::Complexity::Parser do
subject(:parser) { described_class.new }

let(:file) { Tempfile.new(["ruby", ".rb"]) }

before do
File.write file, <<~RUBY
# frozen_string_literal: true
class PagesController < ApplicationController
def index
@posts = model.active.visible_by(current_user)
render "pages/index/page"
end
def search
@posts = model.active.visible_by(current_user).search(params[:q])
@posts = model.some_process(@posts, current_user)
@posts = model.another_process(@posts, current_user)
render "pages/search/page"
end
end
RUBY
end

it "parses a Ruby file" do
complexities = parser.parse(file)

expect(complexities).to eq "index" => 5.0,
"search" => 14.0
end
end
44 changes: 44 additions & 0 deletions spec/mosaik/metrics/complexity_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# frozen_string_literal: true
# typed: true

RSpec.describe MOSAIK::Metrics::Complexity do
subject(:metric) { build(:complexity_metric, options:, graph:) }

let(:options) { { directory: MOSAIK.root } }
let(:graph) { build(:graph) }

before do
v1 = graph.add_vertex("CoreExt::Object")
v2 = graph.add_vertex("MOSAIK::Logger")
v3 = graph.add_vertex("MOSAIK::Algorithm")

c1 = graph.add_cluster("cluster1")
c2 = graph.add_cluster("cluster2")

c1.add_vertex(v1)

c2.add_vertex(v2)
c2.add_vertex(v3)
end

# Cyclomatic complexity calculated by hand:
#
# Cluster 1: 1.0 / 1 = 1.0
# Cluster 2: 7.0 / 2 + 1.0 / 3 = 5.83 (rounded)
#
# Complexity = 6.83 (rounded)
#

it "sets the ABC size values for each vertex, cluster, and for the graph" do
metric.evaluate

expect(graph.find_vertex("CoreExt::Object").attributes[:complexity]).to eq 1.0
expect(graph.find_vertex("MOSAIK::Logger").attributes[:complexity]).to eq 5.5
expect(graph.find_vertex("MOSAIK::Algorithm").attributes[:complexity]).to eq 0.33

expect(graph.find_cluster("cluster1").attributes[:complexity]).to eq 1.0
expect(graph.find_cluster("cluster2").attributes[:complexity]).to eq 5.83

expect(graph.attributes[:complexity]).to eq 6.83
end
end
2 changes: 2 additions & 0 deletions spec/support/factories/metrics.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

factory :cohesion_metric, parent: :metric, class: "MOSAIK::Metrics::Cohesion"

factory :complexity_metric, parent: :metric, class: "MOSAIK::Metrics::Complexity"

factory :coupling_metric, parent: :metric, class: "MOSAIK::Metrics::Coupling"

factory :modularity_metric, parent: :metric, class: "MOSAIK::Metrics::Modularity"
Expand Down

0 comments on commit 744f207

Please sign in to comment.