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.
- checksums.yaml +4 -4
- data/.editorconfig +48 -0
- data/.vscode/README.md +128 -0
- data/.vscode/extensions.json +31 -0
- data/.vscode/keybindings.json +26 -0
- data/.vscode/launch.json +32 -0
- data/.vscode/rbs.code-snippets +61 -0
- data/.vscode/settings.json +112 -0
- data/.vscode/tasks.json +240 -0
- data/CHANGELOG.md +73 -4
- data/README.md +86 -22
- data/Steepfile +26 -0
- data/docs/ARCHITECTURE.md +501 -0
- data/docs/CHANGELOG_EVOLUTION_EXAMPLE.md +95 -0
- data/examples/README.md +161 -114
- data/examples/basic_usage.rb +88 -0
- data/examples/debug_levels_demo.rb +65 -0
- data/examples/debug_mode_demo.rb +75 -0
- data/examples/inheritance_and_modules.rb +155 -0
- data/lib/class_metrix/extractor.rb +106 -11
- data/lib/class_metrix/extractors/constants_extractor.rb +155 -21
- data/lib/class_metrix/extractors/methods_extractor.rb +186 -21
- data/lib/class_metrix/extractors/multi_type_extractor.rb +8 -7
- data/lib/class_metrix/formatters/base/base_formatter.rb +3 -3
- data/lib/class_metrix/formatters/components/footer_component.rb +4 -4
- data/lib/class_metrix/formatters/components/generic_header_component.rb +2 -2
- data/lib/class_metrix/formatters/components/header_component.rb +4 -4
- data/lib/class_metrix/formatters/components/missing_behaviors_component.rb +7 -7
- data/lib/class_metrix/formatters/components/table_component/column_width_calculator.rb +56 -0
- data/lib/class_metrix/formatters/components/table_component/row_processor.rb +141 -0
- data/lib/class_metrix/formatters/components/table_component/table_data_extractor.rb +57 -0
- data/lib/class_metrix/formatters/components/table_component/table_renderer.rb +55 -0
- data/lib/class_metrix/formatters/components/table_component.rb +32 -245
- data/lib/class_metrix/formatters/csv_formatter.rb +3 -3
- data/lib/class_metrix/formatters/markdown_formatter.rb +3 -4
- data/lib/class_metrix/formatters/shared/markdown_table_builder.rb +12 -7
- data/lib/class_metrix/formatters/shared/table_builder.rb +92 -27
- data/lib/class_metrix/formatters/shared/value_processor.rb +72 -16
- data/lib/class_metrix/utils/debug_logger.rb +159 -0
- data/lib/class_metrix/version.rb +1 -1
- data/sig/class_metrix.rbs +8 -0
- data/sig/extractor.rbs +54 -0
- data/sig/extractors.rbs +84 -0
- data/sig/formatters_base.rbs +59 -0
- data/sig/formatters_components.rbs +133 -0
- data/sig/formatters_main.rbs +20 -0
- data/sig/formatters_shared.rbs +102 -0
- data/sig/manifest.yaml +32 -0
- data/sig/utils.rbs +57 -0
- data/sig/value_processor.rbs +11 -0
- data/sig/version.rbs +4 -0
- metadata +60 -10
- data/examples/advanced/error_handling.rb +0 -199
- data/examples/advanced/hash_expansion.rb +0 -180
- data/examples/basic/01_simple_constants.rb +0 -56
- data/examples/basic/02_simple_methods.rb +0 -99
- data/examples/basic/03_multi_type_extraction.rb +0 -116
- data/examples/components/configurable_reports.rb +0 -201
- data/examples/csv_output_demo.rb +0 -237
- data/examples/real_world/microservices_audit.rb +0 -312
- 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
|
-
|
38
|
+
rows = process_rows_for_expansion(headers)
|
30
39
|
|
31
40
|
{
|
32
41
|
headers: headers,
|
33
|
-
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
|
-
|
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].
|
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..]
|
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
|
-
|
121
|
-
|
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
|
-
|
129
|
-
|
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.
|
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
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
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
|
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.
|
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
|
-
|
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
|
-
|
32
|
-
value.inspect
|
50
|
+
process_hash_for_csv(value, logger)
|
33
51
|
when Array
|
34
|
-
value
|
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
|
-
|
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
|
62
|
+
process_other_for_csv(value, logger)
|
47
63
|
end
|
48
64
|
end
|
49
65
|
|
50
|
-
def self.
|
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
|
-
|
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
|
-
|
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
|
data/lib/class_metrix/version.rb
CHANGED
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
|
data/sig/extractors.rbs
ADDED
@@ -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
|