class-metrix 1.0.0 → 1.1.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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.prettierrc.json +41 -0
  3. data/.qlty/.gitignore +7 -0
  4. data/.qlty/configs/.yamllint.yaml +8 -0
  5. data/.qlty/qlty.toml +108 -0
  6. data/.rubocop.yml +31 -25
  7. data/.vscode/README.md +255 -47
  8. data/.vscode/extensions.json +8 -13
  9. data/.vscode/keybindings.json +0 -0
  10. data/.vscode/settings.json +81 -11
  11. data/.vscode/tasks.json +231 -0
  12. data/CHANGELOG.md +33 -1
  13. data/README.md +107 -23
  14. data/Rakefile +64 -1
  15. data/Steepfile +26 -0
  16. data/config/brakeman.yml +37 -0
  17. data/docs/ARCHITECTURE.md +90 -48
  18. data/docs/CHANGELOG_EVOLUTION_EXAMPLE.md +95 -0
  19. data/docs/QLTY_INTEGRATION.md +181 -0
  20. data/docs/RELEASE_GUIDE.md +318 -0
  21. data/docs/SLACK_INTEGRATION.md +227 -0
  22. data/examples/README.md +23 -17
  23. data/examples/basic_usage.rb +19 -19
  24. data/examples/debug_levels_demo.rb +15 -16
  25. data/examples/debug_mode_demo.rb +12 -13
  26. data/examples/inheritance_and_modules.rb +45 -45
  27. data/lib/class_metrix/extractor.rb +1 -1
  28. data/lib/class_metrix/extractors/constants_extractor.rb +1 -1
  29. data/lib/class_metrix/extractors/methods_extractor.rb +1 -1
  30. data/lib/class_metrix/extractors/multi_type_extractor.rb +2 -2
  31. data/lib/class_metrix/formatters/base/base_formatter.rb +3 -3
  32. data/lib/class_metrix/formatters/components/footer_component.rb +3 -3
  33. data/lib/class_metrix/formatters/components/generic_header_component.rb +2 -2
  34. data/lib/class_metrix/formatters/components/header_component.rb +4 -4
  35. data/lib/class_metrix/formatters/components/missing_behaviors_component.rb +7 -7
  36. data/lib/class_metrix/formatters/components/table_component/row_processor.rb +8 -5
  37. data/lib/class_metrix/formatters/components/table_component/table_data_extractor.rb +4 -1
  38. data/lib/class_metrix/formatters/components/table_component/table_renderer.rb +2 -2
  39. data/lib/class_metrix/formatters/components/table_component.rb +5 -4
  40. data/lib/class_metrix/formatters/csv_formatter.rb +3 -3
  41. data/lib/class_metrix/formatters/markdown_formatter.rb +3 -4
  42. data/lib/class_metrix/formatters/shared/markdown_table_builder.rb +2 -2
  43. data/lib/class_metrix/formatters/shared/table_builder.rb +8 -6
  44. data/lib/class_metrix/version.rb +1 -1
  45. data/sig/class_metrix.rbs +8 -0
  46. data/sig/extractor.rbs +54 -0
  47. data/sig/extractors.rbs +84 -0
  48. data/sig/formatters_base.rbs +59 -0
  49. data/sig/formatters_components.rbs +133 -0
  50. data/sig/formatters_main.rbs +20 -0
  51. data/sig/formatters_shared.rbs +102 -0
  52. data/sig/manifest.yaml +32 -0
  53. data/sig/utils.rbs +57 -0
  54. data/sig/value_processor.rbs +11 -0
  55. data/sig/version.rbs +4 -0
  56. metadata +94 -4
  57. data/RELEASE_GUIDE.md +0 -158
  58. data/sig/class/metrix.rbs +0 -6
@@ -11,11 +11,11 @@ puts "=" * 50
11
11
 
12
12
  module Configurable
13
13
  DEFAULT_TIMEOUT = 30
14
-
14
+
15
15
  def self.included(base)
16
16
  base.extend(ClassMethods)
17
17
  end
18
-
18
+
19
19
  module ClassMethods
20
20
  def configuration
21
21
  { timeout: DEFAULT_TIMEOUT, enabled: true }
@@ -25,11 +25,11 @@ end
25
25
 
26
26
  module Cacheable
27
27
  CACHE_TTL = 3600
28
-
28
+
29
29
  def self.included(base)
30
30
  base.extend(ClassMethods)
31
31
  end
32
-
32
+
33
33
  module ClassMethods
34
34
  def cache_config
35
35
  { ttl: CACHE_TTL, enabled: true }
@@ -39,11 +39,11 @@ end
39
39
 
40
40
  class BaseService
41
41
  SERVICE_VERSION = "1.0"
42
-
42
+
43
43
  def self.service_type
44
44
  "base"
45
45
  end
46
-
46
+
47
47
  def self.health_check
48
48
  { status: "ok", version: SERVICE_VERSION }
49
49
  end
@@ -51,14 +51,14 @@ end
51
51
 
52
52
  class DatabaseService < BaseService
53
53
  include Configurable
54
-
54
+
55
55
  SERVICE_NAME = "database"
56
56
  CONNECTION_POOL_SIZE = 5
57
-
57
+
58
58
  def self.service_type
59
59
  "database"
60
60
  end
61
-
61
+
62
62
  def self.connection_config
63
63
  { pool_size: CONNECTION_POOL_SIZE, timeout: 60 }
64
64
  end
@@ -67,14 +67,14 @@ end
67
67
  class CacheService < BaseService
68
68
  include Configurable
69
69
  include Cacheable
70
-
71
- SERVICE_NAME = "cache"
70
+
71
+ SERVICE_NAME = "cache"
72
72
  MAX_MEMORY = "512MB"
73
-
73
+
74
74
  def self.service_type
75
75
  "cache"
76
76
  end
77
-
77
+
78
78
  def self.memory_config
79
79
  { max_memory: MAX_MEMORY, eviction_policy: "lru" }
80
80
  end
@@ -86,43 +86,43 @@ services = [DatabaseService, CacheService]
86
86
  puts "\n1. Basic Analysis (Own Constants Only)"
87
87
  puts "-" * 40
88
88
  result = ClassMetrix.extract(:constants)
89
- .from(services)
90
- .to_markdown
89
+ .from(services)
90
+ .to_markdown
91
91
  puts result
92
92
 
93
93
  puts "\n2. With Inheritance (Own + Parent Constants)"
94
94
  puts "-" * 40
95
95
  result = ClassMetrix.extract(:constants)
96
- .from(services)
97
- .include_inherited
98
- .to_markdown
96
+ .from(services)
97
+ .include_inherited
98
+ .to_markdown
99
99
  puts result
100
100
 
101
101
  puts "\n3. With Modules (Own + Module Constants)"
102
102
  puts "-" * 40
103
103
  result = ClassMetrix.extract(:constants)
104
- .from(services)
105
- .include_modules
106
- .to_markdown
104
+ .from(services)
105
+ .include_modules
106
+ .to_markdown
107
107
  puts result
108
108
 
109
109
  puts "\n4. Complete Analysis (Own + Inherited + Modules)"
110
110
  puts "-" * 40
111
111
  result = ClassMetrix.extract(:constants, :class_methods)
112
- .from(services)
113
- .include_all
114
- .handle_errors
115
- .to_markdown
112
+ .from(services)
113
+ .include_all
114
+ .handle_errors
115
+ .to_markdown
116
116
  puts result
117
117
 
118
118
  puts "\n5. Filtered Configuration Analysis"
119
119
  puts "-" * 40
120
120
  result = ClassMetrix.extract(:constants, :class_methods)
121
- .from(services)
122
- .include_all
123
- .filter(/config|timeout|service/i)
124
- .expand_hashes
125
- .to_markdown
121
+ .from(services)
122
+ .include_all
123
+ .filter(/config|timeout|service/i)
124
+ .expand_hashes
125
+ .to_markdown
126
126
  puts result
127
127
 
128
128
  puts "\n6. Hash Expansion Modes"
@@ -130,26 +130,26 @@ puts "-" * 40
130
130
 
131
131
  puts "\n6a. Default: Show Only Main Rows (Collapsed Hashes)"
132
132
  result = ClassMetrix.extract(:class_methods)
133
- .from([CacheService])
134
- .filter(/config/)
135
- .expand_hashes
136
- .to_markdown
133
+ .from([CacheService])
134
+ .filter(/config/)
135
+ .expand_hashes
136
+ .to_markdown
137
137
  puts result
138
138
 
139
139
  puts "\n6b. Show Only Key Rows (Expanded Details)"
140
140
  result = ClassMetrix.extract(:class_methods)
141
- .from([CacheService])
142
- .filter(/config/)
143
- .expand_hashes
144
- .show_only_keys
145
- .to_markdown
141
+ .from([CacheService])
142
+ .filter(/config/)
143
+ .expand_hashes
144
+ .show_only_keys
145
+ .to_markdown
146
146
  puts result
147
147
 
148
148
  puts "\n6c. Show Both Main and Key Rows"
149
149
  result = ClassMetrix.extract(:class_methods)
150
- .from([CacheService])
151
- .filter(/config/)
152
- .expand_hashes
153
- .show_expanded_details
154
- .to_markdown
155
- puts result
150
+ .from([CacheService])
151
+ .filter(/config/)
152
+ .expand_hashes
153
+ .show_expanded_details
154
+ .to_markdown
155
+ puts result
@@ -48,7 +48,7 @@ module ClassMetrix
48
48
  @debug_mode = true
49
49
  @debug_level = level
50
50
  @logger = Utils::DebugLogger.new("Extractor", @debug_mode, level)
51
- @logger.log("Debug mode enabled (level: #{level})")
51
+ @logger&.log("Debug mode enabled (level: #{level})")
52
52
  self
53
53
  end
54
54
 
@@ -108,7 +108,7 @@ module ClassMetrix
108
108
  end
109
109
 
110
110
  def get_all_included_modules(klass)
111
- modules = []
111
+ modules = [] # : Array[Module]
112
112
  modules.concat(klass.included_modules)
113
113
 
114
114
  if @options[:include_inherited]
@@ -115,7 +115,7 @@ module ClassMetrix
115
115
  end
116
116
 
117
117
  def get_all_singleton_modules(klass)
118
- modules = []
118
+ modules = [] # : Array[Module]
119
119
  modules.concat(klass.singleton_class.included_modules)
120
120
 
121
121
  if @options[:include_inherited]
@@ -20,7 +20,7 @@ module ClassMetrix
20
20
  # Build headers: ["Type", "Behavior", "Class1", "Class2", ...]
21
21
  headers = %w[Type Behavior] + @classes.map(&:name)
22
22
 
23
- all_rows = []
23
+ all_rows = [] # : Array[Array[untyped]]
24
24
 
25
25
  @types.each do |type|
26
26
  type_data = extract_single_type(type)
@@ -28,7 +28,7 @@ module ClassMetrix
28
28
  # Add rows with type prefix
29
29
  type_data[:rows].each do |row|
30
30
  behavior_name = row[0]
31
- values = row[1..]
31
+ values = row[1..] || []
32
32
 
33
33
  new_row = [type_label(type), behavior_name] + values
34
34
  all_rows << new_row
@@ -26,7 +26,7 @@ module ClassMetrix
26
26
  # Common options for all formatters
27
27
  title: nil,
28
28
  show_metadata: true,
29
- extraction_types: []
29
+ extraction_types: [] # : Array[Symbol]
30
30
  }
31
31
  end
32
32
 
@@ -52,11 +52,11 @@ module ClassMetrix
52
52
 
53
53
  def collect_all_hash_keys(rows, _headers)
54
54
  value_start_idx = value_start_index
55
- all_keys = {} # behavior_name => Set of keys
55
+ all_keys = {} # : Hash[String, Set[String]] # behavior_name => Set of keys
56
56
 
57
57
  rows.each do |row|
58
58
  behavior_name = row[behavior_column_index]
59
- values = row[value_start_idx..]
59
+ values = row[value_start_idx..] || []
60
60
 
61
61
  values.each do |value|
62
62
  if value.is_a?(Hash)
@@ -16,7 +16,7 @@ module ClassMetrix
16
16
  def generate
17
17
  return [] unless @show_footer
18
18
 
19
- output = []
19
+ output = [] # : Array[String]
20
20
 
21
21
  # Add separator line
22
22
  output << "---" if @show_separator
@@ -36,7 +36,7 @@ module ClassMetrix
36
36
  private
37
37
 
38
38
  def generate_default_footer
39
- output = []
39
+ output = [] # : Array[String]
40
40
 
41
41
  output << (@custom_footer || "*Report generated by ClassMetrix gem*")
42
42
 
@@ -49,7 +49,7 @@ module ClassMetrix
49
49
  end
50
50
 
51
51
  def generate_detailed_footer
52
- output = []
52
+ output = [] # : Array[String]
53
53
 
54
54
  output << "## Report Information"
55
55
  output << ""
@@ -30,7 +30,7 @@ module ClassMetrix
30
30
  private
31
31
 
32
32
  def generate_markdown_header
33
- output = []
33
+ output = [] # : Array[String]
34
34
 
35
35
  # Add title
36
36
  if @title
@@ -58,7 +58,7 @@ module ClassMetrix
58
58
  end
59
59
 
60
60
  def generate_csv_header
61
- output = []
61
+ output = [] # : Array[String]
62
62
  comment_char = @options.fetch(:comment_char, "#")
63
63
 
64
64
  # Add title as comment
@@ -15,7 +15,7 @@ module ClassMetrix
15
15
  end
16
16
 
17
17
  def generate
18
- output = []
18
+ output = [] # : Array[String]
19
19
 
20
20
  # Add title
21
21
  output.concat(generate_title) if @title || @show_metadata
@@ -32,7 +32,7 @@ module ClassMetrix
32
32
  private
33
33
 
34
34
  def generate_title
35
- output = []
35
+ output = [] # : Array[String]
36
36
 
37
37
  if @title
38
38
  output << "# #{@title}"
@@ -46,7 +46,7 @@ module ClassMetrix
46
46
  end
47
47
 
48
48
  def generate_classes_section
49
- output = []
49
+ output = [] # : Array[String]
50
50
 
51
51
  has_type_column = @data[:headers].first == "Type"
52
52
  class_headers = if has_type_column
@@ -66,7 +66,7 @@ module ClassMetrix
66
66
  end
67
67
 
68
68
  def generate_extraction_info
69
- output = []
69
+ output = [] # : Array[String]
70
70
 
71
71
  output << "## Extraction Types"
72
72
  output << ""
@@ -9,7 +9,7 @@ module ClassMetrix
9
9
  @options = options
10
10
  @show_missing_summary = options.fetch(:show_missing_summary, false)
11
11
  @summary_style = options.fetch(:summary_style, :grouped) # :grouped, :flat, :detailed
12
- @missing_behaviors = {}
12
+ @missing_behaviors = {} # : Hash[String, Hash[String, String]]
13
13
  end
14
14
 
15
15
  def generate
@@ -30,7 +30,7 @@ module ClassMetrix
30
30
  end
31
31
 
32
32
  # Initialize missing behaviors tracking with hash to store behavior name and error message
33
- class_headers.each { |class_name| @missing_behaviors[class_name] = {} }
33
+ class_headers.each { |class_name| @missing_behaviors[class_name] = {} } # : Hash[String, Hash[String, String]]
34
34
 
35
35
  @data[:rows].each do |row|
36
36
  behavior_name = has_type_column ? row[1] : row[0]
@@ -63,7 +63,7 @@ module ClassMetrix
63
63
  end
64
64
 
65
65
  def generate_grouped_summary
66
- output = []
66
+ output = [] # : Array[String]
67
67
 
68
68
  output << "## Missing Behaviors Summary"
69
69
  output << ""
@@ -84,12 +84,12 @@ module ClassMetrix
84
84
  end
85
85
 
86
86
  def generate_flat_summary
87
- output = []
87
+ output = [] # : Array[String]
88
88
 
89
89
  output << "## Missing Behaviors"
90
90
  output << ""
91
91
 
92
- all_missing = []
92
+ all_missing = [] # : Array[String]
93
93
  @missing_behaviors.each do |class_name, behaviors_hash|
94
94
  behaviors_hash.each do |behavior_name, error_message|
95
95
  all_missing << "- **#{class_name}**: `#{behavior_name}` - #{error_message}"
@@ -103,7 +103,7 @@ module ClassMetrix
103
103
  end
104
104
 
105
105
  def generate_detailed_summary
106
- output = []
106
+ output = [] # : Array[String]
107
107
 
108
108
  total_missing = @missing_behaviors.values.map(&:size).sum
109
109
  total_classes = @missing_behaviors.keys.size
@@ -114,7 +114,7 @@ module ClassMetrix
114
114
  output << ""
115
115
 
116
116
  # Group by error type
117
- by_error_type = {}
117
+ by_error_type = {} # : Hash[String, Array[Hash[Symbol, String]]]
118
118
  @missing_behaviors.each do |class_name, behaviors_hash|
119
119
  behaviors_hash.each do |behavior_name, error_message|
120
120
  error_type = error_message.split.first(2).join(" ") # e.g., "🚫 Not", "⚠️ Error:"
@@ -16,7 +16,8 @@ module ClassMetrix
16
16
  def process_simple_rows(rows)
17
17
  rows.map do |row|
18
18
  processed_row = [row[0]] # Keep the behavior name as-is
19
- row[1..].each do |value|
19
+ rest_values = row[1..] || []
20
+ rest_values.each do |value|
20
21
  processed_row << ValueProcessor.process(value)
21
22
  end
22
23
  processed_row
@@ -24,7 +25,7 @@ module ClassMetrix
24
25
  end
25
26
 
26
27
  def process_expanded_rows(rows)
27
- expanded_rows = []
28
+ expanded_rows = [] # : Array[Array[String]]
28
29
 
29
30
  rows.each do |row|
30
31
  if @data_extractor.row_has_expandable_hash?(row)
@@ -41,9 +42,11 @@ module ClassMetrix
41
42
 
42
43
  def process_non_hash_row(row)
43
44
  if @data_extractor.has_type_column?
44
- [row[0], row[1]] + row[2..].map { |value| ValueProcessor.process(value) }
45
+ rest_values = row[2..] || []
46
+ [row[0], row[1]] + rest_values.map { |value| ValueProcessor.process(value) }
45
47
  else
46
- [row[0]] + row[1..].map { |value| ValueProcessor.process(value) }
48
+ rest_values = row[1..] || []
49
+ [row[0]] + rest_values.map { |value| ValueProcessor.process(value) }
47
50
  end
48
51
  end
49
52
 
@@ -57,7 +60,7 @@ module ClassMetrix
57
60
  end
58
61
 
59
62
  def build_expanded_rows(row_data, all_hash_keys, original_row)
60
- expanded_rows = []
63
+ expanded_rows = [] # : Array[Array[String]]
61
64
 
62
65
  # Add main row if configured to show
63
66
  expanded_rows << build_main_expanded_row(row_data) if should_show_main_row?
@@ -37,7 +37,10 @@ module ClassMetrix
37
37
  end
38
38
 
39
39
  def row_has_expandable_hash?(row)
40
- row[value_start_index..].any? { |cell| cell.is_a?(Hash) }
40
+ values = row[value_start_index..] || []
41
+ return false if values.nil?
42
+
43
+ values.any? { |cell| cell.is_a?(Hash) }
41
44
  end
42
45
 
43
46
  def collect_hash_keys(values)
@@ -11,7 +11,7 @@ module ClassMetrix
11
11
  end
12
12
 
13
13
  def render_table(headers, rows, column_widths)
14
- output = []
14
+ output = [] # : Array[String]
15
15
  output << build_row(headers, column_widths)
16
16
  output << build_separator(column_widths)
17
17
 
@@ -19,7 +19,7 @@ module ClassMetrix
19
19
  output << build_row(row, column_widths)
20
20
  end
21
21
 
22
- output.join("\n")
22
+ output
23
23
  end
24
24
 
25
25
  private
@@ -21,9 +21,10 @@ module ClassMetrix
21
21
 
22
22
  # Initialize helper objects
23
23
  @data_extractor = TableDataExtractor.new(@data[:headers])
24
- @row_processor = RowProcessor.new(@data_extractor,
25
- hide_main_row: @hide_main_row,
26
- hide_key_rows: @hide_key_rows)
24
+ @row_processor = RowProcessor.new(@data_extractor, {
25
+ hide_main_row: @hide_main_row,
26
+ hide_key_rows: @hide_key_rows
27
+ })
27
28
  @width_calculator = ColumnWidthCalculator.new(
28
29
  table_style: @table_style,
29
30
  min_column_width: @min_column_width,
@@ -36,7 +37,7 @@ module ClassMetrix
36
37
  end
37
38
 
38
39
  def generate
39
- return "" if @data[:headers].empty? || @data[:rows].empty?
40
+ return [""] if @data[:headers].empty? || @data[:rows].empty?
40
41
 
41
42
  headers = @data[:headers]
42
43
  rows = if @expand_hashes
@@ -15,7 +15,7 @@ module ClassMetrix
15
15
  def format
16
16
  return "" if @data[:headers].empty? || @data[:rows].empty?
17
17
 
18
- output_lines = []
18
+ output_lines = [] # : Array[String]
19
19
 
20
20
  # Add CSV header comments
21
21
  if @options.fetch(:show_metadata, true)
@@ -65,7 +65,7 @@ module ClassMetrix
65
65
 
66
66
  return [] if headers.empty?
67
67
 
68
- output = []
68
+ output = [] # : Array[String]
69
69
  separator = @options.fetch(:separator, ",")
70
70
  quote_char = @options.fetch(:quote_char, '"')
71
71
 
@@ -75,7 +75,7 @@ module ClassMetrix
75
75
  csv_rows.each do |row|
76
76
  # Ensure all row values are strings and properly quoted
77
77
  formatted_row = row.map { |cell| format_csv_cell(cell) }
78
- csv_line = CSV.generate_line(formatted_row, col_sep: separator, quote_char: quote_char).chomp
78
+ csv_line = CSV.generate(col_sep: separator, quote_char: quote_char) { |csv| csv << formatted_row }.chomp
79
79
  output << csv_line
80
80
  end
81
81
 
@@ -16,7 +16,7 @@ module ClassMetrix
16
16
  def format
17
17
  return "" if @data[:headers].empty? || @data[:rows].empty?
18
18
 
19
- output = []
19
+ output = [] # : Array[String]
20
20
 
21
21
  # Add header sections (title, classes, extraction info)
22
22
  if @options.fetch(:show_metadata, true)
@@ -77,7 +77,7 @@ module ClassMetrix
77
77
 
78
78
  widths = calculate_column_widths(headers, rows)
79
79
 
80
- output = []
80
+ output = [] # : Array[String]
81
81
  output << build_header_row(headers, widths)
82
82
  output << build_separator_row(widths)
83
83
  output.concat(build_data_rows(rows, headers, widths))
@@ -170,8 +170,7 @@ module ClassMetrix
170
170
  if max_width >= 15
171
171
  # Look for the first complete key-value pair (handle :symbol=>value format)
172
172
  match = text.match(/\{(:[^,}]+=>[^,}]+)/)
173
- if match
174
- first_pair = match[1]
173
+ if match && (first_pair = match[1])
175
174
  needed_length = first_pair.length + 6 # For "{", ", ...}"
176
175
  return "{#{first_pair}, ...}" if needed_length <= max_width
177
176
  end
@@ -19,7 +19,7 @@ module ClassMetrix
19
19
  # Create proper flat table structure for hash expansion
20
20
  def expand_row(row, _headers)
21
21
  behavior_name = row[behavior_column_index]
22
- values = row[value_start_index..]
22
+ values = row[value_start_index..] || []
23
23
 
24
24
  all_hash_keys = collect_unique_hash_keys(values)
25
25
  return [process_row(row)] if all_hash_keys.empty?
@@ -36,7 +36,7 @@ module ClassMetrix
36
36
  end
37
37
 
38
38
  def build_expanded_row_structure(row, behavior_name, values, all_hash_keys)
39
- expanded_rows = []
39
+ expanded_rows = [] # : Array[Array[String]]
40
40
 
41
41
  # Add main row if configured to show
42
42
  expanded_rows << build_main_row(row, behavior_name, values) if should_show_main_row?
@@ -63,7 +63,7 @@ module ClassMetrix
63
63
  private
64
64
 
65
65
  def process_rows_for_expansion(headers)
66
- expanded_rows = []
66
+ expanded_rows = [] # : Array[Array[String]]
67
67
  expandable_count = 0
68
68
 
69
69
  @data[:rows].each_with_index do |row, index|
@@ -88,6 +88,8 @@ module ClassMetrix
88
88
  @logger.log_hash_detection_summary(values)
89
89
 
90
90
  # Only consider real Hash objects as expandable
91
+ return false if values.nil?
92
+
91
93
  result = values.any? { |cell| cell.is_a?(Hash) && cell.instance_of?(Hash) }
92
94
  @logger.log_decision("Row expandable", "#{result ? "Has" : "No"} real Hash objects", :detailed)
93
95
  result
@@ -130,7 +132,7 @@ module ClassMetrix
130
132
 
131
133
  def expand_row(row, _headers)
132
134
  behavior_name = row[behavior_column_index]
133
- values = row[value_start_index..]
135
+ values = row[value_start_index..] || []
134
136
 
135
137
  @logger.log("Expanding row for behavior '#{behavior_name}'")
136
138
 
@@ -161,7 +163,7 @@ module ClassMetrix
161
163
  end
162
164
 
163
165
  def build_expanded_row_set(row, behavior_name, values, all_hash_keys)
164
- expanded_rows = []
166
+ expanded_rows = [] # : Array[Array[String]]
165
167
 
166
168
  # Add main row if configured to show
167
169
  expanded_rows << build_main_row(row, behavior_name, values) if should_show_main_row?
@@ -232,7 +234,7 @@ module ClassMetrix
232
234
 
233
235
  def collect_all_hash_keys(rows, _headers)
234
236
  value_start_idx = value_start_index
235
- all_keys = {} # behavior_name => Set of keys
237
+ all_keys = {} # : Hash[String, Set[String]] # behavior_name => Set of keys
236
238
  total_hash_count = 0
237
239
 
238
240
  rows.each_with_index do |row, index|
@@ -247,7 +249,7 @@ module ClassMetrix
247
249
 
248
250
  def collect_hash_keys_for_row(row, value_start_idx, all_keys)
249
251
  behavior_name = row[behavior_column_index]
250
- values = row[value_start_idx..]
252
+ values = row[value_start_idx..] || []
251
253
  hash_count = 0
252
254
 
253
255
  values.each_with_index do |value, index|
@@ -299,7 +301,7 @@ module ClassMetrix
299
301
  def add_flattened_hash_values(flattened, row, behavior_name, all_hash_keys)
300
302
  return unless all_hash_keys[behavior_name]
301
303
 
302
- values = row[value_start_index..]
304
+ values = row[value_start_index..] || []
303
305
 
304
306
  all_hash_keys[behavior_name].to_a.sort.each do |key|
305
307
  add_flattened_values_for_key(flattened, values, key)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClassMetrix
4
- VERSION = "1.0.0"
4
+ VERSION = "1.1.0"
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