Module: Woods::Extractors::SharedDependencyScanner
- Included in:
- ActionCableExtractor, CachingExtractor, ConcernExtractor, ConfigurationExtractor, ControllerExtractor, DatabaseViewExtractor, DecoratorExtractor, EventExtractor, FactoryExtractor, GraphQLExtractor, JobExtractor, LibExtractor, MailerExtractor, ManagerExtractor, MigrationExtractor, ModelExtractor, PhlexExtractor, PolicyExtractor, PoroExtractor, PunditExtractor, RakeTaskExtractor, SerializerExtractor, ServiceExtractor, StateMachineExtractor, TestMappingExtractor, ValidatorExtractor, ViewComponentExtractor
- Defined in:
- lib/woods/extractors/shared_dependency_scanner.rb
Overview
Common dependency scanning patterns shared across extractors.
Most extractors scan source code for the same four dependency types: model references (via ModelNameCache), service objects, background jobs, and mailers. This module centralizes those scanning patterns.
Individual scan methods accept an optional :via parameter so extractors can customize the relationship label (e.g., :serialization instead of the default :code_reference).
Constant Summary collapse
- ROUTE_HELPER_PATTERN =
Match _path/_url route helpers anywhere in source. This intentionally matches all usages (assignments, string interpolation, etc.) not just link_to/redirect_to calls — any reference to a route helper indicates a dependency on that controller. False positives from non-route _path/_url suffixes (file_path, base_url, etc.) are filtered by RouteHelperResolver::IGNORED_HELPER_PREFIXES. Requires the including class to also include RouteHelperResolver and call build_route_helper_map in its initializer.
/\b(\w+)_(path|url)\b/- FORM_ACTION_HELPER =
Match form_with/form_for with a named route helper as the action/url. Scans only within the form opening tag (up to the first ‘do`, `%>`, or `end`) to avoid matching unrelated _path/_url helpers that appear after the form.
/form_(with|for)\b[^%]*?(\w+)_(path|url)/
Instance Method Summary collapse
-
#extract_constantize_targets(source) ⇒ Array<String>
Extract string-literal arguments passed to ‘.constantize` or `const_get(…)`.
-
#scan_common_dependencies(source) ⇒ Array<Hash>
Scan for all common dependency types and return a deduplicated array.
-
#scan_form_dependencies(source) ⇒ Array<Hash>
Scan source for form_with/form_for calls targeting named route helpers.
-
#scan_job_dependencies(source, via: :code_reference) ⇒ Array<Hash>
Scan for background job references (e.g., FooJob.perform_later).
-
#scan_mailer_dependencies(source, via: :code_reference) ⇒ Array<Hash>
Scan for mailer references (e.g., UserMailer.welcome_email).
-
#scan_model_dependencies(source, via: :code_reference) ⇒ Array<Hash>
Scan for ActiveRecord model references using the precomputed regex.
-
#scan_navigation_dependencies(source, via_type: :link_to) ⇒ Array<Hash>
Scan source for named route helpers and resolve them to controller targets.
-
#scan_service_dependencies(source, via: :code_reference) ⇒ Array<Hash>
Scan for service object references (e.g., FooService.call, FooService::new).
Instance Method Details
#extract_constantize_targets(source) ⇒ Array<String>
Extract string-literal arguments passed to ‘.constantize` or `const_get(…)`. Matches both `“Library::Book”.constantize` and `Object.const_get(“Library::Book”)` / `const_get(“…”)`. Only returns names actually present in ModelNameCache.model_names so non-model uses (e.g. `“String”.constantize` in infra code) do not produce ghost edges.
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
# File 'lib/woods/extractors/shared_dependency_scanner.rb', line 82 def extract_constantize_targets(source) return [] unless ModelNameCache.respond_to?(:model_names) known = ModelNameCache.model_names.to_set return [] if known.empty? targets = [] source.scan(/(["'])([A-Z][\w:]*)\1\s*\.\s*constantize\b/) do |_quote, name| targets << name if known.include?(name) end source.scan(/const_get\s*\(\s*(["'])([A-Z][\w:]*)\1/) do |_quote, name| targets << name if known.include?(name) end targets end |
#scan_common_dependencies(source) ⇒ Array<Hash>
Scan for all common dependency types and return a deduplicated array.
Combines model, service, job, and mailer scans. Use this when an extractor needs all four standard dependency types with the default :code_reference via label.
139 140 141 142 143 144 145 146 |
# File 'lib/woods/extractors/shared_dependency_scanner.rb', line 139 def scan_common_dependencies(source) deps = [] deps.concat(scan_model_dependencies(source)) deps.concat(scan_service_dependencies(source)) deps.concat(scan_job_dependencies(source)) deps.concat(scan_mailer_dependencies(source)) deps.uniq { |d| [d[:type], d[:target]] } end |
#scan_form_dependencies(source) ⇒ Array<Hash>
Scan source for form_with/form_for calls targeting named route helpers.
Gated by Woods.configuration.extract_navigation_edges. Requires RouteHelperResolver to be included and initialized.
200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 |
# File 'lib/woods/extractors/shared_dependency_scanner.rb', line 200 def scan_form_dependencies(source) return [] unless Woods.configuration&. seen = Set.new deps = [] source.scan(FORM_ACTION_HELPER).each do |_, route_name, suffix| resolved = resolve_route_helper("#{route_name}_#{suffix}") next unless resolved target = resolved[:controller] next if seen.include?(target) seen.add(target) deps << { type: :controller, target: target, via: :form_action } end deps end |
#scan_job_dependencies(source, via: :code_reference) ⇒ Array<Hash>
Scan for background job references (e.g., FooJob.perform_later).
114 115 116 117 118 |
# File 'lib/woods/extractors/shared_dependency_scanner.rb', line 114 def scan_job_dependencies(source, via: :code_reference) source.scan(/(\w+Job)\.perform/).flatten.uniq.map do |job| { type: :job, target: job, via: via } end end |
#scan_mailer_dependencies(source, via: :code_reference) ⇒ Array<Hash>
Scan for mailer references (e.g., UserMailer.welcome_email).
125 126 127 128 129 |
# File 'lib/woods/extractors/shared_dependency_scanner.rb', line 125 def scan_mailer_dependencies(source, via: :code_reference) source.scan(/(\w+Mailer)\./).flatten.uniq.map do |mailer| { type: :mailer, target: mailer, via: via } end end |
#scan_model_dependencies(source, via: :code_reference) ⇒ Array<Hash>
Scan for ActiveRecord model references using the precomputed regex.
Three passes:
-
Fully-qualified names via the main ‘b(?:Foo|Bar::Baz)b` regex.
-
‘.constantize` / `const_get(…)` string-literal arguments —a `“Library::Book”.constantize` used to return zero edges because the scan ran over raw source and the regex didn’t pick up the quoted constant. Now we extract the string argument and resolve it.
-
Bare short names (e.g. ‘Book` inside `module Library`) resolved through ModelNameCache.resolve_short_name when unambiguous.
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
# File 'lib/woods/extractors/shared_dependency_scanner.rb', line 47 def scan_model_dependencies(source, via: :code_reference) targets = Set.new source.scan(ModelNameCache.model_names_regex).each { |m| targets << m } extract_constantize_targets(source).each { |t| targets << t } # Short-name + constantize resolution are additive passes guarded # by `respond_to?` so partial test doubles that only stub # `model_names_regex` still work. Real extraction runs always # have the full API. if ModelNameCache.respond_to?(:short_names_regex) && ModelNameCache.respond_to?(:resolve_short_name) # Strip `#` line comments before scanning so references inside # YARD docstrings / TODO comments don't generate ghost edges. # The negative lookahead `(?!\{)` keeps Ruby's `#{...}` string # interpolation intact — stripping blindly would eat every model # reference inside `"Book: #{Library::Book.new}"` etc., which # is a common ERB/Phlex/string pattern. scannable = source.gsub(/#(?!\{)[^\n]*/, '') scannable.scan(ModelNameCache.short_names_regex).each do |short| resolved = ModelNameCache.resolve_short_name(short) targets << resolved if resolved end end targets.map { |model_name| { type: :model, target: model_name, via: via } } end |
#scan_navigation_dependencies(source, via_type: :link_to) ⇒ Array<Hash>
Scan source for named route helpers and resolve them to controller targets.
Gated by Woods.configuration.extract_navigation_edges. Requires RouteHelperResolver to be included and initialized.
170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 |
# File 'lib/woods/extractors/shared_dependency_scanner.rb', line 170 def (source, via_type: :link_to) return [] unless Woods.configuration&. seen_helpers = Set.new seen_targets = Set.new deps = [] source.scan(ROUTE_HELPER_PATTERN).each do |route_name, suffix| helper = "#{route_name}_#{suffix}" next if seen_helpers.include?(helper) seen_helpers.add(helper) resolved = resolve_route_helper(helper) next unless resolved target = resolved[:controller] next if seen_targets.include?(target) seen_targets.add(target) deps << { type: :controller, target: target, via: via_type } end deps end |
#scan_service_dependencies(source, via: :code_reference) ⇒ Array<Hash>
Scan for service object references (e.g., FooService.call, FooService::new).
103 104 105 106 107 |
# File 'lib/woods/extractors/shared_dependency_scanner.rb', line 103 def scan_service_dependencies(source, via: :code_reference) source.scan(/(\w+Service)(?:\.|::)/).flatten.uniq.map do |service| { type: :service, target: service, via: via } end end |