woods 1.2.0 → 1.3.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 (107) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +169 -0
  3. data/README.md +20 -8
  4. data/exe/woods-console +51 -6
  5. data/exe/woods-console-mcp +24 -4
  6. data/exe/woods-mcp +30 -7
  7. data/exe/woods-mcp-http +47 -6
  8. data/lib/generators/woods/install_generator.rb +13 -4
  9. data/lib/generators/woods/templates/woods.rb.tt +155 -0
  10. data/lib/tasks/woods.rake +15 -50
  11. data/lib/woods/builder.rb +174 -9
  12. data/lib/woods/cache/cache_middleware.rb +360 -31
  13. data/lib/woods/chunking/semantic_chunker.rb +334 -7
  14. data/lib/woods/console/adapters/job_adapter.rb +10 -4
  15. data/lib/woods/console/audit_logger.rb +76 -4
  16. data/lib/woods/console/bridge.rb +48 -15
  17. data/lib/woods/console/bridge_protocol.rb +44 -0
  18. data/lib/woods/console/confirmation.rb +3 -4
  19. data/lib/woods/console/console_response_renderer.rb +56 -18
  20. data/lib/woods/console/credential_index.rb +201 -0
  21. data/lib/woods/console/credential_scanner.rb +302 -0
  22. data/lib/woods/console/dispatch_pipeline.rb +138 -0
  23. data/lib/woods/console/embedded_executor.rb +682 -35
  24. data/lib/woods/console/eval_guard.rb +319 -0
  25. data/lib/woods/console/model_validator.rb +1 -3
  26. data/lib/woods/console/rack_middleware.rb +185 -29
  27. data/lib/woods/console/redactor.rb +161 -0
  28. data/lib/woods/console/response_context.rb +127 -0
  29. data/lib/woods/console/safe_context.rb +220 -23
  30. data/lib/woods/console/scope_predicate_parser.rb +131 -0
  31. data/lib/woods/console/server.rb +417 -486
  32. data/lib/woods/console/sql_noise_stripper.rb +87 -0
  33. data/lib/woods/console/sql_table_scanner.rb +213 -0
  34. data/lib/woods/console/sql_validator.rb +81 -31
  35. data/lib/woods/console/table_gate.rb +93 -0
  36. data/lib/woods/console/tool_specs.rb +552 -0
  37. data/lib/woods/console/tools/tier1.rb +3 -3
  38. data/lib/woods/console/tools/tier4.rb +7 -1
  39. data/lib/woods/dependency_graph.rb +66 -7
  40. data/lib/woods/embedding/indexer.rb +190 -6
  41. data/lib/woods/embedding/openai.rb +40 -4
  42. data/lib/woods/embedding/provider.rb +104 -8
  43. data/lib/woods/embedding/text_preparer.rb +23 -3
  44. data/lib/woods/embedding/token_counter.rb +133 -0
  45. data/lib/woods/evaluation/baseline_runner.rb +20 -2
  46. data/lib/woods/evaluation/metrics.rb +4 -1
  47. data/lib/woods/extracted_unit.rb +1 -0
  48. data/lib/woods/extractor.rb +7 -1
  49. data/lib/woods/extractors/controller_extractor.rb +6 -0
  50. data/lib/woods/extractors/mailer_extractor.rb +16 -2
  51. data/lib/woods/extractors/model_extractor.rb +6 -1
  52. data/lib/woods/extractors/phlex_extractor.rb +13 -4
  53. data/lib/woods/extractors/rails_source_extractor.rb +2 -0
  54. data/lib/woods/extractors/route_helper_resolver.rb +130 -0
  55. data/lib/woods/extractors/shared_dependency_scanner.rb +130 -2
  56. data/lib/woods/extractors/view_component_extractor.rb +12 -1
  57. data/lib/woods/extractors/view_engines/base.rb +141 -0
  58. data/lib/woods/extractors/view_engines/erb.rb +145 -0
  59. data/lib/woods/extractors/view_template_extractor.rb +92 -133
  60. data/lib/woods/flow_assembler.rb +23 -15
  61. data/lib/woods/flow_precomputer.rb +21 -2
  62. data/lib/woods/graph_analyzer.rb +3 -4
  63. data/lib/woods/index_artifact.rb +173 -0
  64. data/lib/woods/mcp/bearer_auth.rb +45 -0
  65. data/lib/woods/mcp/bootstrap_state.rb +94 -0
  66. data/lib/woods/mcp/bootstrapper.rb +337 -16
  67. data/lib/woods/mcp/config_resolver.rb +288 -0
  68. data/lib/woods/mcp/errors.rb +134 -0
  69. data/lib/woods/mcp/index_reader.rb +265 -30
  70. data/lib/woods/mcp/origin_guard.rb +132 -0
  71. data/lib/woods/mcp/provider_probe.rb +166 -0
  72. data/lib/woods/mcp/renderers/claude_renderer.rb +6 -0
  73. data/lib/woods/mcp/renderers/markdown_renderer.rb +39 -3
  74. data/lib/woods/mcp/renderers/plain_renderer.rb +16 -2
  75. data/lib/woods/mcp/server.rb +737 -137
  76. data/lib/woods/model_name_cache.rb +78 -2
  77. data/lib/woods/notion/client.rb +25 -2
  78. data/lib/woods/notion/mappers/model_mapper.rb +36 -2
  79. data/lib/woods/railtie.rb +55 -15
  80. data/lib/woods/resilience/circuit_breaker.rb +9 -2
  81. data/lib/woods/resilience/retryable_provider.rb +40 -3
  82. data/lib/woods/resolved_config.rb +299 -0
  83. data/lib/woods/retrieval/context_assembler.rb +112 -5
  84. data/lib/woods/retrieval/query_classifier.rb +1 -1
  85. data/lib/woods/retrieval/ranker.rb +55 -6
  86. data/lib/woods/retrieval/search_executor.rb +42 -13
  87. data/lib/woods/retriever.rb +330 -24
  88. data/lib/woods/session_tracer/middleware.rb +35 -1
  89. data/lib/woods/storage/graph_store.rb +39 -0
  90. data/lib/woods/storage/inapplicable_backend.rb +14 -0
  91. data/lib/woods/storage/metadata_store.rb +129 -1
  92. data/lib/woods/storage/pgvector.rb +70 -8
  93. data/lib/woods/storage/qdrant.rb +196 -5
  94. data/lib/woods/storage/snapshotter/metadata.rb +172 -0
  95. data/lib/woods/storage/snapshotter/vector.rb +238 -0
  96. data/lib/woods/storage/snapshotter.rb +24 -0
  97. data/lib/woods/storage/vector_store.rb +184 -35
  98. data/lib/woods/tasks.rb +85 -0
  99. data/lib/woods/temporal/snapshot_store.rb +49 -1
  100. data/lib/woods/token_utils.rb +44 -5
  101. data/lib/woods/unblocked/client.rb +1 -1
  102. data/lib/woods/unblocked/document_builder.rb +35 -10
  103. data/lib/woods/unblocked/exporter.rb +1 -1
  104. data/lib/woods/util/host_guard.rb +61 -0
  105. data/lib/woods/version.rb +1 -1
  106. data/lib/woods.rb +126 -6
  107. metadata +69 -4
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'set'
3
4
  require_relative '../model_name_cache'
5
+ require_relative 'route_helper_resolver'
4
6
 
5
7
  module Woods
6
8
  module Extractors
@@ -28,13 +30,69 @@ module Woods
28
30
  module SharedDependencyScanner
29
31
  # Scan for ActiveRecord model references using the precomputed regex.
30
32
  #
33
+ # Three passes:
34
+ # 1. Fully-qualified names via the main `\b(?:Foo|Bar::Baz)\b` regex.
35
+ # 2. `.constantize` / `const_get(...)` string-literal arguments —
36
+ # a `"Library::Book".constantize` used to return zero edges
37
+ # because the scan ran over raw source and the regex didn't pick
38
+ # up the quoted constant. Now we extract the string argument and
39
+ # resolve it.
40
+ # 3. Bare short names (e.g. `Book` inside `module Library`)
41
+ # resolved through {ModelNameCache.resolve_short_name} when
42
+ # unambiguous.
43
+ #
31
44
  # @param source [String] Ruby source code to scan
32
45
  # @param via [Symbol] Relationship label (default: :code_reference)
33
46
  # @return [Array<Hash>] Dependency hashes with :type, :target, :via
34
47
  def scan_model_dependencies(source, via: :code_reference)
35
- source.scan(ModelNameCache.model_names_regex).uniq.map do |model_name|
36
- { type: :model, target: model_name, via: via }
48
+ targets = Set.new
49
+ source.scan(ModelNameCache.model_names_regex).each { |m| targets << m }
50
+ extract_constantize_targets(source).each { |t| targets << t }
51
+
52
+ # Short-name + constantize resolution are additive passes guarded
53
+ # by `respond_to?` so partial test doubles that only stub
54
+ # `model_names_regex` still work. Real extraction runs always
55
+ # have the full API.
56
+ if ModelNameCache.respond_to?(:short_names_regex) && ModelNameCache.respond_to?(:resolve_short_name)
57
+ # Strip `#` line comments before scanning so references inside
58
+ # YARD docstrings / TODO comments don't generate ghost edges.
59
+ # The negative lookahead `(?!\{)` keeps Ruby's `#{...}` string
60
+ # interpolation intact — stripping blindly would eat every model
61
+ # reference inside `"Book: #{Library::Book.new}"` etc., which
62
+ # is a common ERB/Phlex/string pattern.
63
+ scannable = source.gsub(/#(?!\{)[^\n]*/, '')
64
+ scannable.scan(ModelNameCache.short_names_regex).each do |short|
65
+ resolved = ModelNameCache.resolve_short_name(short)
66
+ targets << resolved if resolved
67
+ end
68
+ end
69
+
70
+ targets.map { |model_name| { type: :model, target: model_name, via: via } }
71
+ end
72
+
73
+ # Extract string-literal arguments passed to `.constantize` or
74
+ # `const_get(...)`. Matches both `"Library::Book".constantize`
75
+ # and `Object.const_get("Library::Book")` / `const_get("...")`.
76
+ # Only returns names actually present in {ModelNameCache.model_names}
77
+ # so non-model uses (e.g. `"String".constantize` in infra code) do
78
+ # not produce ghost edges.
79
+ #
80
+ # @param source [String]
81
+ # @return [Array<String>]
82
+ def extract_constantize_targets(source)
83
+ return [] unless ModelNameCache.respond_to?(:model_names)
84
+
85
+ known = ModelNameCache.model_names.to_set
86
+ return [] if known.empty?
87
+
88
+ targets = []
89
+ source.scan(/(["'])([A-Z][\w:]*)\1\s*\.\s*constantize\b/) do |_quote, name|
90
+ targets << name if known.include?(name)
91
+ end
92
+ source.scan(/const_get\s*\(\s*(["'])([A-Z][\w:]*)\1/) do |_quote, name|
93
+ targets << name if known.include?(name)
37
94
  end
95
+ targets
38
96
  end
39
97
 
40
98
  # Scan for service object references (e.g., FooService.call, FooService::new).
@@ -86,6 +144,76 @@ module Woods
86
144
  deps.concat(scan_mailer_dependencies(source))
87
145
  deps.uniq { |d| [d[:type], d[:target]] }
88
146
  end
147
+
148
+ # Match _path/_url route helpers anywhere in source.
149
+ # This intentionally matches all usages (assignments, string interpolation, etc.)
150
+ # not just link_to/redirect_to calls — any reference to a route helper indicates
151
+ # a dependency on that controller. False positives from non-route _path/_url
152
+ # suffixes (file_path, base_url, etc.) are filtered by RouteHelperResolver::IGNORED_HELPER_PREFIXES.
153
+ # Requires the including class to also include RouteHelperResolver
154
+ # and call build_route_helper_map in its initializer.
155
+ ROUTE_HELPER_PATTERN = /\b(\w+)_(path|url)\b/
156
+
157
+ # Match form_with/form_for with a named route helper as the action/url.
158
+ # Scans only within the form opening tag (up to the first `do`, `%>`, or `end`)
159
+ # to avoid matching unrelated _path/_url helpers that appear after the form.
160
+ FORM_ACTION_HELPER = /form_(with|for)\b[^%]*?(\w+)_(path|url)/
161
+
162
+ # Scan source for named route helpers and resolve them to controller targets.
163
+ #
164
+ # Gated by +Woods.configuration.extract_navigation_edges+.
165
+ # Requires {RouteHelperResolver} to be included and initialized.
166
+ #
167
+ # @param source [String] Ruby/ERB/HAML source code to scan
168
+ # @param via_type [Symbol] Relationship label (default: :link_to)
169
+ # @return [Array<Hash>] Dependency hashes with :type, :target, :via
170
+ def scan_navigation_dependencies(source, via_type: :link_to)
171
+ return [] unless Woods.configuration&.extract_navigation_edges
172
+
173
+ seen_helpers = Set.new
174
+ seen_targets = Set.new
175
+ deps = []
176
+ source.scan(ROUTE_HELPER_PATTERN).each do |route_name, suffix|
177
+ helper = "#{route_name}_#{suffix}"
178
+ next if seen_helpers.include?(helper)
179
+
180
+ seen_helpers.add(helper)
181
+ resolved = resolve_route_helper(helper)
182
+ next unless resolved
183
+
184
+ target = resolved[:controller]
185
+ next if seen_targets.include?(target)
186
+
187
+ seen_targets.add(target)
188
+ deps << { type: :controller, target: target, via: via_type }
189
+ end
190
+ deps
191
+ end
192
+
193
+ # Scan source for form_with/form_for calls targeting named route helpers.
194
+ #
195
+ # Gated by +Woods.configuration.extract_navigation_edges+.
196
+ # Requires {RouteHelperResolver} to be included and initialized.
197
+ #
198
+ # @param source [String] Template/Ruby source code
199
+ # @return [Array<Hash>] Dependency hashes with via: :form_action
200
+ def scan_form_dependencies(source)
201
+ return [] unless Woods.configuration&.extract_navigation_edges
202
+
203
+ seen = Set.new
204
+ deps = []
205
+ source.scan(FORM_ACTION_HELPER).each do |_, route_name, suffix|
206
+ resolved = resolve_route_helper("#{route_name}_#{suffix}")
207
+ next unless resolved
208
+
209
+ target = resolved[:controller]
210
+ next if seen.include?(target)
211
+
212
+ seen.add(target)
213
+ deps << { type: :controller, target: target, via: :form_action }
214
+ end
215
+ deps
216
+ end
89
217
  end
90
218
  end
91
219
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative 'shared_utility_methods'
4
4
  require_relative 'shared_dependency_scanner'
5
+ require_relative 'route_helper_resolver'
5
6
 
6
7
  module Woods
7
8
  module Extractors
@@ -26,9 +27,11 @@ module Woods
26
27
  class ViewComponentExtractor
27
28
  include SharedUtilityMethods
28
29
  include SharedDependencyScanner
30
+ include RouteHelperResolver
29
31
 
30
32
  def initialize
31
33
  @component_base = find_component_base
34
+ build_route_helper_map
32
35
  end
33
36
 
34
37
  # Extract all ViewComponent components
@@ -299,8 +302,16 @@ module Woods
299
302
  deps << { type: :stimulus_controller, target: controller, via: :html_attribute }
300
303
  end
301
304
 
302
- # URL helpers
305
+ # Navigation edges — resolve _path / _url helpers to real controllers
306
+ # via RouteHelperResolver (wired through the include + build_route_helper_map
307
+ # call in #initialize). Replaces the older unresolved-route emission.
308
+ deps.concat(scan_navigation_dependencies(source))
309
+ deps.concat(scan_form_dependencies(source))
310
+ # Keep the raw-helper fallback for helpers that don't resolve (e.g.,
311
+ # engine-mounted routes outside the main routes table).
303
312
  source.scan(/(\w+)_(?:path|url)/).flatten.uniq.each do |route|
313
+ next if route.match?(/\A(file|base|asset|image|javascript|stylesheet|font|video|audio)\z/)
314
+
304
315
  deps << { type: :route, target: route, via: :url_helper }
305
316
  end
306
317
 
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Woods
4
+ module Extractors
5
+ module ViewEngines
6
+ # Abstract base class documenting the TemplateEngine contract that
7
+ # {ViewTemplateExtractor} dispatches to.
8
+ #
9
+ # Concrete engines (e.g. {Erb}, and future HAML / Slim / Turbo
10
+ # implementations) subclass Base and override the abstract methods.
11
+ # The orchestrator consults the engine collection via
12
+ # {ViewTemplateExtractor::ENGINES}, finds the first engine whose
13
+ # {#handles?} returns true for a file, and delegates scanning and
14
+ # partial-identifier resolution to that engine.
15
+ #
16
+ # {#parse} is an optional per-engine memoization seam: engines whose
17
+ # parse step is expensive (e.g. HAML / Slim compiling through
18
+ # Temple) can override it to return an internal IR and call it once
19
+ # before the three scan operations. ERB and Turbo Streams, whose
20
+ # scanners regex raw source, leave the identity default in place.
21
+ # The orchestrator does not invoke {#parse} — engines opt in
22
+ # internally when it pays off.
23
+ #
24
+ # @abstract Subclass and override the abstract methods ({#name},
25
+ # {#extensions}, {#scan_partials}, {#scan_instance_variables},
26
+ # {#scan_helpers}, {#resolve_partial_identifier}).
27
+ class Base
28
+ # Stable engine identifier surfaced through
29
+ # {ViewTemplateExtractor.supported_template_engines} and the MCP
30
+ # `structure` tool.
31
+ #
32
+ # @return [Symbol]
33
+ def name
34
+ raise NotImplementedError, "#{self.class.name} must implement #name"
35
+ end
36
+
37
+ # File extensions this engine handles, each including the leading
38
+ # dot (e.g. `.html.erb`).
39
+ #
40
+ # @return [Array<String>]
41
+ def extensions
42
+ raise NotImplementedError, "#{self.class.name} must implement #extensions"
43
+ end
44
+
45
+ # Whether this engine handles the given file path. The default
46
+ # implementation suffix-matches against {#extensions}; engines
47
+ # with more elaborate discrimination (e.g. looking at file
48
+ # content) can override.
49
+ #
50
+ # @param file_path [String]
51
+ # @return [Boolean]
52
+ def handles?(file_path)
53
+ extensions.any? { |ext| file_path.end_with?(ext) }
54
+ end
55
+
56
+ # Optional memoization hook for engines whose parse step is
57
+ # expensive. Default is identity — returns the source string
58
+ # unchanged — so engines that scan raw source (ERB, Turbo
59
+ # Streams) pay no overhead. HAML/Slim implementations can
60
+ # override to compile once and cache an internal IR, then call
61
+ # {#parse} inside their own {#scan_partials} /
62
+ # {#scan_instance_variables} / {#scan_helpers}.
63
+ #
64
+ # The orchestrator does not invoke this method; it is a
65
+ # convention for engine-internal reuse.
66
+ #
67
+ # @param source [String] Template source code
68
+ # @return [Object] An engine-private IR, or the source itself
69
+ def parse(source)
70
+ source
71
+ end
72
+
73
+ # Partial names referenced by render calls in the given source.
74
+ # Returns the raw partial names as they appear in render calls
75
+ # (e.g. `'comments/comment'`, `'sidebar'`, `'header'`); the
76
+ # orchestrator calls {#resolve_partial_identifier} per entry to
77
+ # translate each into a canonical file identifier.
78
+ #
79
+ # @param source [String] Template source code
80
+ # @return [Array<String>]
81
+ def scan_partials(_source)
82
+ raise NotImplementedError, "#{self.class.name} must implement #scan_partials"
83
+ end
84
+
85
+ # Instance-variable names referenced in the template source,
86
+ # returned sorted and deduplicated (including the leading `@`).
87
+ #
88
+ # @param source [String] Template source code
89
+ # @return [Array<String>]
90
+ def scan_instance_variables(_source)
91
+ raise NotImplementedError, "#{self.class.name} must implement #scan_instance_variables"
92
+ end
93
+
94
+ # Helper method names detected in the template source, returned
95
+ # sorted. The set of "helpers" is engine-defined — ERB scans for
96
+ # the common Rails view-helper vocabulary, HAML/Slim would scan
97
+ # the same vocabulary in their own syntaxes.
98
+ #
99
+ # @param source [String] Template source code
100
+ # @return [Array<String>]
101
+ def scan_helpers(_source)
102
+ raise NotImplementedError, "#{self.class.name} must implement #scan_helpers"
103
+ end
104
+
105
+ # Resolve a partial name (as it appeared in a render call) to the
106
+ # file identifier of the partial template. Engines own this
107
+ # because the filename convention is engine-specific: ERB looks
108
+ # for `_foo.html.erb`, HAML for `_foo.html.haml`, etc.
109
+ #
110
+ # @param partial_name [String] The partial name from the render call
111
+ # @param current_identifier [String] Identifier of the template
112
+ # issuing the render, used to anchor relative partial names
113
+ # @return [String]
114
+ def resolve_partial_identifier(_partial_name, _current_identifier)
115
+ raise NotImplementedError,
116
+ "#{self.class.name} must implement #resolve_partial_identifier"
117
+ end
118
+
119
+ # Route-helper references found in the template source. Each
120
+ # candidate is a hash shaped `{ helper: 'posts_path', via: Symbol }`
121
+ # where `via` is typically `:link_to` or `:form_action`. The
122
+ # orchestrator resolves each candidate's `helper` to a controller
123
+ # target via {RouteHelperResolver} — engines do NOT need to know
124
+ # about Rails route state, they only surface raw helper calls
125
+ # from the source in whatever way their syntax requires.
126
+ #
127
+ # Engines matter here because form-call syntax differs across
128
+ # engines (e.g. ERB's `<%= form_with ... %>` vs. HAML's
129
+ # `= form_with ...`); returning candidates rather than resolved
130
+ # edges keeps Rails-coupled logic off the engine.
131
+ #
132
+ # @param source [String] Template source code
133
+ # @return [Array<Hash>]
134
+ def scan_navigation_candidates(_source)
135
+ raise NotImplementedError,
136
+ "#{self.class.name} must implement #scan_navigation_candidates"
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+ require_relative 'base'
5
+
6
+ module Woods
7
+ module Extractors
8
+ module ViewEngines
9
+ # ERB implementation of the {Base} template-engine contract. Owns
10
+ # the ERB-specific parsing surface that {ViewTemplateExtractor}
11
+ # delegates to — extension list, partial filename convention, and
12
+ # the three scan operations (partials, instance variables, helper
13
+ # calls).
14
+ class Erb < Base
15
+ # File extensions this engine handles.
16
+ EXTENSIONS = %w[.html.erb .erb].freeze
17
+
18
+ # Stable engine identifier — see Base#name.
19
+ ENGINE_NAME = :erb
20
+
21
+ # Matches named route helpers (e.g. `posts_path`, `user_url`) in
22
+ # template source. Identical shape across ERB / plain Ruby, so
23
+ # shared with {SharedDependencyScanner}.
24
+ ROUTE_HELPER_PATTERN = /\b(\w+)_(path|url)\b/
25
+
26
+ # Matches form_with / form_for calls whose action is a named
27
+ # route helper. ERB-flavored (`[^%]*?` stops at the next `%`
28
+ # which appears at ERB tag terminators and HAML tag starts) —
29
+ # when HAML/Slim land they will define their own form pattern.
30
+ FORM_ACTION_HELPER = /form_(with|for)\b[^%]*?(\w+)_(path|url)/
31
+
32
+ # Common Rails view helper methods to detect in template source.
33
+ COMMON_HELPERS = %w[
34
+ link_to
35
+ button_to
36
+ form_for
37
+ form_with
38
+ form_tag
39
+ image_tag
40
+ stylesheet_link_tag
41
+ javascript_include_tag
42
+ content_for
43
+ yield
44
+ render
45
+ redirect_to
46
+ truncate
47
+ pluralize
48
+ number_to_currency
49
+ number_to_percentage
50
+ number_with_delimiter
51
+ time_ago_in_words
52
+ distance_of_time_in_words
53
+ simple_format
54
+ sanitize
55
+ raw
56
+ safe_join
57
+ content_tag
58
+ tag
59
+ mail_to
60
+ url_for
61
+ asset_path
62
+ asset_url
63
+ ].freeze
64
+
65
+ # @see Base#name
66
+ def name
67
+ ENGINE_NAME
68
+ end
69
+
70
+ # @see Base#extensions
71
+ def extensions
72
+ EXTENSIONS
73
+ end
74
+
75
+ # Matches:
76
+ # - `render partial: 'foo/bar'`
77
+ # - `render 'foo/bar'`
78
+ # - `render :foo`
79
+ #
80
+ # @see Base#scan_partials
81
+ def scan_partials(source)
82
+ partials = Set.new
83
+
84
+ source.scan(/render\s+partial:\s*['"]([^'"]+)['"]/).each do |match|
85
+ partials << match[0]
86
+ end
87
+
88
+ source.scan(/render\s+['"]([^'"]+)['"]/).each do |match|
89
+ partials << match[0]
90
+ end
91
+
92
+ source.scan(/render\s+:(\w+)/).each do |match|
93
+ partials << match[0]
94
+ end
95
+
96
+ partials.to_a
97
+ end
98
+
99
+ # @see Base#scan_instance_variables
100
+ def scan_instance_variables(source)
101
+ source.scan(/@[a-zA-Z_]\w*/).uniq.sort
102
+ end
103
+
104
+ # @see Base#scan_helpers
105
+ def scan_helpers(source)
106
+ found = Set.new
107
+ COMMON_HELPERS.each do |helper|
108
+ found << helper if source.match?(/\b#{Regexp.escape(helper)}\b/)
109
+ end
110
+ found.to_a.sort
111
+ end
112
+
113
+ # Given `render 'comments/comment'` from a template at
114
+ # `posts/show.html.erb`, resolves to `comments/_comment.html.erb`.
115
+ #
116
+ # @see Base#resolve_partial_identifier
117
+ def resolve_partial_identifier(partial_name, current_identifier)
118
+ if partial_name.include?('/')
119
+ dir = File.dirname(partial_name)
120
+ base = File.basename(partial_name)
121
+ "#{dir}/_#{base}.html.erb"
122
+ else
123
+ dir = File.dirname(current_identifier)
124
+ if dir == '.'
125
+ "_#{partial_name}.html.erb"
126
+ else
127
+ "#{dir}/_#{partial_name}.html.erb"
128
+ end
129
+ end
130
+ end
131
+
132
+ # @see Base#scan_navigation_candidates
133
+ def scan_navigation_candidates(source)
134
+ link_to_candidates = source.scan(ROUTE_HELPER_PATTERN).map do |route_name, suffix|
135
+ { helper: "#{route_name}_#{suffix}", via: :link_to }
136
+ end
137
+ form_candidates = source.scan(FORM_ACTION_HELPER).map do |_form_kind, route_name, suffix|
138
+ { helper: "#{route_name}_#{suffix}", via: :form_action }
139
+ end
140
+ link_to_candidates + form_candidates
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end