class-metrix 0.1.2 → 1.0.1

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/.editorconfig +48 -0
  3. data/.vscode/README.md +128 -0
  4. data/.vscode/extensions.json +31 -0
  5. data/.vscode/keybindings.json +26 -0
  6. data/.vscode/launch.json +32 -0
  7. data/.vscode/rbs.code-snippets +61 -0
  8. data/.vscode/settings.json +112 -0
  9. data/.vscode/tasks.json +240 -0
  10. data/CHANGELOG.md +73 -4
  11. data/README.md +86 -22
  12. data/Steepfile +26 -0
  13. data/docs/ARCHITECTURE.md +501 -0
  14. data/docs/CHANGELOG_EVOLUTION_EXAMPLE.md +95 -0
  15. data/examples/README.md +161 -114
  16. data/examples/basic_usage.rb +88 -0
  17. data/examples/debug_levels_demo.rb +65 -0
  18. data/examples/debug_mode_demo.rb +75 -0
  19. data/examples/inheritance_and_modules.rb +155 -0
  20. data/lib/class_metrix/extractor.rb +106 -11
  21. data/lib/class_metrix/extractors/constants_extractor.rb +155 -21
  22. data/lib/class_metrix/extractors/methods_extractor.rb +186 -21
  23. data/lib/class_metrix/extractors/multi_type_extractor.rb +8 -7
  24. data/lib/class_metrix/formatters/base/base_formatter.rb +3 -3
  25. data/lib/class_metrix/formatters/components/footer_component.rb +4 -4
  26. data/lib/class_metrix/formatters/components/generic_header_component.rb +2 -2
  27. data/lib/class_metrix/formatters/components/header_component.rb +4 -4
  28. data/lib/class_metrix/formatters/components/missing_behaviors_component.rb +7 -7
  29. data/lib/class_metrix/formatters/components/table_component/column_width_calculator.rb +56 -0
  30. data/lib/class_metrix/formatters/components/table_component/row_processor.rb +141 -0
  31. data/lib/class_metrix/formatters/components/table_component/table_data_extractor.rb +57 -0
  32. data/lib/class_metrix/formatters/components/table_component/table_renderer.rb +55 -0
  33. data/lib/class_metrix/formatters/components/table_component.rb +32 -245
  34. data/lib/class_metrix/formatters/csv_formatter.rb +3 -3
  35. data/lib/class_metrix/formatters/markdown_formatter.rb +3 -4
  36. data/lib/class_metrix/formatters/shared/markdown_table_builder.rb +12 -7
  37. data/lib/class_metrix/formatters/shared/table_builder.rb +92 -27
  38. data/lib/class_metrix/formatters/shared/value_processor.rb +72 -16
  39. data/lib/class_metrix/utils/debug_logger.rb +159 -0
  40. data/lib/class_metrix/version.rb +1 -1
  41. data/sig/class_metrix.rbs +8 -0
  42. data/sig/extractor.rbs +54 -0
  43. data/sig/extractors.rbs +84 -0
  44. data/sig/formatters_base.rbs +59 -0
  45. data/sig/formatters_components.rbs +133 -0
  46. data/sig/formatters_main.rbs +20 -0
  47. data/sig/formatters_shared.rbs +102 -0
  48. data/sig/manifest.yaml +32 -0
  49. data/sig/utils.rbs +57 -0
  50. data/sig/value_processor.rbs +11 -0
  51. data/sig/version.rbs +4 -0
  52. metadata +60 -10
  53. data/examples/advanced/error_handling.rb +0 -199
  54. data/examples/advanced/hash_expansion.rb +0 -180
  55. data/examples/basic/01_simple_constants.rb +0 -56
  56. data/examples/basic/02_simple_methods.rb +0 -99
  57. data/examples/basic/03_multi_type_extraction.rb +0 -116
  58. data/examples/components/configurable_reports.rb +0 -201
  59. data/examples/csv_output_demo.rb +0 -237
  60. data/examples/real_world/microservices_audit.rb +0 -312
  61. data/sig/class/metrix.rbs +0 -6
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "value_processor"
4
+ require_relative "../../utils/debug_logger"
4
5
 
5
6
  module ClassMetrix
6
7
  module Formatters
@@ -13,6 +14,13 @@ module ClassMetrix
13
14
  @expand_hashes = expand_hashes
14
15
  @options = options
15
16
  @value_processor = ValueProcessor
17
+ @debug_mode = options.fetch(:debug_mode, false)
18
+ @debug_level = options.fetch(:debug_level, :basic)
19
+ @logger = Utils::DebugLogger.new("TableBuilder", @debug_mode, @debug_level)
20
+
21
+ @logger.log("TableBuilder initialized with expand_hashes: #{expand_hashes}")
22
+ @logger.log("Data headers: #{@data[:headers]}", :detailed)
23
+ @logger.log("Number of rows: #{@data[:rows]&.length || 0}")
16
24
  end
17
25
 
18
26
  def build_simple_table
@@ -25,24 +33,26 @@ module ClassMetrix
25
33
  def build_expanded_table
26
34
  return build_simple_table unless @expand_hashes
27
35
 
36
+ @logger.log("Building expanded table with hash expansion")
28
37
  headers = @data[:headers]
29
- expanded_rows = process_rows_for_expansion(headers)
38
+ rows = process_rows_for_expansion(headers)
30
39
 
31
40
  {
32
41
  headers: headers,
33
- rows: expanded_rows
42
+ rows: rows
34
43
  }
35
44
  end
36
45
 
37
46
  def build_flattened_table
38
47
  return build_simple_table unless @expand_hashes
39
48
 
49
+ @logger.log("Building flattened table with hash expansion")
40
50
  headers = @data[:headers]
41
- rows = @data[:rows]
51
+ all_hash_keys = collect_all_hash_keys(@data[:rows], headers)
52
+ @logger.log("Collected hash keys: #{all_hash_keys}")
42
53
 
43
- all_hash_keys = collect_all_hash_keys(rows, headers)
44
54
  flattened_headers = create_flattened_headers(headers, all_hash_keys)
45
- flattened_rows = create_flattened_rows(rows, headers, all_hash_keys)
55
+ flattened_rows = create_flattened_rows(@data[:rows], headers, all_hash_keys)
46
56
 
47
57
  {
48
58
  headers: flattened_headers,
@@ -53,21 +63,36 @@ module ClassMetrix
53
63
  private
54
64
 
55
65
  def process_rows_for_expansion(headers)
56
- expanded_rows = []
66
+ expanded_rows = [] # : Array[Array[String]]
67
+ expandable_count = 0
57
68
 
58
- @data[:rows].each do |row|
69
+ @data[:rows].each_with_index do |row, index|
59
70
  if row_has_expandable_hash?(row)
71
+ expandable_count += 1
72
+ @logger.log("Row #{index} has expandable hashes", :detailed)
60
73
  expanded_rows.concat(expand_row(row, headers))
61
74
  else
75
+ @logger.log("Row #{index} has no expandable hashes", :verbose)
62
76
  expanded_rows << process_row(row)
63
77
  end
64
78
  end
65
79
 
80
+ @logger.log("Processed #{@data[:rows].length} rows, #{expandable_count} had expandable hashes")
66
81
  expanded_rows
67
82
  end
68
83
 
69
84
  def row_has_expandable_hash?(row)
70
- row[value_start_index..].any? { |cell| cell.is_a?(Hash) }
85
+ values = row[value_start_index..]
86
+
87
+ # Use summary logging instead of per-value logging
88
+ @logger.log_hash_detection_summary(values)
89
+
90
+ # Only consider real Hash objects as expandable
91
+ return false if values.nil?
92
+
93
+ result = values.any? { |cell| cell.is_a?(Hash) && cell.instance_of?(Hash) }
94
+ @logger.log_decision("Row expandable", "#{result ? "Has" : "No"} real Hash objects", :detailed)
95
+ result
71
96
  end
72
97
 
73
98
  def create_flattened_rows(rows, headers, all_hash_keys)
@@ -107,7 +132,9 @@ module ClassMetrix
107
132
 
108
133
  def expand_row(row, _headers)
109
134
  behavior_name = row[behavior_column_index]
110
- values = row[value_start_index..]
135
+ values = row[value_start_index..] || []
136
+
137
+ @logger.log("Expanding row for behavior '#{behavior_name}'")
111
138
 
112
139
  all_hash_keys = collect_hash_keys_from_values(values)
113
140
  return [process_row(row)] if all_hash_keys.empty?
@@ -117,19 +144,44 @@ module ClassMetrix
117
144
 
118
145
  def collect_hash_keys_from_values(values)
119
146
  all_hash_keys = Set.new
120
- values.each do |value|
121
- all_hash_keys.merge(value.keys.map(&:to_s)) if value.is_a?(Hash)
147
+ hash_count = 0
148
+
149
+ values.each_with_index do |value, index|
150
+ # Be more strict about what we consider a "hash"
151
+ if value.is_a?(Hash) && value.instance_of?(Hash) && value.respond_to?(:keys)
152
+ hash_count += 1
153
+ keys = @logger.safe_keys(value)
154
+ @logger.log("Value #{index} is a real Hash with keys: #{keys}", :detailed)
155
+ all_hash_keys.merge(keys.map(&:to_s))
156
+ elsif value.is_a?(Hash)
157
+ @logger.log_anomaly("Hash-like object at index #{index} (#{@logger.safe_class(value)}) skipped")
158
+ end
122
159
  end
160
+
161
+ @logger.log("Collected hash keys from #{hash_count} real hashes: #{all_hash_keys.to_a}")
123
162
  all_hash_keys
124
163
  end
125
164
 
126
165
  def build_expanded_row_set(row, behavior_name, values, all_hash_keys)
127
- expanded_rows = []
128
- expanded_rows << build_main_row(row, behavior_name, values)
129
- expanded_rows.concat(build_sub_rows(all_hash_keys, values))
166
+ expanded_rows = [] # : Array[Array[String]]
167
+
168
+ # Add main row if configured to show
169
+ expanded_rows << build_main_row(row, behavior_name, values) if should_show_main_row?
170
+
171
+ # Add sub rows if configured to show
172
+ expanded_rows.concat(build_sub_rows(all_hash_keys, values)) if should_show_key_rows?
173
+
130
174
  expanded_rows
131
175
  end
132
176
 
177
+ def should_show_main_row?
178
+ !@options.fetch(:hide_main_row, false)
179
+ end
180
+
181
+ def should_show_key_rows?
182
+ !@options.fetch(:hide_key_rows, true) # Default: hide key rows
183
+ end
184
+
133
185
  def build_main_row(row, behavior_name, values)
134
186
  processed_values = values.map { |value| process_value(value) }
135
187
 
@@ -172,8 +224,8 @@ module ClassMetrix
172
224
  end
173
225
 
174
226
  def extract_hash_value_for_key(hash, key)
175
- if @value_processor.has_hash_key?(hash, key)
176
- hash_value = @value_processor.safe_hash_lookup(hash, key)
227
+ if @value_processor.has_hash_key?(hash, key, debug_mode: @debug_mode)
228
+ hash_value = @value_processor.safe_hash_lookup(hash, key, debug_mode: @debug_mode)
177
229
  process_value(hash_value)
178
230
  else
179
231
  get_null_value
@@ -182,25 +234,38 @@ module ClassMetrix
182
234
 
183
235
  def collect_all_hash_keys(rows, _headers)
184
236
  value_start_idx = value_start_index
185
- all_keys = {} # behavior_name => Set of keys
237
+ all_keys = {} # : Hash[String, Set[String]] # behavior_name => Set of keys
238
+ total_hash_count = 0
186
239
 
187
- rows.each do |row|
188
- collect_hash_keys_for_row(row, value_start_idx, all_keys)
240
+ rows.each_with_index do |row, index|
241
+ hash_count = collect_hash_keys_for_row(row, value_start_idx, all_keys)
242
+ total_hash_count += hash_count
243
+ @logger.log("Row #{index}: #{hash_count} hashes found", :detailed)
189
244
  end
190
245
 
246
+ @logger.log("Final hash key collection: #{total_hash_count} total hashes, #{all_keys.keys.length} behaviors with hashes")
191
247
  all_keys
192
248
  end
193
249
 
194
250
  def collect_hash_keys_for_row(row, value_start_idx, all_keys)
195
251
  behavior_name = row[behavior_column_index]
196
- values = row[value_start_idx..]
197
-
198
- values.each do |value|
199
- next unless value.is_a?(Hash)
200
-
201
- all_keys[behavior_name] ||= Set.new
202
- all_keys[behavior_name].merge(value.keys.map(&:to_s))
252
+ values = row[value_start_idx..] || []
253
+ hash_count = 0
254
+
255
+ values.each_with_index do |value, index|
256
+ # Be more strict about what we consider a "hash"
257
+ if value.is_a?(Hash) && value.instance_of?(Hash) && value.respond_to?(:keys)
258
+ hash_count += 1
259
+ keys = @logger.safe_keys(value)
260
+ @logger.log("Behavior '#{behavior_name}' value #{index}: Hash with keys #{keys}", :verbose)
261
+ all_keys[behavior_name] ||= Set.new
262
+ all_keys[behavior_name].merge(keys.map(&:to_s))
263
+ elsif value.is_a?(Hash)
264
+ @logger.log_anomaly("Hash-like object in '#{behavior_name}' at index #{index} (#{@logger.safe_class(value)}) skipped")
265
+ end
203
266
  end
267
+
268
+ hash_count
204
269
  end
205
270
 
206
271
  def create_flattened_headers(headers, all_hash_keys)
@@ -236,7 +301,7 @@ module ClassMetrix
236
301
  def add_flattened_hash_values(flattened, row, behavior_name, all_hash_keys)
237
302
  return unless all_hash_keys[behavior_name]
238
303
 
239
- values = row[value_start_index..]
304
+ values = row[value_start_index..] || []
240
305
 
241
306
  all_hash_keys[behavior_name].to_a.sort.each do |key|
242
307
  add_flattened_values_for_key(flattened, values, key)
@@ -1,14 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../../utils/debug_logger"
4
+
3
5
  module ClassMetrix
4
6
  module Formatters
5
7
  module Shared
6
8
  class ValueProcessor
7
- def self.process_for_markdown(value)
9
+ def self.process_for_markdown(value, debug_mode: false, debug_level: :basic)
10
+ logger = Utils::DebugLogger.new("ValueProcessor", debug_mode, debug_level)
11
+ logger.log_value_details(value)
12
+
8
13
  case value
9
14
  when Hash
10
- value.inspect
15
+ logger.log("Processing Hash with keys: #{logger.safe_keys(value)}", :detailed)
16
+ logger.safe_inspect(value)
11
17
  when Array
18
+ logger.log("Processing Array with #{logger.safe_length(value)} elements", :detailed)
12
19
  value.join(", ")
13
20
  when true
14
21
  "✅"
@@ -17,21 +24,32 @@ module ClassMetrix
17
24
  when nil
18
25
  "❌"
19
26
  when String
27
+ logger.log("Processing String: #{logger.safe_truncate(value, 50)}", :verbose)
20
28
  value
21
29
  else
22
- value.to_s
30
+ logger.log("Processing other type (#{logger.safe_class(value)}): #{logger.safe_truncate(logger.safe_inspect(value), 100)}",
31
+ :detailed)
32
+ logger.safe_to_s(value)
23
33
  end
24
34
  end
25
35
 
26
- def self.process_for_csv(value, options = {})
27
- null_value = options.fetch(:null_value, "")
36
+ def self.process_for_csv(value, options = {}, debug_mode: false, debug_level: :basic)
37
+ debug_mode = options.fetch(:debug_mode, false) if options.is_a?(Hash)
38
+ debug_level = options.fetch(:debug_level, :basic) if options.is_a?(Hash)
39
+ null_value = options.fetch(:null_value, "") if options.is_a?(Hash)
40
+
41
+ logger = Utils::DebugLogger.new("ValueProcessor", debug_mode, debug_level)
42
+ logger.log_value_details(value)
43
+
44
+ process_csv_value_by_type(value, logger, null_value)
45
+ end
28
46
 
47
+ def self.process_csv_value_by_type(value, logger, null_value)
29
48
  case value
30
49
  when Hash
31
- # For non-flattened CSV, represent hash as JSON-like string
32
- value.inspect
50
+ process_hash_for_csv(value, logger)
33
51
  when Array
34
- value.join("; ") # Use semicolon to avoid CSV comma conflicts
52
+ process_array_for_csv(value, logger)
35
53
  when true
36
54
  "TRUE"
37
55
  when false
@@ -39,21 +57,59 @@ module ClassMetrix
39
57
  when nil
40
58
  null_value
41
59
  when String
42
- # Clean up emoji for CSV compatibility
43
- clean_value = value.gsub(/🚫|⚠️|✅|❌/, "").strip
44
- clean_value.empty? ? null_value : clean_value
60
+ process_string_for_csv(value, null_value)
45
61
  else
46
- value.to_s
62
+ process_other_for_csv(value, logger)
47
63
  end
48
64
  end
49
65
 
50
- def self.safe_hash_lookup(hash, key)
66
+ def self.process_hash_for_csv(value, logger)
67
+ logger.log("Processing Hash for CSV with keys: #{logger.safe_keys(value)}", :detailed)
68
+ logger.safe_inspect(value)
69
+ end
70
+
71
+ def self.process_array_for_csv(value, logger)
72
+ logger.log("Processing Array for CSV with #{logger.safe_length(value)} elements", :detailed)
73
+ value.join("; ") # Use semicolon to avoid CSV comma conflicts
74
+ end
75
+
76
+ def self.process_string_for_csv(value, null_value)
77
+ clean_value = value.gsub(/🚫|⚠️|✅|❌/, "").strip
78
+ clean_value.empty? ? null_value : clean_value
79
+ end
80
+
81
+ def self.process_other_for_csv(value, logger)
82
+ logger.log(
83
+ "Processing other type for CSV (#{logger.safe_class(value)}): #{logger.safe_truncate(logger.safe_inspect(value),
84
+ 100)}", :detailed
85
+ )
86
+ logger.safe_to_s(value)
87
+ end
88
+
89
+ def self.safe_hash_lookup(hash, key, debug_mode: false, debug_level: :basic)
90
+ logger = Utils::DebugLogger.new("ValueProcessor", debug_mode, debug_level)
91
+ logger.log("Hash lookup for key '#{key}' in hash with keys: #{logger.safe_keys(hash)}", :verbose)
92
+
51
93
  # Properly handle false values in hash lookup
52
- hash.key?(key.to_sym) ? hash[key.to_sym] : hash[key.to_s]
94
+ begin
95
+ hash.key?(key.to_sym) ? hash[key.to_sym] : hash[key.to_s]
96
+ rescue StandardError => e
97
+ logger.log("Error during hash lookup: #{e.class.name}: #{e.message}")
98
+ nil
99
+ end
53
100
  end
54
101
 
55
- def self.has_hash_key?(hash, key)
56
- hash.key?(key.to_sym) || hash.key?(key.to_s)
102
+ def self.has_hash_key?(hash, key, debug_mode: false, debug_level: :basic)
103
+ logger = Utils::DebugLogger.new("ValueProcessor", debug_mode, debug_level)
104
+
105
+ begin
106
+ result = hash.key?(key.to_sym) || hash.key?(key.to_s)
107
+ logger.log("Checking if hash has key '#{key}': #{result} (hash keys: #{logger.safe_keys(hash)})", :verbose)
108
+ result
109
+ rescue StandardError => e
110
+ logger.log("Error checking hash key: #{e.class.name}: #{e.message}")
111
+ false
112
+ end
57
113
  end
58
114
 
59
115
  # Error message generators
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClassMetrix
4
+ module Utils
5
+ # Debug logging utility for ClassMetrix
6
+ # Provides safe, consistent debug output across all components
7
+ class DebugLogger
8
+ # Debug levels: :basic, :detailed, :verbose
9
+ LEVELS = { basic: 1, detailed: 2, verbose: 3 }.freeze
10
+
11
+ def initialize(component_name, debug_mode = false, level = :basic)
12
+ @component_name = component_name
13
+ @debug_mode = debug_mode
14
+ @level = LEVELS[level] || LEVELS[:basic]
15
+ end
16
+
17
+ def log(message, level = :basic)
18
+ return unless @debug_mode && LEVELS[level] <= @level
19
+
20
+ puts "[DEBUG #{@component_name}] #{message}"
21
+ end
22
+
23
+ def log_safe_operation(operation_name, level = :detailed, &block)
24
+ log("Starting #{operation_name}", level)
25
+ result = block.call
26
+ log("Completed #{operation_name} successfully", level)
27
+ result
28
+ rescue StandardError => e
29
+ log("Error in #{operation_name}: #{e.class.name}: #{e.message}")
30
+ raise
31
+ end
32
+
33
+ # Summary logging for groups of similar operations
34
+ def log_summary(operation, items, &block)
35
+ return unless @debug_mode
36
+
37
+ if @level >= LEVELS[:detailed]
38
+ log("#{operation} (#{items.length} items)")
39
+ items.each_with_index { |item, i| block.call(item, i) } if block_given?
40
+ else
41
+ log("#{operation} (#{items.length} items)")
42
+ end
43
+ end
44
+
45
+ def log_decision(decision, reason, level = :basic)
46
+ log("Decision: #{decision} - #{reason}", level)
47
+ end
48
+
49
+ def log_anomaly(description)
50
+ log("⚠️ Anomaly: #{description}")
51
+ end
52
+
53
+ # Safe inspection methods to handle problematic objects
54
+ def safe_inspect(value)
55
+ value.inspect
56
+ rescue StandardError => e
57
+ "[inspect failed: #{e.class.name}]"
58
+ end
59
+
60
+ def safe_class(value)
61
+ value.class
62
+ rescue StandardError => e
63
+ "[class failed: #{e.class.name}]"
64
+ end
65
+
66
+ def safe_keys(value)
67
+ value.keys
68
+ rescue StandardError => e
69
+ "[keys failed: #{e.class.name}]"
70
+ end
71
+
72
+ def safe_length(value)
73
+ value.length
74
+ rescue StandardError
75
+ "[length failed]"
76
+ end
77
+
78
+ def safe_truncate(str, max_length)
79
+ return str unless str.respond_to?(:length) && str.respond_to?(:[])
80
+
81
+ str.length > max_length ? "#{str[0...max_length]}..." : str
82
+ rescue StandardError => e
83
+ "[truncate failed: #{e.class.name}]"
84
+ end
85
+
86
+ def safe_to_s(value)
87
+ value.to_s
88
+ rescue StandardError => e
89
+ begin
90
+ value.class.name
91
+ rescue StandardError => e2
92
+ "[to_s failed: #{e.class.name}, class failed: #{e2.class.name}]"
93
+ end
94
+ end
95
+
96
+ def log_value_details(value, index = nil, level = :verbose)
97
+ return unless @debug_mode && @level >= LEVELS[level]
98
+
99
+ prefix = index ? "Value #{index}" : "Value"
100
+ log("#{prefix}: #{safe_inspect(value)} (#{safe_class(value)})", level)
101
+ end
102
+
103
+ def log_hash_detection(value, index = nil, level = :detailed)
104
+ return unless @debug_mode && @level >= LEVELS[level]
105
+
106
+ prefix = index ? "Value #{index}" : "Value"
107
+ is_hash = value.is_a?(Hash)
108
+ is_real_hash = value.is_a?(Hash) && value.instance_of?(Hash)
109
+
110
+ return unless is_hash
111
+
112
+ log("#{prefix} hash detection:")
113
+ log(" is_a?(Hash): #{is_hash}, class == Hash: #{is_real_hash}")
114
+ log(" respond_to?(:keys): #{value.respond_to?(:keys)}")
115
+
116
+ if is_hash && is_real_hash
117
+ log(" keys: #{safe_keys(value)}")
118
+ elsif is_hash
119
+ log_anomaly("Hash-like object (#{safe_class(value)}) but not real Hash - will be skipped")
120
+ end
121
+ end
122
+
123
+ # Smart hash detection summary
124
+ def log_hash_detection_summary(values)
125
+ return unless @debug_mode
126
+
127
+ hash_count = 0
128
+ real_hash_count = 0
129
+ anomaly_count = 0
130
+
131
+ values.each do |value|
132
+ next unless value.is_a?(Hash)
133
+
134
+ hash_count += 1
135
+ if value.instance_of?(Hash)
136
+ real_hash_count += 1
137
+ else
138
+ anomaly_count += 1
139
+ end
140
+ end
141
+
142
+ return unless hash_count.positive?
143
+
144
+ log("Hash detection summary: #{real_hash_count} real hashes, #{anomaly_count} hash-like objects")
145
+ log_anomaly("Found #{anomaly_count} hash-like proxy objects") if anomaly_count.positive?
146
+ end
147
+
148
+ def enabled?
149
+ @debug_mode
150
+ end
151
+
152
+ attr_reader :level
153
+
154
+ def set_level(level)
155
+ @level = LEVELS[level] || LEVELS[:basic]
156
+ end
157
+ end
158
+ end
159
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClassMetrix
4
- VERSION = "0.1.2"
4
+ VERSION = "1.0.1"
5
5
  end
@@ -0,0 +1,8 @@
1
+ # Main ClassMetrix module
2
+ module ClassMetrix
3
+ class Error < StandardError
4
+ end
5
+
6
+ # Main entry point for the gem
7
+ def self.extract: (*Symbol types) -> ClassMetrix::Extractor
8
+ end
data/sig/extractor.rbs ADDED
@@ -0,0 +1,54 @@
1
+ # Core extractor class that provides the fluent interface
2
+ module ClassMetrix
3
+ class Extractor
4
+ @types: Array[Symbol]
5
+ @classes: Array[Class]
6
+ @filters: Array[Regexp | String]
7
+ @expand_hashes: bool
8
+ @handle_errors: bool
9
+ @modules: Array[Module]
10
+ @include_inherited: bool
11
+ @include_modules: bool
12
+ @show_source: bool
13
+ @hide_main_row: bool
14
+ @hide_key_rows: bool
15
+ @debug_mode: bool
16
+ @debug_level: Symbol
17
+ @logger: ClassMetrix::Utils::DebugLogger?
18
+
19
+ def initialize: (*Symbol types) -> void
20
+
21
+ # Core configuration methods
22
+ def from: (Array[Class] classes) -> self
23
+ def filter: (Regexp | String pattern) -> self
24
+ def expand_hashes: () -> self
25
+ def debug: (?Symbol level) -> self
26
+ def handle_errors: () -> self
27
+ def modules: (Array[Module] module_list) -> self
28
+
29
+ # Inheritance and module inclusion options
30
+ def include_inherited: () -> self
31
+ def include_modules: () -> self
32
+ def show_source: () -> self
33
+ def include_all: () -> self
34
+
35
+ # Hash expansion display options
36
+ def show_only_main: () -> self
37
+ def show_only_keys: () -> self
38
+ def show_expanded_details: () -> self
39
+ def hide_main_row: () -> self
40
+ def hide_key_rows: () -> self
41
+
42
+ # Output methods
43
+ def to_markdown: (?String filename) -> String
44
+ def to_csv: (?String filename) -> String
45
+
46
+ private
47
+
48
+ def extract_all_data: () -> Hash[Symbol, untyped]
49
+ def extract_single_type: (Symbol type) -> Hash[Symbol, untyped]
50
+ def extract_multiple_types: () -> Hash[Symbol, untyped]
51
+ def get_extractor: (Symbol type) -> (ClassMetrix::ConstantsExtractor | ClassMetrix::MethodsExtractor)
52
+ def extraction_options: () -> Hash[Symbol, untyped]
53
+ end
54
+ end
@@ -0,0 +1,84 @@
1
+ # Specialized extractor classes
2
+ module ClassMetrix
3
+ class ConstantsExtractor
4
+ @classes: Array[Class]
5
+ @filters: Array[Regexp | String]
6
+ @handle_errors: bool
7
+ @options: Hash[Symbol, untyped]
8
+ @debug_level: Symbol
9
+ @logger: ClassMetrix::Utils::DebugLogger
10
+
11
+ def initialize: (Array[Class] classes, Array[Regexp | String] filters, bool handle_errors, ?Hash[Symbol, untyped] options) -> void
12
+ def extract: () -> Hash[Symbol, untyped]
13
+
14
+ private
15
+
16
+ def default_options: () -> Hash[Symbol, untyped]
17
+ def build_headers: () -> Array[String]
18
+ def build_rows: (Array[String] constant_names) -> Array[Array[untyped]]
19
+ def get_all_constant_names: () -> Array[String]
20
+ def inheritance_or_modules_enabled?: () -> bool
21
+ def get_comprehensive_constants: (Class klass) -> Array[Symbol]
22
+ def get_inherited_constants: (Class klass) -> Set[Symbol]
23
+ def get_module_constants: (Class klass) -> Set[Symbol]
24
+ def get_all_included_modules: (Class klass) -> Array[Module]
25
+ def core_class?: (untyped klass) -> bool
26
+ def apply_filters: (Array[String] constant_names) -> Array[String]
27
+ def extract_constant_value: (Class klass, String const_name) -> untyped
28
+ def find_constant_source: (Class klass, String const_name) -> Hash[Symbol, untyped]?
29
+ def find_inherited_constant: (Class klass, String const_name) -> Hash[Symbol, untyped]?
30
+ def find_module_constant: (Class klass, String const_name) -> Hash[Symbol, untyped]?
31
+ def build_constant_info: (untyped value, String source, Symbol type) -> Hash[Symbol, untyped]
32
+ def debug_log: (String message) -> void
33
+ end
34
+
35
+ class MethodsExtractor
36
+ @classes: Array[Class]
37
+ @filters: Array[Regexp | String]
38
+ @handle_errors: bool
39
+ @options: Hash[Symbol, untyped]
40
+
41
+ def initialize: (Array[Class] classes, Array[Regexp | String] filters, bool handle_errors, ?Hash[Symbol, untyped] options) -> void
42
+ def extract: () -> Hash[Symbol, untyped]
43
+
44
+ private
45
+
46
+ def default_options: () -> Hash[Symbol, untyped]
47
+ def build_headers: () -> Array[String]
48
+ def build_rows: (Array[String] method_names) -> Array[Array[untyped]]
49
+ def get_all_class_method_names: () -> Array[String]
50
+ def inheritance_or_modules_enabled?: () -> bool
51
+ def get_comprehensive_methods: (Class klass) -> Array[Symbol]
52
+ def get_inherited_methods: (Class klass) -> Set[Symbol]
53
+ def get_module_methods: (Class klass) -> Set[Symbol]
54
+ def excluded_module_method?: (String method_name) -> bool
55
+ def get_all_singleton_modules: (Class klass) -> Array[Module]
56
+ def core_class?: (untyped klass) -> bool
57
+ def core_module?: (untyped mod) -> bool
58
+ def apply_filters: (Array[String] method_names) -> Array[String]
59
+ def call_class_method: (Class klass, String method_name) -> untyped
60
+ def call_method: (Hash[Symbol, untyped] method_info, Class klass, String method_name) -> untyped
61
+ def find_method_source: (Class klass, String method_name) -> Hash[Symbol, untyped]?
62
+ def find_inherited_method: (Class klass, Symbol method_sym, String method_name) -> Hash[Symbol, untyped]?
63
+ def find_module_method: (Class klass, Symbol method_sym, String method_name) -> Hash[Symbol, untyped]?
64
+ def determine_module_source: (Class klass, Module mod) -> String
65
+ def build_method_info: (String source, Symbol type, Proc? callable) -> Hash[Symbol, untyped]
66
+ end
67
+
68
+ class MultiTypeExtractor
69
+ @classes: Array[Class]
70
+ @types: Array[Symbol]
71
+ @filters: Array[Regexp | String]
72
+ @modules: Array[Module]
73
+ @handle_errors: bool
74
+ @options: Hash[Symbol, untyped]
75
+
76
+ def initialize: (Array[Class] classes, Array[Symbol] types, Array[Regexp | String] filters, ?Hash[Symbol, untyped] extraction_config) -> void
77
+ def extract: () -> Hash[Symbol, untyped]
78
+
79
+ private
80
+
81
+ def extract_single_type: (Symbol type) -> Hash[Symbol, untyped]
82
+ def type_label: (Symbol type) -> String
83
+ end
84
+ end