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 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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Solidarity
4
+ VERSION = "1.0.0"
5
+ 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
@@ -0,0 +1,4 @@
1
+ module Solidarity
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
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: []