solidarity 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/exe/solidarity +22 -0
- data/lib/solidarity/ast_processor.rb +64 -0
- data/lib/solidarity/graph.rb +68 -0
- data/lib/solidarity/railroady_runner.rb +52 -0
- data/lib/solidarity/reporter.rb +28 -0
- data/lib/solidarity/solid_evaluator.rb +74 -0
- data/lib/solidarity/version.rb +5 -0
- data/lib/solidarity.rb +11 -0
- data/sig/solidarity.rbs +4 -0
- metadata +86 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 27543cb4c591b8b62bbf07cbf83ad9165bb0e9a1650a0e29bb265f986fdba5cd
|
|
4
|
+
data.tar.gz: c1a8836ed00bb416303ed6a43a1ce26006483052e4d936c12359c5dd216842c3
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: c9304bfa105687a0ae8354e824a06978c5f542d7301de9d065d58dcacf1d1d9998e2370c70f8b54450eacf2d3ad79f4255f4d115b1def72a61d8f3fb2471636f
|
|
7
|
+
data.tar.gz: 308e3516157366177bdc0b449a5e9d2232563c29acf45709b58dedad5a496056e36974481fe7c6018aeee09abd9a28473f7942fe24ee4e87f577123992571898
|
data/exe/solidarity
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require "solidarity"
|
|
4
|
+
|
|
5
|
+
# TODO: Add option parsing (e.g., using OptionParser)
|
|
6
|
+
|
|
7
|
+
project_path = ARGV[0]
|
|
8
|
+
|
|
9
|
+
unless project_path
|
|
10
|
+
warn "Usage: #{File.basename($PROGRAM_NAME)} <path_to_ruby_project>"
|
|
11
|
+
exit 1
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
begin
|
|
15
|
+
graph = Solidarity::RailRoadyRunner.run(project_path)
|
|
16
|
+
solid_results = Solidarity::SolidEvaluator.new(graph).evaluate_all
|
|
17
|
+
report = Solidarity::Reporter.generate_report(solid_results)
|
|
18
|
+
puts report
|
|
19
|
+
rescue => e
|
|
20
|
+
warn "Error: #{e.message}"
|
|
21
|
+
exit 1
|
|
22
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'parser/current'
|
|
4
|
+
|
|
5
|
+
module Solidarity
|
|
6
|
+
class AstProcessor
|
|
7
|
+
attr_reader :classes, :modules, :inheritances, :includes
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@classes = []
|
|
11
|
+
@modules = []
|
|
12
|
+
@inheritances = [] # Format: [['ChildClass', 'ParentClass']]
|
|
13
|
+
@includes = [] # Format: [['ClassOrModule', 'IncludedModule']]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def process_file(file_path)
|
|
17
|
+
content = File.read(file_path)
|
|
18
|
+
ast = Parser::CurrentRuby.parse(content)
|
|
19
|
+
traverse_ast(ast, nil) # Pass initial context as nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def traverse_ast(node, current_context)
|
|
25
|
+
return unless node.is_a?(Parser::AST::Node)
|
|
26
|
+
|
|
27
|
+
new_context = current_context
|
|
28
|
+
|
|
29
|
+
case node.type
|
|
30
|
+
when :class
|
|
31
|
+
class_name = extract_const_name(node.children[0])
|
|
32
|
+
superclass_name = extract_const_name(node.children[1])
|
|
33
|
+
@classes << class_name
|
|
34
|
+
@inheritances << [class_name, superclass_name] if superclass_name
|
|
35
|
+
new_context = class_name
|
|
36
|
+
when :module
|
|
37
|
+
module_name = extract_const_name(node.children[0])
|
|
38
|
+
@modules << module_name
|
|
39
|
+
new_context = module_name
|
|
40
|
+
when :send
|
|
41
|
+
if node.children[1] == :include
|
|
42
|
+
included_module = extract_const_name(node.children[2])
|
|
43
|
+
@includes << [current_context, included_module] if current_context && included_module
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
node.children.each { |child| traverse_ast(child, new_context) }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def extract_const_name(node)
|
|
51
|
+
return unless node.is_a?(Parser::AST::Node)
|
|
52
|
+
|
|
53
|
+
case node.type
|
|
54
|
+
when :const
|
|
55
|
+
parent = extract_const_name(node.children[0])
|
|
56
|
+
[parent, node.children[1]].compact.join('::')
|
|
57
|
+
when :cbase
|
|
58
|
+
'' # Absolute constant, e.g., ::MyClass
|
|
59
|
+
else
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
module Solidarity
|
|
2
|
+
class Graph
|
|
3
|
+
attr_reader :nodes, :edges
|
|
4
|
+
|
|
5
|
+
def initialize(ast_data)
|
|
6
|
+
@nodes = []
|
|
7
|
+
@edges = []
|
|
8
|
+
|
|
9
|
+
initialize_from_ast_data(ast_data)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.from_ast_data(ast_data)
|
|
13
|
+
new(ast_data)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def find_node(name)
|
|
17
|
+
@nodes.find { |node| node.name == name }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def outgoing_edges(node_name)
|
|
21
|
+
@edges.select { |edge| edge.source == node_name }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def incoming_edges(node_name)
|
|
25
|
+
@edges.select { |edge| edge.target == node_name }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def initialize_from_ast_data(ast_data)
|
|
31
|
+
ast_data[:classes].each do |class_name|
|
|
32
|
+
@nodes << Node.new(name: class_name, label: class_name, type: 'class')
|
|
33
|
+
end
|
|
34
|
+
ast_data[:modules].each do |module_name|
|
|
35
|
+
@nodes << Node.new(name: module_name, label: module_name, type: 'module')
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
ast_data[:inheritances].each do |child, parent|
|
|
39
|
+
@edges << Edge.new(source: parent, target: child, label: '', type: 'is-a')
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
ast_data[:includes].each do |includer, included|
|
|
43
|
+
@edges << Edge.new(source: includer, target: included, label: '', type: 'includes')
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class Node
|
|
48
|
+
attr_reader :name, :label, :type
|
|
49
|
+
|
|
50
|
+
def initialize(data)
|
|
51
|
+
@name = data[:name]
|
|
52
|
+
@label = data[:label]
|
|
53
|
+
@type = data[:type]
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
class Edge
|
|
58
|
+
attr_reader :source, :target, :label, :type
|
|
59
|
+
|
|
60
|
+
def initialize(data)
|
|
61
|
+
@source = data[:source]
|
|
62
|
+
@target = data[:target]
|
|
63
|
+
@label = data[:label]
|
|
64
|
+
@type = data[:type]
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
require 'fileutils'
|
|
2
|
+
require 'parser/current'
|
|
3
|
+
require_relative 'ast_processor' # Add this line
|
|
4
|
+
|
|
5
|
+
module Solidarity
|
|
6
|
+
class RailRoadyRunner
|
|
7
|
+
def self.run(project_path)
|
|
8
|
+
unless File.directory?(project_path)
|
|
9
|
+
raise ArgumentError, "Project path '#{project_path}' is not a valid directory."
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Remove Railroady's Rails-specific environment loading
|
|
13
|
+
# options = OptionsStruct.new(root: project_path, verbose: false, output: nil)
|
|
14
|
+
# diagram = ModelsDiagram.new(options)
|
|
15
|
+
# diagram.process
|
|
16
|
+
# diagram.generate
|
|
17
|
+
# diagram.graph
|
|
18
|
+
|
|
19
|
+
ruby_files = Dir.glob(File.join(project_path, "**", "*.rb"))
|
|
20
|
+
ast_data = process_ruby_files_with_ast(ruby_files)
|
|
21
|
+
Solidarity::Graph.from_ast_data(ast_data)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def self.process_ruby_files_with_ast(ruby_files)
|
|
27
|
+
processor = AstProcessor.new
|
|
28
|
+
ruby_files.each do |file_path|
|
|
29
|
+
processor.process_file(file_path)
|
|
30
|
+
end
|
|
31
|
+
{ classes: processor.classes, modules: processor.modules, inheritances: processor.inheritances, includes: processor.includes }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.add_nodes_from_ast(graph, ast_data)
|
|
35
|
+
ast_data[:classes].each do |class_name|
|
|
36
|
+
graph.add_node(['class', class_name])
|
|
37
|
+
end
|
|
38
|
+
ast_data[:modules].each do |module_name|
|
|
39
|
+
graph.add_node(['module', module_name])
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.add_edges_from_ast(graph, ast_data)
|
|
44
|
+
ast_data[:inheritances].each do |child, parent|
|
|
45
|
+
graph.add_edge(['is-a', parent, child]) unless parent.empty? # Railroady expects parent then child for 'is-a'
|
|
46
|
+
end
|
|
47
|
+
ast_data[:includes].each do |includer, included|
|
|
48
|
+
graph.add_edge(['includes', includer, included])
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module Solidarity
|
|
2
|
+
class Reporter
|
|
3
|
+
def self.generate_report(solid_results)
|
|
4
|
+
report = "# Ruby SOLID Principles Analysis Report\n\n"
|
|
5
|
+
|
|
6
|
+
solid_results.each do |principle, result|
|
|
7
|
+
report += "## #{principle.to_s.upcase} - #{full_principle_name(principle)}\n"
|
|
8
|
+
report += "Score: #{result[:score].round(2)}/100\n"
|
|
9
|
+
report += "Details: #{result[:details]}\n\n"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
report
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def self.full_principle_name(principle_symbol)
|
|
18
|
+
case principle_symbol
|
|
19
|
+
when :srp then "Single Responsibility Principle"
|
|
20
|
+
when :ocp then "Open/Closed Principle"
|
|
21
|
+
when :lsp then "Liskov Substitution Principle"
|
|
22
|
+
when :isp then "Interface Segregation Principle"
|
|
23
|
+
when :dip then "Dependency Inversion Principle"
|
|
24
|
+
else "Unknown Principle"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
module Solidarity
|
|
2
|
+
class SolidEvaluator
|
|
3
|
+
def initialize(graph)
|
|
4
|
+
@graph = graph
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def evaluate_srp
|
|
8
|
+
# Single Responsibility Principle (SRP)
|
|
9
|
+
# Heuristic: High outgoing edges might indicate multiple responsibilities.
|
|
10
|
+
# For a more accurate measure, we'd need to analyze method calls/dependencies within each class.
|
|
11
|
+
srp_scores = {}
|
|
12
|
+
@graph.nodes.each do |node|
|
|
13
|
+
outgoing_count = @graph.outgoing_edges(node.name).count
|
|
14
|
+
srp_scores[node.name] = outgoing_count < 5 ? 100 : (100 - (outgoing_count - 5) * 10).clamp(0, 100)
|
|
15
|
+
end
|
|
16
|
+
{ score: srp_scores.values.sum / srp_scores.size.to_f, details: srp_scores }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def evaluate_ocp
|
|
20
|
+
# Open/Closed Principle (OCP)
|
|
21
|
+
# Heuristic: Presence of inheritance/module inclusions. Hard to measure purely from dependency graph.
|
|
22
|
+
# A higher number of classes participating in inheritance/module inclusion might suggest better OCP.
|
|
23
|
+
ocp_score = 0
|
|
24
|
+
inheriting_nodes = @graph.edges.count { |edge| edge.type == :inheritance || edge.type == :includes }
|
|
25
|
+
ocp_score = (inheriting_nodes > @graph.nodes.count / 4) ? 100 : (inheriting_nodes * 100.0 / (@graph.nodes.count / 4)).clamp(0, 100)
|
|
26
|
+
{ score: ocp_score, details: "Number of inheritance/inclusion relationships: #{inheriting_nodes}" }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def evaluate_lsp
|
|
30
|
+
# Liskov Substitution Principle (LSP)
|
|
31
|
+
# Very hard to evaluate statically from a dependency graph. Requires behavioral analysis.
|
|
32
|
+
# Placeholder: Assume a perfectly flat hierarchy (no deep inheritance) is better, but this is a very weak heuristic.
|
|
33
|
+
{ score: 70, details: "LSP is difficult to assess statically from a dependency graph." }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def evaluate_isp
|
|
37
|
+
# Interface Segregation Principle (ISP)
|
|
38
|
+
# Heuristic: Could look for modules included by many classes but with few used methods by each class.
|
|
39
|
+
# Requires method-level analysis, which is not available from railroady's DOT output.
|
|
40
|
+
{ score: 70, details: "ISP is difficult to assess without method-level dependency analysis." }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def evaluate_dip
|
|
44
|
+
# Dependency Inversion Principle (DIP)
|
|
45
|
+
# Heuristic: High-level modules should depend on abstractions, not concretions.
|
|
46
|
+
# We can look for dependencies from concrete classes to other concrete classes as a potential violation.
|
|
47
|
+
# This requires distinguishing between abstract/concrete classes, which railroady doesn't explicitly provide.
|
|
48
|
+
dip_score = 100
|
|
49
|
+
# For now, a very simplistic view: count edges between concrete classes.
|
|
50
|
+
# This needs significant refinement to be meaningful.
|
|
51
|
+
# concrete_to_concrete_dependencies = 0 # Placeholder
|
|
52
|
+
# @graph.edges.each do |edge|
|
|
53
|
+
# source_node = @graph.find_node(edge.source)
|
|
54
|
+
# target_node = @graph.find_node(edge.target)
|
|
55
|
+
# if source_node&.type == :class && target_node&.type == :class # Assuming :class is concrete
|
|
56
|
+
# concrete_to_concrete_dependencies += 1
|
|
57
|
+
# end
|
|
58
|
+
# end
|
|
59
|
+
# dip_score = (100 - concrete_to_concrete_dependencies * 5).clamp(0, 100)
|
|
60
|
+
|
|
61
|
+
{ score: dip_score, details: "DIP assessment requires distinguishing abstract from concrete classes." }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def evaluate_all
|
|
65
|
+
{
|
|
66
|
+
srp: evaluate_srp,
|
|
67
|
+
ocp: evaluate_ocp,
|
|
68
|
+
lsp: evaluate_lsp,
|
|
69
|
+
isp: evaluate_isp,
|
|
70
|
+
dip: evaluate_dip
|
|
71
|
+
}
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
data/lib/solidarity.rb
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "solidarity/version"
|
|
4
|
+
require_relative "solidarity/railroady_runner"
|
|
5
|
+
require_relative "solidarity/graph"
|
|
6
|
+
require_relative "solidarity/solid_evaluator"
|
|
7
|
+
require_relative "solidarity/reporter"
|
|
8
|
+
|
|
9
|
+
module Solidarity
|
|
10
|
+
class Error < StandardError; end
|
|
11
|
+
end
|
data/sig/solidarity.rbs
ADDED
metadata
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: solidarity
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Your Name
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2025-09-21 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: parser
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: 3.3.0.2
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: 3.3.0.2
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: minitest
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '5.0'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '5.0'
|
|
41
|
+
description: This gem uses RailRoady to generate class diagrams and then analyzes
|
|
42
|
+
them to provide an assessment of SOLID principles.
|
|
43
|
+
email:
|
|
44
|
+
- your_email@example.com
|
|
45
|
+
executables:
|
|
46
|
+
- solidarity
|
|
47
|
+
extensions: []
|
|
48
|
+
extra_rdoc_files: []
|
|
49
|
+
files:
|
|
50
|
+
- exe/solidarity
|
|
51
|
+
- lib/solidarity.rb
|
|
52
|
+
- lib/solidarity/ast_processor.rb
|
|
53
|
+
- lib/solidarity/graph.rb
|
|
54
|
+
- lib/solidarity/railroady_runner.rb
|
|
55
|
+
- lib/solidarity/reporter.rb
|
|
56
|
+
- lib/solidarity/solid_evaluator.rb
|
|
57
|
+
- lib/solidarity/version.rb
|
|
58
|
+
- sig/solidarity.rbs
|
|
59
|
+
homepage: https://github.com/solidarity-gem/solidarity
|
|
60
|
+
licenses:
|
|
61
|
+
- MIT
|
|
62
|
+
metadata:
|
|
63
|
+
allowed_push_host: https://rubygems.org
|
|
64
|
+
homepage_uri: https://github.com/solidarity-gem/solidarity
|
|
65
|
+
source_code_uri: https://github.com/solidarity-gem/solidarity
|
|
66
|
+
changelog_uri: https://github.com/solidarity-gem/solidarity/blob/main/CHANGELOG.md
|
|
67
|
+
post_install_message:
|
|
68
|
+
rdoc_options: []
|
|
69
|
+
require_paths:
|
|
70
|
+
- lib
|
|
71
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: 2.4.0
|
|
76
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - ">="
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '0'
|
|
81
|
+
requirements: []
|
|
82
|
+
rubygems_version: 3.4.19
|
|
83
|
+
signing_key:
|
|
84
|
+
specification_version: 4
|
|
85
|
+
summary: A Ruby gem for analyzing Ruby projects for SOLID principles adherence.
|
|
86
|
+
test_files: []
|