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
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/class_metrix"
5
+
6
+ puts "ClassMetrix: Inheritance & Module Analysis"
7
+ puts "=" * 50
8
+
9
+ # Example: Service Configuration Analysis
10
+ # This demonstrates analyzing service classes that use inheritance and modules
11
+
12
+ module Configurable
13
+ DEFAULT_TIMEOUT = 30
14
+
15
+ def self.included(base)
16
+ base.extend(ClassMethods)
17
+ end
18
+
19
+ module ClassMethods
20
+ def configuration
21
+ { timeout: DEFAULT_TIMEOUT, enabled: true }
22
+ end
23
+ end
24
+ end
25
+
26
+ module Cacheable
27
+ CACHE_TTL = 3600
28
+
29
+ def self.included(base)
30
+ base.extend(ClassMethods)
31
+ end
32
+
33
+ module ClassMethods
34
+ def cache_config
35
+ { ttl: CACHE_TTL, enabled: true }
36
+ end
37
+ end
38
+ end
39
+
40
+ class BaseService
41
+ SERVICE_VERSION = "1.0"
42
+
43
+ def self.service_type
44
+ "base"
45
+ end
46
+
47
+ def self.health_check
48
+ { status: "ok", version: SERVICE_VERSION }
49
+ end
50
+ end
51
+
52
+ class DatabaseService < BaseService
53
+ include Configurable
54
+
55
+ SERVICE_NAME = "database"
56
+ CONNECTION_POOL_SIZE = 5
57
+
58
+ def self.service_type
59
+ "database"
60
+ end
61
+
62
+ def self.connection_config
63
+ { pool_size: CONNECTION_POOL_SIZE, timeout: 60 }
64
+ end
65
+ end
66
+
67
+ class CacheService < BaseService
68
+ include Configurable
69
+ include Cacheable
70
+
71
+ SERVICE_NAME = "cache"
72
+ MAX_MEMORY = "512MB"
73
+
74
+ def self.service_type
75
+ "cache"
76
+ end
77
+
78
+ def self.memory_config
79
+ { max_memory: MAX_MEMORY, eviction_policy: "lru" }
80
+ end
81
+ end
82
+
83
+ # Demo the functionality
84
+ services = [DatabaseService, CacheService]
85
+
86
+ puts "\n1. Basic Analysis (Own Constants Only)"
87
+ puts "-" * 40
88
+ result = ClassMetrix.extract(:constants)
89
+ .from(services)
90
+ .to_markdown
91
+ puts result
92
+
93
+ puts "\n2. With Inheritance (Own + Parent Constants)"
94
+ puts "-" * 40
95
+ result = ClassMetrix.extract(:constants)
96
+ .from(services)
97
+ .include_inherited
98
+ .to_markdown
99
+ puts result
100
+
101
+ puts "\n3. With Modules (Own + Module Constants)"
102
+ puts "-" * 40
103
+ result = ClassMetrix.extract(:constants)
104
+ .from(services)
105
+ .include_modules
106
+ .to_markdown
107
+ puts result
108
+
109
+ puts "\n4. Complete Analysis (Own + Inherited + Modules)"
110
+ puts "-" * 40
111
+ result = ClassMetrix.extract(:constants, :class_methods)
112
+ .from(services)
113
+ .include_all
114
+ .handle_errors
115
+ .to_markdown
116
+ puts result
117
+
118
+ puts "\n5. Filtered Configuration Analysis"
119
+ puts "-" * 40
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
126
+ puts result
127
+
128
+ puts "\n6. Hash Expansion Modes"
129
+ puts "-" * 40
130
+
131
+ puts "\n6a. Default: Show Only Main Rows (Collapsed Hashes)"
132
+ result = ClassMetrix.extract(:class_methods)
133
+ .from([CacheService])
134
+ .filter(/config/)
135
+ .expand_hashes
136
+ .to_markdown
137
+ puts result
138
+
139
+ puts "\n6b. Show Only Key Rows (Expanded Details)"
140
+ result = ClassMetrix.extract(:class_methods)
141
+ .from([CacheService])
142
+ .filter(/config/)
143
+ .expand_hashes
144
+ .show_only_keys
145
+ .to_markdown
146
+ puts result
147
+
148
+ puts "\n6c. Show Both Main and Key Rows"
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
@@ -6,6 +6,7 @@ require_relative "extractors/multi_type_extractor"
6
6
  require_relative "formatters/markdown_formatter"
7
7
  require_relative "formatters/csv_formatter"
8
8
  require_relative "utils/class_resolver"
9
+ require_relative "utils/debug_logger"
9
10
 
10
11
  module ClassMetrix
11
12
  class Extractor
@@ -16,10 +17,19 @@ module ClassMetrix
16
17
  @expand_hashes = false
17
18
  @handle_errors = false
18
19
  @modules = []
20
+ @include_inherited = false
21
+ @include_modules = false
22
+ @show_source = false
23
+ @hide_main_row = false
24
+ @hide_key_rows = true # Default: show only main rows
25
+ @debug_mode = false
26
+ @debug_level = :basic
27
+ @logger = nil # Will be initialized when debug mode is enabled
19
28
  end
20
29
 
21
30
  def from(classes)
22
31
  @classes = ClassResolver.normalize_classes(classes)
32
+ @logger&.log("Normalized classes: #{@classes.map(&:name)}")
23
33
  self
24
34
  end
25
35
 
@@ -30,6 +40,15 @@ module ClassMetrix
30
40
 
31
41
  def expand_hashes
32
42
  @expand_hashes = true
43
+ @logger&.log("Hash expansion enabled")
44
+ self
45
+ end
46
+
47
+ def debug(level = :basic)
48
+ @debug_mode = true
49
+ @debug_level = level
50
+ @logger = Utils::DebugLogger.new("Extractor", @debug_mode, level)
51
+ @logger&.log("Debug mode enabled (level: #{level})")
33
52
  self
34
53
  end
35
54
 
@@ -43,10 +62,63 @@ module ClassMetrix
43
62
  self
44
63
  end
45
64
 
65
+ # Inheritance and module inclusion options
66
+ def include_inherited
67
+ @include_inherited = true
68
+ self
69
+ end
70
+
71
+ def include_modules
72
+ @include_modules = true
73
+ self
74
+ end
75
+
76
+ def show_source
77
+ @show_source = true
78
+ self
79
+ end
80
+
81
+ def include_all
82
+ @include_inherited = true
83
+ @include_modules = true
84
+ self
85
+ end
86
+
87
+ # Hash expansion display options
88
+ def show_only_main
89
+ @hide_main_row = false
90
+ @hide_key_rows = true
91
+ self
92
+ end
93
+
94
+ def show_only_keys
95
+ @hide_main_row = true
96
+ @hide_key_rows = false
97
+ self
98
+ end
99
+
100
+ def show_expanded_details
101
+ @hide_main_row = false
102
+ @hide_key_rows = false
103
+ self
104
+ end
105
+
106
+ # Lower-level options (for advanced usage)
107
+ def hide_main_row
108
+ @hide_main_row = true
109
+ self
110
+ end
111
+
112
+ def hide_key_rows
113
+ @hide_key_rows = true
114
+ self
115
+ end
116
+
46
117
  def to_markdown(filename = nil, **options)
118
+ @logger&.log("Starting markdown generation...")
47
119
  data = extract_all_data
120
+ @logger&.log("Extracted data structure with #{data[:rows]&.length || 0} rows")
48
121
 
49
- # Merge default options with passed options
50
122
  format_options = {
51
123
  extraction_types: @types,
52
124
  show_missing_summary: false,
@@ -57,7 +129,11 @@ module ClassMetrix
57
129
  show_classes: true,
58
130
  show_extraction_info: true,
59
131
  table_style: :standard,
60
- summary_style: :grouped
132
+ summary_style: :grouped,
133
+ hide_main_row: @hide_main_row,
134
+ hide_key_rows: @hide_key_rows,
135
+ debug_mode: @debug_mode,
136
+ debug_level: @debug_level
61
137
  }.merge(options)
62
138
 
63
139
  formatted = MarkdownFormatter.new(data, @expand_hashes, format_options).format
@@ -67,9 +143,9 @@ module ClassMetrix
67
143
  end
68
144
 
69
145
  def to_csv(filename = nil, **options)
146
+ @logger&.log("Starting CSV generation...")
70
147
  data = extract_all_data
71
148
 
72
- # Merge default options with passed options
73
149
  format_options = {
74
150
  extraction_types: @types,
75
151
  show_metadata: true,
@@ -77,7 +153,11 @@ module ClassMetrix
77
153
  quote_char: '"',
78
154
  flatten_hashes: true,
79
155
  null_value: "",
80
- comment_char: "#"
156
+ comment_char: "#",
157
+ hide_main_row: @hide_main_row,
158
+ hide_key_rows: @hide_key_rows,
159
+ debug_mode: @debug_mode,
160
+ debug_level: @debug_level
81
161
  }.merge(options)
82
162
 
83
163
  formatted = CsvFormatter.new(data, @expand_hashes, format_options).format
@@ -89,7 +169,7 @@ module ClassMetrix
89
169
  private
90
170
 
91
171
  def extract_all_data
92
- # Handle single or multiple extraction types
172
+ @logger&.log("Extracting data for types: #{@types}")
93
173
  if @types.size == 1
94
174
  extract_single_type(@types.first)
95
175
  else
@@ -98,24 +178,39 @@ module ClassMetrix
98
178
  end
99
179
 
100
180
  def extract_single_type(type)
101
- extractor = get_extractor(type)
102
- extractor.extract
181
+ @logger&.log("Extracting single type: #{type}")
182
+ get_extractor(type).extract
103
183
  end
104
184
 
105
185
  def extract_multiple_types
106
- # Combine multiple extraction types into one table
107
- MultiTypeExtractor.new(@classes, @types, @filters, @modules, @handle_errors).extract
186
+ @logger&.log("Extracting multiple types: #{@types}")
187
+ extraction_config = {
188
+ modules: @modules,
189
+ handle_errors: @handle_errors,
190
+ options: extraction_options
191
+ }
192
+ MultiTypeExtractor.new(@classes, @types, @filters, extraction_config).extract
108
193
  end
109
194
 
110
195
  def get_extractor(type)
111
196
  case type
112
197
  when :constants
113
- ConstantsExtractor.new(@classes, @filters, @handle_errors)
198
+ ConstantsExtractor.new(@classes, @filters, @handle_errors, extraction_options)
114
199
  when :class_methods
115
- MethodsExtractor.new(@classes, @filters, @handle_errors)
200
+ MethodsExtractor.new(@classes, @filters, @handle_errors, extraction_options)
116
201
  else
117
202
  raise ArgumentError, "Unknown extraction type: #{type}"
118
203
  end
119
204
  end
205
+
206
+ def extraction_options
207
+ {
208
+ include_inherited: @include_inherited,
209
+ include_modules: @include_modules,
210
+ show_source: @show_source,
211
+ debug_mode: @debug_mode,
212
+ debug_level: @debug_level
213
+ }
214
+ end
120
215
  end
121
216
  end
@@ -1,57 +1,131 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../processors/value_processor"
4
+ require_relative "../utils/debug_logger"
4
5
 
5
6
  module ClassMetrix
6
7
  class ConstantsExtractor
7
- def initialize(classes, filters, handle_errors)
8
+ def initialize(classes, filters, handle_errors, options = {})
8
9
  @classes = classes
9
10
  @filters = filters
10
11
  @handle_errors = handle_errors
12
+ @options = default_options.merge(options)
13
+ @debug_level = @options[:debug_level] || :basic
14
+ @logger = Utils::DebugLogger.new("ConstantsExtractor", @options[:debug_mode], @debug_level)
11
15
  end
12
16
 
13
17
  def extract
14
18
  return { headers: [], rows: [] } if @classes.empty?
15
19
 
16
- # Get all constant names across all classes
17
20
  constant_names = get_all_constant_names
18
-
19
- # Apply filters
20
21
  constant_names = apply_filters(constant_names)
22
+ headers = build_headers
23
+ rows = build_rows(constant_names)
21
24
 
22
- # Build headers: ["Constant", "Class1", "Class2", ...]
23
- headers = ["Constant"] + @classes.map(&:name)
25
+ { headers: headers, rows: rows }
26
+ end
24
27
 
25
- # Build rows: each row represents one constant across all classes
26
- rows = constant_names.map do |const_name|
27
- row = [const_name]
28
+ private
29
+
30
+ def default_options
31
+ {
32
+ include_inherited: false,
33
+ include_modules: false,
34
+ show_source: false
35
+ }
36
+ end
37
+
38
+ def build_headers
39
+ if @options[:show_source]
40
+ ["Constant (Source)"] + @classes.map(&:name)
41
+ else
42
+ ["Constant"] + @classes.map(&:name)
43
+ end
44
+ end
28
45
 
46
+ def build_rows(constant_names)
47
+ constant_names.map do |const_name|
48
+ row = [const_name]
29
49
  @classes.each do |klass|
30
50
  value = extract_constant_value(klass, const_name)
31
- # Pass the raw value for hash expansion to work properly
32
51
  row << value
33
52
  end
34
-
35
53
  row
36
54
  end
37
-
38
- { headers: headers, rows: rows }
39
55
  end
40
56
 
41
- private
42
-
43
57
  def get_all_constant_names
44
58
  all_constants = Set.new
45
59
 
46
60
  @classes.each do |klass|
47
- # Get constants defined directly in this class (not inherited)
48
- class_constants = klass.constants(false)
49
- all_constants.merge(class_constants.map(&:to_s))
61
+ constants = if inheritance_or_modules_enabled?
62
+ get_comprehensive_constants(klass)
63
+ else
64
+ klass.constants(false)
65
+ end
66
+ all_constants.merge(constants.map(&:to_s))
50
67
  end
51
68
 
52
69
  all_constants.to_a.sort
53
70
  end
54
71
 
72
+ def inheritance_or_modules_enabled?
73
+ @options[:include_inherited] || @options[:include_modules]
74
+ end
75
+
76
+ def get_comprehensive_constants(klass)
77
+ constants = Set.new
78
+ constants.merge(klass.constants(false))
79
+
80
+ constants.merge(get_inherited_constants(klass)) if @options[:include_inherited]
81
+
82
+ constants.merge(get_module_constants(klass)) if @options[:include_modules]
83
+
84
+ constants.to_a
85
+ end
86
+
87
+ def get_inherited_constants(klass)
88
+ constants = Set.new
89
+ parent = klass.superclass
90
+
91
+ while parent && !core_class?(parent)
92
+ constants.merge(parent.constants(false))
93
+ parent = parent.superclass
94
+ end
95
+
96
+ constants
97
+ end
98
+
99
+ def get_module_constants(klass)
100
+ constants = Set.new
101
+ all_modules = get_all_included_modules(klass)
102
+
103
+ all_modules.each do |mod|
104
+ constants.merge(mod.constants(false))
105
+ end
106
+
107
+ constants
108
+ end
109
+
110
+ def get_all_included_modules(klass)
111
+ modules = [] # : Array[Module]
112
+ modules.concat(klass.included_modules)
113
+
114
+ if @options[:include_inherited]
115
+ parent = klass.superclass
116
+ while parent && !core_class?(parent)
117
+ modules.concat(parent.included_modules)
118
+ parent = parent.superclass
119
+ end
120
+ end
121
+
122
+ modules
123
+ end
124
+
125
+ def core_class?(klass)
126
+ [Object, BasicObject].include?(klass)
127
+ end
128
+
55
129
  def apply_filters(constant_names)
56
130
  return constant_names if @filters.empty?
57
131
 
@@ -72,16 +146,76 @@ module ClassMetrix
72
146
  end
73
147
 
74
148
  def extract_constant_value(klass, const_name)
75
- # Check if constant exists before trying to get it
76
- if klass.const_defined?(const_name, false)
77
- klass.const_get(const_name)
149
+ constant_info = find_constant_source(klass, const_name)
150
+
151
+ if constant_info
152
+ value = constant_info[:value]
153
+ debug_log("Extracted constant '#{const_name}' from #{klass.name}: #{@logger.safe_inspect(value)} (#{@logger.safe_class(value)})")
154
+ value
78
155
  else
156
+ debug_log("Constant '#{const_name}' not found in #{klass.name}")
79
157
  @handle_errors ? ValueProcessor.missing_constant : nil
80
158
  end
81
159
  rescue NameError => e
160
+ debug_log("NameError extracting constant '#{const_name}' from #{klass.name}: #{e.message}")
82
161
  @handle_errors ? ValueProcessor.handle_extraction_error(e) : (raise e)
83
162
  rescue StandardError => e
163
+ debug_log("Error extracting constant '#{const_name}' from #{klass.name}: #{e.message}")
84
164
  @handle_errors ? ValueProcessor.handle_extraction_error(e) : (raise e)
85
165
  end
166
+
167
+ def find_constant_source(klass, const_name)
168
+ # Check own constants first
169
+ return build_constant_info(klass.const_get(const_name), klass.name, :own) if klass.const_defined?(const_name, false)
170
+
171
+ # Check inherited constants
172
+ if @options[:include_inherited]
173
+ inherited_info = find_inherited_constant(klass, const_name)
174
+ return inherited_info if inherited_info
175
+ end
176
+
177
+ # Check module constants
178
+ if @options[:include_modules]
179
+ module_info = find_module_constant(klass, const_name)
180
+ return module_info if module_info
181
+ end
182
+
183
+ # Fallback check with inheritance
184
+ return build_constant_info(klass.const_get(const_name), "inherited", :unknown) if klass.const_defined?(const_name, true)
185
+
186
+ nil
187
+ end
188
+
189
+ def find_inherited_constant(klass, const_name)
190
+ parent = klass.superclass
191
+ while parent && !core_class?(parent)
192
+ return build_constant_info(parent.const_get(const_name), parent.name, :inherited) if parent.const_defined?(const_name, false)
193
+
194
+ parent = parent.superclass
195
+ end
196
+ nil
197
+ end
198
+
199
+ def find_module_constant(klass, const_name)
200
+ all_modules = get_all_included_modules(klass)
201
+
202
+ all_modules.each do |mod|
203
+ return build_constant_info(mod.const_get(const_name), mod.name, :module) if mod.const_defined?(const_name, false)
204
+ end
205
+
206
+ nil
207
+ end
208
+
209
+ def build_constant_info(value, source, type)
210
+ {
211
+ value: value,
212
+ source: source,
213
+ type: type
214
+ }
215
+ end
216
+
217
+ def debug_log(message)
218
+ @logger.log(message)
219
+ end
86
220
  end
87
221
  end