Module: Woods::Extractors::SharedUtilityMethods
- Included in:
- ActionCableExtractor, CachingExtractor, ConcernExtractor, ConfigurationExtractor, ControllerExtractor, DatabaseViewExtractor, DecoratorExtractor, EngineExtractor, EventExtractor, FactoryExtractor, GraphQLExtractor, JobExtractor, LibExtractor, MailerExtractor, ManagerExtractor, MigrationExtractor, ModelExtractor, PhlexExtractor, PolicyExtractor, PoroExtractor, PunditExtractor, RakeTaskExtractor, RouteExtractor, SerializerExtractor, ServiceExtractor, StateMachineExtractor, TestMappingExtractor, ValidatorExtractor, ViewComponentExtractor
- Defined in:
- lib/woods/extractors/shared_utility_methods.rb
Overview
Utility methods shared across multiple extractors.
Provides common helpers for namespace extraction, public method scanning, class method scanning, and initialize parameter parsing. These methods are duplicated across 4-11 extractors; this module centralizes them.
Instance Method Summary collapse
-
#app_source?(path, app_root) ⇒ Boolean
Check whether a path points to application source (under app_root, but not inside vendor/ or node_modules/ directories).
-
#condition_label(condition) ⇒ String
Human-readable label for a non-ActionFilter condition.
-
#count_loc(source) ⇒ Integer
Count non-blank, non-comment lines of code.
-
#detect_entry_points(source) ⇒ Array<String>
Detect common entry point methods in a source file.
-
#extract_action_filter_actions(condition) ⇒ Array<String>?
Extract action names from an ActionFilter-like condition object.
-
#extract_callback_conditions(callback) ⇒ Array(Array<String>, Array<String>, Array<String>, Array<String>)
Extract :only/:except action lists and :if/:unless conditions from a callback.
-
#extract_class_methods(source) ⇒ Array<String>
Extract class-level (self.) method names from source code.
-
#extract_class_name(file_path, source, dir_prefix) ⇒ String
Extract the primary class name from source or fall back to a file path convention.
-
#extract_custom_errors(source) ⇒ Array<String>
Extract custom error/exception class names defined inline.
-
#extract_initialize_params(source) ⇒ Array<Hash>
Extract initialize parameters from source code via regex.
-
#extract_namespace(name_or_object) ⇒ String?
Extract namespace from a class name string or class object.
-
#extract_parent_class(source) ⇒ String?
Extract the parent class name from a class definition.
-
#extract_public_methods(source) ⇒ Array<String>
Extract public instance and class methods from source code.
-
#resolve_source_location(klass, app_root:, fallback:) ⇒ String
Resolve the source file for a class using reliable introspection, filtered through #app_source? to reject vendor/gem paths.
-
#skip_file?(source) ⇒ Boolean
Skip module-only files (concerns, base modules without a class).
Instance Method Details
#app_source?(path, app_root) ⇒ Boolean
Check whether a path points to application source (under app_root, but not inside vendor/ or node_modules/ directories).
In Docker environments where Rails.root is ‘/app`, a naive `start_with?(app_root)` also matches vendor bundle paths like `/app/vendor/bundle/ruby/…`. This helper rejects those.
33 34 35 36 37 |
# File 'lib/woods/extractors/shared_utility_methods.rb', line 33 def app_source?(path, app_root) return false unless path path.start_with?(app_root) && !path.include?('/vendor/') && !path.include?('/node_modules/') end |
#condition_label(condition) ⇒ String
Human-readable label for a non-ActionFilter condition.
192 193 194 195 196 197 198 199 |
# File 'lib/woods/extractors/shared_utility_methods.rb', line 192 def condition_label(condition) case condition when Symbol then ":#{condition}" when Proc then 'Proc' when String then condition else condition.class.name end end |
#count_loc(source) ⇒ Integer
Count non-blank, non-comment lines of code.
101 102 103 |
# File 'lib/woods/extractors/shared_utility_methods.rb', line 101 def count_loc(source) source.lines.count { |l| l.strip.length.positive? && !l.strip.start_with?('#') } end |
#detect_entry_points(source) ⇒ Array<String>
Detect common entry point methods in a source file.
125 126 127 128 129 130 131 132 133 |
# File 'lib/woods/extractors/shared_utility_methods.rb', line 125 def detect_entry_points(source) points = [] points << 'call' if source.match?(/def (self\.)?call\b/) points << 'perform' if source.match?(/def (self\.)?perform\b/) points << 'execute' if source.match?(/def (self\.)?execute\b/) points << 'run' if source.match?(/def (self\.)?run\b/) points << 'process' if source.match?(/def (self\.)?process\b/) points.empty? ? ['unknown'] : points end |
#extract_action_filter_actions(condition) ⇒ Array<String>?
Extract action names from an ActionFilter-like condition object. Duck-types on the @actions ivar being a Set, avoiding dependence on private class names across Rails versions.
179 180 181 182 183 184 185 186 |
# File 'lib/woods/extractors/shared_utility_methods.rb', line 179 def extract_action_filter_actions(condition) return nil unless condition.instance_variable_defined?(:@actions) actions = condition.instance_variable_get(:@actions) return nil unless actions.is_a?(Set) actions.to_a end |
#extract_callback_conditions(callback) ⇒ Array(Array<String>, Array<String>, Array<String>, Array<String>)
Extract :only/:except action lists and :if/:unless conditions from a callback.
Modern Rails (4.2+) stores conditions in @if/@unless ivar arrays. ActionFilter objects hold action Sets; other conditions are procs/symbols.
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 |
# File 'lib/woods/extractors/shared_utility_methods.rb', line 143 def extract_callback_conditions(callback) if_conditions = callback.instance_variable_get(:@if) || [] unless_conditions = callback.instance_variable_get(:@unless) || [] only = [] except = [] if_labels = [] unless_labels = [] if_conditions.each do |cond| actions = extract_action_filter_actions(cond) if actions only.concat(actions) else if_labels << condition_label(cond) end end unless_conditions.each do |cond| actions = extract_action_filter_actions(cond) if actions except.concat(actions) else unless_labels << condition_label(cond) end end [only, except, if_labels, unless_labels] end |
#extract_class_methods(source) ⇒ Array<String>
Extract class-level (self.) method names from source code.
247 248 249 |
# File 'lib/woods/extractors/shared_utility_methods.rb', line 247 def extract_class_methods(source) source.scan(/def\s+self\.(\w+[?!=]?)/).flatten end |
#extract_class_name(file_path, source, dir_prefix) ⇒ String
Extract the primary class name from source or fall back to a file path convention.
82 83 84 85 86 |
# File 'lib/woods/extractors/shared_utility_methods.rb', line 82 def extract_class_name(file_path, source, dir_prefix) return ::Regexp.last_match(1) if source =~ /^\s*class\s+([\w:]+)/ file_path.sub("#{Rails.root}/", '').sub(%r{^app/#{dir_prefix}/}, '').sub('.rb', '').camelize end |
#extract_custom_errors(source) ⇒ Array<String>
Extract custom error/exception class names defined inline.
117 118 119 |
# File 'lib/woods/extractors/shared_utility_methods.rb', line 117 def extract_custom_errors(source) source.scan(/class\s+(\w+(?:Error|Exception))\s*</).flatten end |
#extract_initialize_params(source) ⇒ Array<Hash>
Extract initialize parameters from source code via regex.
Parses the parameter list of the initialize method to determine parameter names, defaults, and whether they are keyword arguments.
Note: PhlexExtractor and ViewComponentExtractor override this with a runtime-introspection version that takes a Class object instead of source text, providing richer type information (:req, :opt, :keyreq, :rest, etc.).
262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 |
# File 'lib/woods/extractors/shared_utility_methods.rb', line 262 def extract_initialize_params(source) init_match = source.match(/def\s+initialize\s*\((.*?)\)/m) return [] unless init_match params_str = init_match[1] params = [] params_str.scan(/(\w+)(?::\s*([^,\n]+))?/) do |name, default| params << { name: name, has_default: !default.nil?, keyword: params_str.include?("#{name}:") } end params end |
#extract_namespace(name_or_object) ⇒ String?
Extract namespace from a class name string or class object.
Handles both string input (e.g., “Payments::StripeService”) and class object input (e.g., a Controller class).
208 209 210 211 212 |
# File 'lib/woods/extractors/shared_utility_methods.rb', line 208 def extract_namespace(name_or_object) name = name_or_object.is_a?(String) ? name_or_object : name_or_object.name parts = name.split('::') parts.size > 1 ? parts[0..-2].join('::') : nil end |
#extract_parent_class(source) ⇒ String?
Extract the parent class name from a class definition.
92 93 94 95 |
# File 'lib/woods/extractors/shared_utility_methods.rb', line 92 def extract_parent_class(source) match = source.match(/^\s*class\s+[\w:]+\s*<\s*([\w:]+)/) match ? match[1] : nil end |
#extract_public_methods(source) ⇒ Array<String>
Extract public instance and class methods from source code.
Walks source line-by-line tracking private/protected visibility. Returns method names that are in public scope and don’t start with underscore.
221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 |
# File 'lib/woods/extractors/shared_utility_methods.rb', line 221 def extract_public_methods(source) methods = [] in_private = false in_protected = false source.each_line do |line| stripped = line.strip in_private = true if stripped == 'private' in_protected = true if stripped == 'protected' in_private = false if stripped == 'public' in_protected = false if stripped == 'public' if !in_private && !in_protected && stripped =~ /def\s+((?:self\.)?\w+[?!=]?)/ method_name = ::Regexp.last_match(1) methods << method_name unless method_name.start_with?('_') end end methods end |
#resolve_source_location(klass, app_root:, fallback:) ⇒ String
Resolve the source file for a class using reliable introspection, filtered through #app_source? to reject vendor/gem paths.
Tier order:
1. +const_source_location+ (returns the class definition site)
2. Instance method source locations (first match wins)
3. Class/singleton method source locations (first match wins)
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
# File 'lib/woods/extractors/shared_utility_methods.rb', line 51 def resolve_source_location(klass, app_root:, fallback:) # Tier 1: const_source_location (most reliable — returns class definition site) if Object.respond_to?(:const_source_location) && klass.name loc = Object.const_source_location(klass.name)&.first return loc if app_source?(loc, app_root) end # Tier 2: Instance methods defined directly on this class klass.instance_methods(false).each do |method_name| loc = klass.instance_method(method_name).source_location&.first return loc if app_source?(loc, app_root) end # Tier 3: Class/singleton methods defined on this class klass.methods(false).each do |method_name| loc = klass.method(method_name).source_location&.first return loc if app_source?(loc, app_root) end fallback rescue StandardError fallback end |
#skip_file?(source) ⇒ Boolean
Skip module-only files (concerns, base modules without a class).
109 110 111 |
# File 'lib/woods/extractors/shared_utility_methods.rb', line 109 def skip_file?(source) source.match?(/^\s*module\s+[\w:]+\s*$/) && !source.match?(/^\s*class\s+/) end |