class-metrix 0.1.2

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.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +88 -0
  4. data/.tool-versions +1 -0
  5. data/CHANGELOG.md +41 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +417 -0
  8. data/RELEASE_GUIDE.md +158 -0
  9. data/Rakefile +12 -0
  10. data/examples/README.md +155 -0
  11. data/examples/advanced/error_handling.rb +199 -0
  12. data/examples/advanced/hash_expansion.rb +180 -0
  13. data/examples/basic/01_simple_constants.rb +56 -0
  14. data/examples/basic/02_simple_methods.rb +99 -0
  15. data/examples/basic/03_multi_type_extraction.rb +116 -0
  16. data/examples/components/configurable_reports.rb +201 -0
  17. data/examples/csv_output_demo.rb +237 -0
  18. data/examples/real_world/microservices_audit.rb +312 -0
  19. data/lib/class_metrix/extractor.rb +121 -0
  20. data/lib/class_metrix/extractors/constants_extractor.rb +87 -0
  21. data/lib/class_metrix/extractors/methods_extractor.rb +87 -0
  22. data/lib/class_metrix/extractors/multi_type_extractor.rb +66 -0
  23. data/lib/class_metrix/formatters/base/base_component.rb +62 -0
  24. data/lib/class_metrix/formatters/base/base_formatter.rb +93 -0
  25. data/lib/class_metrix/formatters/components/footer_component.rb +67 -0
  26. data/lib/class_metrix/formatters/components/generic_header_component.rb +87 -0
  27. data/lib/class_metrix/formatters/components/header_component.rb +92 -0
  28. data/lib/class_metrix/formatters/components/missing_behaviors_component.rb +140 -0
  29. data/lib/class_metrix/formatters/components/table_component.rb +268 -0
  30. data/lib/class_metrix/formatters/csv_formatter.rb +98 -0
  31. data/lib/class_metrix/formatters/markdown_formatter.rb +184 -0
  32. data/lib/class_metrix/formatters/shared/csv_table_builder.rb +21 -0
  33. data/lib/class_metrix/formatters/shared/markdown_table_builder.rb +97 -0
  34. data/lib/class_metrix/formatters/shared/table_builder.rb +267 -0
  35. data/lib/class_metrix/formatters/shared/value_processor.rb +78 -0
  36. data/lib/class_metrix/processors/value_processor.rb +40 -0
  37. data/lib/class_metrix/utils/class_resolver.rb +20 -0
  38. data/lib/class_metrix/version.rb +5 -0
  39. data/lib/class_metrix.rb +12 -0
  40. data/sig/class/metrix.rbs +6 -0
  41. metadata +118 -0
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../shared/value_processor"
4
+
5
+ module ClassMetrix
6
+ module Formatters
7
+ module Base
8
+ class BaseComponent
9
+ attr_reader :data, :options, :value_processor
10
+
11
+ def initialize(data, options = {})
12
+ @data = data
13
+ @options = options
14
+ @value_processor = Shared::ValueProcessor
15
+ end
16
+
17
+ def generate
18
+ raise NotImplementedError, "Subclasses must implement #generate method"
19
+ end
20
+
21
+ protected
22
+
23
+ def has_type_column?
24
+ @data[:headers].first == "Type"
25
+ end
26
+
27
+ def behavior_column_index
28
+ has_type_column? ? 1 : 0
29
+ end
30
+
31
+ def value_start_index
32
+ has_type_column? ? 2 : 1
33
+ end
34
+
35
+ def class_headers
36
+ if has_type_column?
37
+ @data[:headers][2..] # Skip "Type" and "Behavior"
38
+ else
39
+ @data[:headers][1..] # Skip first column (behavior name)
40
+ end
41
+ end
42
+
43
+ def extraction_types_description
44
+ return "" if @options[:extraction_types].empty?
45
+
46
+ @options[:extraction_types].map do |type|
47
+ case type
48
+ when :constants then "Constants"
49
+ when :class_methods then "Class Methods"
50
+ when :module_methods then "Module Methods"
51
+ else type.to_s.split("_").map(&:capitalize).join(" ")
52
+ end
53
+ end.join(", ")
54
+ end
55
+
56
+ def format_timestamp
57
+ Time.now.strftime("%Y-%m-%d %H:%M:%S %Z")
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../shared/value_processor"
4
+
5
+ module ClassMetrix
6
+ module Formatters
7
+ module Base
8
+ class BaseFormatter
9
+ attr_reader :data, :expand_hashes, :options
10
+
11
+ def initialize(data, expand_hashes = false, options = {})
12
+ @data = data
13
+ @expand_hashes = expand_hashes
14
+ @options = default_options.merge(options)
15
+ @value_processor = Shared::ValueProcessor
16
+ end
17
+
18
+ def format
19
+ raise NotImplementedError, "Subclasses must implement #format method"
20
+ end
21
+
22
+ protected
23
+
24
+ def default_options
25
+ {
26
+ # Common options for all formatters
27
+ title: nil,
28
+ show_metadata: true,
29
+ extraction_types: []
30
+ }
31
+ end
32
+
33
+ def has_type_column?
34
+ @data[:headers].first == "Type"
35
+ end
36
+
37
+ def behavior_column_index
38
+ has_type_column? ? 1 : 0
39
+ end
40
+
41
+ def value_start_index
42
+ has_type_column? ? 2 : 1
43
+ end
44
+
45
+ def class_headers
46
+ if has_type_column?
47
+ @data[:headers][2..] # Skip "Type" and "Behavior"
48
+ else
49
+ @data[:headers][1..] # Skip first column (behavior name)
50
+ end
51
+ end
52
+
53
+ def collect_all_hash_keys(rows, _headers)
54
+ value_start_idx = value_start_index
55
+ all_keys = {} # behavior_name => Set of keys
56
+
57
+ rows.each do |row|
58
+ behavior_name = row[behavior_column_index]
59
+ values = row[value_start_idx..]
60
+
61
+ values.each do |value|
62
+ if value.is_a?(Hash)
63
+ all_keys[behavior_name] ||= Set.new
64
+ all_keys[behavior_name].merge(value.keys.map(&:to_s))
65
+ end
66
+ end
67
+ end
68
+
69
+ all_keys
70
+ end
71
+
72
+ def process_value(value, format = :markdown)
73
+ case format
74
+ when :markdown
75
+ @value_processor.process_for_markdown(value)
76
+ when :csv
77
+ @value_processor.process_for_csv(value, @options)
78
+ else
79
+ raise ArgumentError, "Unknown format: #{format}"
80
+ end
81
+ end
82
+
83
+ def safe_hash_lookup(hash, key)
84
+ @value_processor.safe_hash_lookup(hash, key)
85
+ end
86
+
87
+ def has_hash_key?(hash, key)
88
+ @value_processor.has_hash_key?(hash, key)
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClassMetrix
4
+ module Formatters
5
+ module Components
6
+ class FooterComponent
7
+ def initialize(options = {})
8
+ @options = options
9
+ @show_footer = options.fetch(:show_footer, true)
10
+ @custom_footer = options[:custom_footer]
11
+ @show_timestamp = options.fetch(:show_timestamp, false)
12
+ @show_separator = options.fetch(:show_separator, true)
13
+ @footer_style = options.fetch(:footer_style, :default) # :default, :minimal, :detailed
14
+ end
15
+
16
+ def generate
17
+ return [] unless @show_footer
18
+
19
+ output = []
20
+
21
+ # Add separator line
22
+ output << "---" if @show_separator
23
+
24
+ case @footer_style
25
+ when :minimal
26
+ output << "*Generated by ClassMetrix*"
27
+ when :detailed
28
+ output.concat(generate_detailed_footer)
29
+ else
30
+ output.concat(generate_default_footer)
31
+ end
32
+
33
+ output
34
+ end
35
+
36
+ private
37
+
38
+ def generate_default_footer
39
+ output = []
40
+
41
+ output << (@custom_footer || "*Report generated by ClassMetrix gem*")
42
+
43
+ if @show_timestamp
44
+ output << ""
45
+ output << "*Generated at: #{Time.now.strftime("%Y-%m-%d %H:%M:%S %Z")}*"
46
+ end
47
+
48
+ output
49
+ end
50
+
51
+ def generate_detailed_footer
52
+ output = []
53
+
54
+ output << "## Report Information"
55
+ output << ""
56
+ output << "- **Generated by**: [ClassMetrix gem](https://github.com/your-username/class-metrix)"
57
+ output << "- **Generated at**: #{Time.now.strftime("%Y-%m-%d %H:%M:%S %Z")}"
58
+ output << "- **Ruby version**: #{RUBY_VERSION}"
59
+
60
+ output << "- **Note**: #{@custom_footer}" if @custom_footer
61
+
62
+ output
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base/base_component"
4
+
5
+ module ClassMetrix
6
+ module Formatters
7
+ module Components
8
+ class GenericHeaderComponent < Base::BaseComponent
9
+ def initialize(data, options = {})
10
+ super
11
+ @format = options.fetch(:format, :markdown)
12
+ @title = options[:title]
13
+ @extraction_types = options[:extraction_types] || []
14
+ @show_metadata = options.fetch(:show_metadata, true)
15
+ end
16
+
17
+ def generate
18
+ return [] unless @show_metadata
19
+
20
+ case @format
21
+ when :markdown
22
+ generate_markdown_header
23
+ when :csv
24
+ generate_csv_header
25
+ else
26
+ raise ArgumentError, "Unknown format: #{@format}"
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def generate_markdown_header
33
+ output = []
34
+
35
+ # Add title
36
+ if @title
37
+ output << "# #{@title}"
38
+ output << ""
39
+ end
40
+
41
+ # Add classes analyzed
42
+ output << "## Classes Analyzed"
43
+ output << ""
44
+ class_headers.each do |class_name|
45
+ output << "- **#{class_name}**"
46
+ end
47
+ output << ""
48
+
49
+ # Add extraction types
50
+ unless @extraction_types.empty?
51
+ output << "## Extraction Types"
52
+ output << ""
53
+ output << extraction_types_description
54
+ output << ""
55
+ end
56
+
57
+ output
58
+ end
59
+
60
+ def generate_csv_header
61
+ output = []
62
+ comment_char = @options.fetch(:comment_char, "#")
63
+
64
+ # Add title as comment
65
+ if @title
66
+ output << "#{comment_char} #{@title}"
67
+ else
68
+ extraction_label = @extraction_types.empty? ? "Class Analysis" : @extraction_types.map(&:to_s).map(&:capitalize).join(" and ")
69
+ output << "#{comment_char} #{extraction_label} Report"
70
+ end
71
+
72
+ # Add classes analyzed
73
+ output << "#{comment_char} Classes: #{class_headers.join(", ")}"
74
+
75
+ # Add extraction types
76
+ output << "#{comment_char} Extraction Types: #{extraction_types_description}" unless @extraction_types.empty?
77
+
78
+ # Add generation timestamp
79
+ output << "#{comment_char} Generated: #{format_timestamp}"
80
+ output << comment_char.to_s # Empty comment line for separation
81
+
82
+ output
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClassMetrix
4
+ module Formatters
5
+ module Components
6
+ class HeaderComponent
7
+ def initialize(data, options = {})
8
+ @data = data
9
+ @options = options
10
+ @title = options[:title]
11
+ @extraction_types = options[:extraction_types] || []
12
+ @show_metadata = options.fetch(:show_metadata, true)
13
+ @show_classes = options.fetch(:show_classes, true)
14
+ @show_extraction_info = options.fetch(:show_extraction_info, true)
15
+ end
16
+
17
+ def generate
18
+ output = []
19
+
20
+ # Add title
21
+ output.concat(generate_title) if @title || @show_metadata
22
+
23
+ # Add classes being analyzed
24
+ output.concat(generate_classes_section) if @show_classes
25
+
26
+ # Add extraction types info
27
+ output.concat(generate_extraction_info) if @show_extraction_info && !@extraction_types.empty?
28
+
29
+ output
30
+ end
31
+
32
+ private
33
+
34
+ def generate_title
35
+ output = []
36
+
37
+ if @title
38
+ output << "# #{@title}"
39
+ else
40
+ extraction_label = @extraction_types.empty? ? "Class Analysis" : @extraction_types.map(&:to_s).map(&:capitalize).join(" and ")
41
+ output << "# #{extraction_label} Report"
42
+ end
43
+
44
+ output << ""
45
+ output
46
+ end
47
+
48
+ def generate_classes_section
49
+ output = []
50
+
51
+ has_type_column = @data[:headers].first == "Type"
52
+ class_headers = if has_type_column
53
+ @data[:headers][2..] # Skip "Type" and "Behavior"
54
+ else
55
+ @data[:headers][1..] # Skip first column (behavior name)
56
+ end
57
+
58
+ output << "## Classes Analyzed"
59
+ output << ""
60
+ class_headers.each do |class_name|
61
+ output << "- **#{class_name}**"
62
+ end
63
+ output << ""
64
+
65
+ output
66
+ end
67
+
68
+ def generate_extraction_info
69
+ output = []
70
+
71
+ output << "## Extraction Types"
72
+ output << ""
73
+ @extraction_types.each do |type|
74
+ output << case type
75
+ when :constants
76
+ "- **Constants**: Class constants and their values"
77
+ when :class_methods
78
+ "- **Class Methods**: Class method results and return values"
79
+ when :module_methods
80
+ "- **Module Methods**: Methods from included modules"
81
+ else
82
+ "- **#{type.to_s.split("_").map(&:capitalize).join(" ")}**"
83
+ end
84
+ end
85
+ output << ""
86
+
87
+ output
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClassMetrix
4
+ module Formatters
5
+ module Components
6
+ class MissingBehaviorsComponent
7
+ def initialize(data, options = {})
8
+ @data = data
9
+ @options = options
10
+ @show_missing_summary = options.fetch(:show_missing_summary, false)
11
+ @summary_style = options.fetch(:summary_style, :grouped) # :grouped, :flat, :detailed
12
+ @missing_behaviors = {}
13
+ end
14
+
15
+ def generate
16
+ return [] unless @show_missing_summary
17
+
18
+ track_missing_behaviors
19
+ generate_missing_behaviors_summary
20
+ end
21
+
22
+ private
23
+
24
+ def track_missing_behaviors
25
+ has_type_column = @data[:headers].first == "Type"
26
+ class_headers = if has_type_column
27
+ @data[:headers][2..]
28
+ else
29
+ @data[:headers][1..]
30
+ end
31
+
32
+ # Initialize missing behaviors tracking with hash to store behavior name and error message
33
+ class_headers.each { |class_name| @missing_behaviors[class_name] = {} }
34
+
35
+ @data[:rows].each do |row|
36
+ behavior_name = has_type_column ? row[1] : row[0]
37
+ values = has_type_column ? row[2..] : row[1..]
38
+
39
+ values.each_with_index do |value, index|
40
+ class_name = class_headers[index]
41
+ next unless value.to_s.include?("🚫") || value.nil?
42
+
43
+ # Store the actual error message from the table
44
+ error_message = value.to_s.include?("🚫") ? value.to_s : "🚫 Not defined"
45
+ @missing_behaviors[class_name][behavior_name] = error_message
46
+ end
47
+ end
48
+ end
49
+
50
+ def generate_missing_behaviors_summary
51
+ # Only show summary if there are missing behaviors
52
+ total_missing = @missing_behaviors.values.map(&:size).sum
53
+ return [] if total_missing.zero?
54
+
55
+ case @summary_style
56
+ when :flat
57
+ generate_flat_summary
58
+ when :detailed
59
+ generate_detailed_summary
60
+ else
61
+ generate_grouped_summary
62
+ end
63
+ end
64
+
65
+ def generate_grouped_summary
66
+ output = []
67
+
68
+ output << "## Missing Behaviors Summary"
69
+ output << ""
70
+ output << "The following behaviors are not defined in some classes:"
71
+ output << ""
72
+
73
+ @missing_behaviors.each do |class_name, behaviors_hash|
74
+ next if behaviors_hash.empty?
75
+
76
+ output << "### #{class_name}"
77
+ behaviors_hash.each do |behavior_name, error_message|
78
+ output << "- `#{behavior_name}` - #{error_message}"
79
+ end
80
+ output << ""
81
+ end
82
+
83
+ output
84
+ end
85
+
86
+ def generate_flat_summary
87
+ output = []
88
+
89
+ output << "## Missing Behaviors"
90
+ output << ""
91
+
92
+ all_missing = []
93
+ @missing_behaviors.each do |class_name, behaviors_hash|
94
+ behaviors_hash.each do |behavior_name, error_message|
95
+ all_missing << "- **#{class_name}**: `#{behavior_name}` - #{error_message}"
96
+ end
97
+ end
98
+
99
+ output.concat(all_missing.sort)
100
+ output << ""
101
+
102
+ output
103
+ end
104
+
105
+ def generate_detailed_summary
106
+ output = []
107
+
108
+ total_missing = @missing_behaviors.values.map(&:size).sum
109
+ total_classes = @missing_behaviors.keys.size
110
+
111
+ output << "## Missing Behaviors Analysis"
112
+ output << ""
113
+ output << "**Summary**: #{total_missing} missing behaviors across #{total_classes} classes"
114
+ output << ""
115
+
116
+ # Group by error type
117
+ by_error_type = {}
118
+ @missing_behaviors.each do |class_name, behaviors_hash|
119
+ behaviors_hash.each do |behavior_name, error_message|
120
+ error_type = error_message.split.first(2).join(" ") # e.g., "🚫 Not", "⚠️ Error:"
121
+ by_error_type[error_type] ||= []
122
+ by_error_type[error_type] << { class: class_name, behavior: behavior_name, message: error_message }
123
+ end
124
+ end
125
+
126
+ by_error_type.each do |error_type, items|
127
+ output << "### #{error_type} (#{items.size} items)"
128
+ output << ""
129
+ items.each do |item|
130
+ output << "- **#{item[:class]}**: `#{item[:behavior]}` - #{item[:message]}"
131
+ end
132
+ output << ""
133
+ end
134
+
135
+ output
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end