Module: Woods::Extractors::SharedUtilityMethods
- Included in:
- ActionCableExtractor, CachingExtractor, ConcernExtractor, ConfigurationExtractor, ControllerExtractor, DatabaseViewExtractor, DecoratorExtractor, EngineExtractor, EventExtractor, FactoryExtractor, GraphQLExtractor, I18nExtractor, 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.
-
#find_files_in_directories(directories, pattern = '**/*.rb') ⇒ Array<String>
Glob files matching a pattern across a list of directories.
-
#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.
47 48 49 50 51 |
# File 'lib/woods/extractors/shared_utility_methods.rb', line 47 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.
206 207 208 209 210 211 212 213 |
# File 'lib/woods/extractors/shared_utility_methods.rb', line 206 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.
115 116 117 |
# File 'lib/woods/extractors/shared_utility_methods.rb', line 115 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.
139 140 141 142 143 144 145 146 147 |
# File 'lib/woods/extractors/shared_utility_methods.rb', line 139 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.
193 194 195 196 197 198 199 200 |
# File 'lib/woods/extractors/shared_utility_methods.rb', line 193 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.
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 |
# File 'lib/woods/extractors/shared_utility_methods.rb', line 157 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.
261 262 263 |
# File 'lib/woods/extractors/shared_utility_methods.rb', line 261 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.
96 97 98 99 100 |
# File 'lib/woods/extractors/shared_utility_methods.rb', line 96 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.
131 132 133 |
# File 'lib/woods/extractors/shared_utility_methods.rb', line 131 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.).
276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 |
# File 'lib/woods/extractors/shared_utility_methods.rb', line 276 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).
222 223 224 225 226 |
# File 'lib/woods/extractors/shared_utility_methods.rb', line 222 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.
106 107 108 109 |
# File 'lib/woods/extractors/shared_utility_methods.rb', line 106 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.
235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 |
# File 'lib/woods/extractors/shared_utility_methods.rb', line 235 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 |
#find_files_in_directories(directories, pattern = '**/*.rb') ⇒ Array<String>
Glob files matching a pattern across a list of directories.
Centralizes the Dir[dir.join('**/*.rb')] loop duplicated across
the directory-scanning extractors. Directories are globbed in order
and the per-directory results concatenated, matching the original
per-directory iteration semantics.
33 34 35 |
# File 'lib/woods/extractors/shared_utility_methods.rb', line 33 def find_files_in_directories(directories, pattern = '**/*.rb') directories.flat_map { |dir| Dir[dir.join(pattern).to_s] } 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)
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
# File 'lib/woods/extractors/shared_utility_methods.rb', line 65 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).
123 124 125 |
# File 'lib/woods/extractors/shared_utility_methods.rb', line 123 def skip_file?(source) source.match?(/^\s*module\s+[\w:]+\s*$/) && !source.match?(/^\s*class\s+/) end |