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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +88 -0
- data/.tool-versions +1 -0
- data/CHANGELOG.md +41 -0
- data/LICENSE.txt +21 -0
- data/README.md +417 -0
- data/RELEASE_GUIDE.md +158 -0
- data/Rakefile +12 -0
- data/examples/README.md +155 -0
- data/examples/advanced/error_handling.rb +199 -0
- data/examples/advanced/hash_expansion.rb +180 -0
- data/examples/basic/01_simple_constants.rb +56 -0
- data/examples/basic/02_simple_methods.rb +99 -0
- data/examples/basic/03_multi_type_extraction.rb +116 -0
- data/examples/components/configurable_reports.rb +201 -0
- data/examples/csv_output_demo.rb +237 -0
- data/examples/real_world/microservices_audit.rb +312 -0
- data/lib/class_metrix/extractor.rb +121 -0
- data/lib/class_metrix/extractors/constants_extractor.rb +87 -0
- data/lib/class_metrix/extractors/methods_extractor.rb +87 -0
- data/lib/class_metrix/extractors/multi_type_extractor.rb +66 -0
- data/lib/class_metrix/formatters/base/base_component.rb +62 -0
- data/lib/class_metrix/formatters/base/base_formatter.rb +93 -0
- data/lib/class_metrix/formatters/components/footer_component.rb +67 -0
- data/lib/class_metrix/formatters/components/generic_header_component.rb +87 -0
- data/lib/class_metrix/formatters/components/header_component.rb +92 -0
- data/lib/class_metrix/formatters/components/missing_behaviors_component.rb +140 -0
- data/lib/class_metrix/formatters/components/table_component.rb +268 -0
- data/lib/class_metrix/formatters/csv_formatter.rb +98 -0
- data/lib/class_metrix/formatters/markdown_formatter.rb +184 -0
- data/lib/class_metrix/formatters/shared/csv_table_builder.rb +21 -0
- data/lib/class_metrix/formatters/shared/markdown_table_builder.rb +97 -0
- data/lib/class_metrix/formatters/shared/table_builder.rb +267 -0
- data/lib/class_metrix/formatters/shared/value_processor.rb +78 -0
- data/lib/class_metrix/processors/value_processor.rb +40 -0
- data/lib/class_metrix/utils/class_resolver.rb +20 -0
- data/lib/class_metrix/version.rb +5 -0
- data/lib/class_metrix.rb +12 -0
- data/sig/class/metrix.rbs +6 -0
- 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
|