rails-ai-context 5.32.2 → 5.34.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +1491 -0
- data/CONTEXT.md +27 -3
- data/README.md +16 -12
- data/Rakefile +6 -0
- data/app/controllers/rails_ai_context/mcp_controller.rb +112 -84
- data/config/routes.rb +2 -3
- data/docs/ARCHITECTURE.md +7 -5
- data/docs/CLI.md +79 -6
- data/docs/COMPATIBILITY.md +75 -1
- data/docs/CONFIGURATION.md +30 -11
- data/docs/CUSTOM_TOOLS.md +2 -1
- data/docs/FAQ.md +16 -6
- data/docs/GUIDE.md +117 -71
- data/docs/INTROSPECTORS.md +10 -8
- data/docs/QUICKSTART.md +3 -3
- data/docs/RECIPES.md +1 -1
- data/docs/SECURITY.md +109 -25
- data/docs/SETUP.md +30 -6
- data/docs/STANDALONE.md +10 -3
- data/docs/TOOLS.md +152 -34
- data/docs/TROUBLESHOOTING.md +90 -12
- data/docs/adr/0003-shared-tool-cache-semantics.md +8 -0
- data/docs/adr/0005-workspace-mcp-entries.md +39 -0
- data/exe/rails-ai-context +733 -104
- data/lib/generators/rails_ai_context/install/install_generator.rb +341 -125
- data/lib/rails_ai_context/app_kind.rb +18 -5
- data/lib/rails_ai_context/ast_cache.rb +7 -0
- data/lib/rails_ai_context/base_mixins.rb +8 -8
- data/lib/rails_ai_context/boot_manager.rb +22 -3
- data/lib/rails_ai_context/change_watch.rb +14 -11
- data/lib/rails_ai_context/cli/app_root.rb +401 -0
- data/lib/rails_ai_context/cli/entry_boot.rb +58 -22
- data/lib/rails_ai_context/cli/tool_runner.rb +47 -21
- data/lib/rails_ai_context/code_reloader.rb +115 -1
- data/lib/rails_ai_context/concern_membership.rb +3 -1
- data/lib/rails_ai_context/concern_paths.rb +8 -3
- data/lib/rails_ai_context/configuration.rb +41 -9
- data/lib/rails_ai_context/context_file_report.rb +19 -2
- data/lib/rails_ai_context/database_file.rb +26 -0
- data/lib/rails_ai_context/doctor.rb +1122 -184
- data/lib/rails_ai_context/engine.rb +18 -1
- data/lib/rails_ai_context/erb_source.rb +19 -0
- data/lib/rails_ai_context/executable.rb +21 -0
- data/lib/rails_ai_context/facts_formatter.rb +5 -2
- data/lib/rails_ai_context/file_walk.rb +61 -10
- data/lib/rails_ai_context/fingerprinter/snapshot.rb +234 -0
- data/lib/rails_ai_context/fingerprinter.rb +40 -12
- data/lib/rails_ai_context/gem_lock.rb +186 -27
- data/lib/rails_ai_context/git_ignore.rb +22 -0
- data/lib/rails_ai_context/install/ai_tool.rb +31 -5
- data/lib/rails_ai_context/install/cleanup.rb +21 -0
- data/lib/rails_ai_context/install/program.rb +209 -35
- data/lib/rails_ai_context/install/selection_record.rb +258 -97
- data/lib/rails_ai_context/install/surface.rb +6 -1
- data/lib/rails_ai_context/install/validation_hook.rb +282 -0
- data/lib/rails_ai_context/install/workspace.rb +166 -0
- data/lib/rails_ai_context/install_mode.rb +71 -13
- data/lib/rails_ai_context/introspectors/action_mailbox_introspector.rb +1 -1
- data/lib/rails_ai_context/introspectors/action_presence.rb +11 -6
- data/lib/rails_ai_context/introspectors/action_source.rb +182 -0
- data/lib/rails_ai_context/introspectors/action_text_introspector.rb +1 -2
- data/lib/rails_ai_context/introspectors/active_record_settings.rb +1 -1
- data/lib/rails_ai_context/introspectors/active_storage_introspector.rb +1 -3
- data/lib/rails_ai_context/introspectors/active_support_introspector.rb +3 -3
- data/lib/rails_ai_context/introspectors/admin_resources.rb +1 -1
- data/lib/rails_ai_context/introspectors/anyway_configs.rb +1 -1
- data/lib/rails_ai_context/introspectors/api_introspector.rb +3 -3
- data/lib/rails_ai_context/introspectors/auth_introspector.rb +15 -10
- data/lib/rails_ai_context/introspectors/call_site_expansion.rb +24 -1
- data/lib/rails_ai_context/introspectors/component_introspector.rb +3 -2
- data/lib/rails_ai_context/introspectors/controller_introspector.rb +387 -60
- data/lib/rails_ai_context/introspectors/convention_introspector.rb +1 -1
- data/lib/rails_ai_context/introspectors/database_stats_introspector.rb +15 -3
- data/lib/rails_ai_context/introspectors/devops_introspector.rb +3 -1
- data/lib/rails_ai_context/introspectors/eager_load.rb +201 -9
- data/lib/rails_ai_context/introspectors/engine_introspector.rb +1 -1
- data/lib/rails_ai_context/introspectors/env_config_introspector.rb +1 -1
- data/lib/rails_ai_context/introspectors/env_references.rb +10 -1
- data/lib/rails_ai_context/introspectors/frontend_framework_introspector.rb +1 -1
- data/lib/rails_ai_context/introspectors/gem_introspector.rb +23 -10
- data/lib/rails_ai_context/introspectors/grape_endpoints.rb +1 -1
- data/lib/rails_ai_context/introspectors/habtm_join_tables.rb +1 -1
- data/lib/rails_ai_context/introspectors/helper_names.rb +1 -1
- data/lib/rails_ai_context/introspectors/i18n_introspector.rb +3 -3
- data/lib/rails_ai_context/introspectors/job_introspector.rb +8 -6
- data/lib/rails_ai_context/introspectors/listeners/base_listener.rb +12 -8
- data/lib/rails_ai_context/introspectors/listeners/callbacks_listener.rb +78 -4
- data/lib/rails_ai_context/introspectors/listeners/config_assignment_listener.rb +1 -0
- data/lib/rails_ai_context/introspectors/listeners/env_access_listener.rb +10 -1
- data/lib/rails_ai_context/introspectors/listeners/foreign_evals_listener.rb +61 -0
- data/lib/rails_ai_context/introspectors/listeners/macros_listener.rb +13 -4
- data/lib/rails_ai_context/introspectors/listeners/migration_dsl_listener.rb +2 -1
- data/lib/rails_ai_context/introspectors/listeners/migration_replay_listener.rb +3 -2
- data/lib/rails_ai_context/introspectors/listeners/routes_dsl_listener.rb +29 -8
- data/lib/rails_ai_context/introspectors/listeners/schema_dsl_listener.rb +2 -2
- data/lib/rails_ai_context/introspectors/listeners/scopes_listener.rb +88 -12
- data/lib/rails_ai_context/introspectors/middleware_introspector.rb +2 -2
- data/lib/rails_ai_context/introspectors/migration_introspector.rb +27 -0
- data/lib/rails_ai_context/introspectors/migration_replay/helpers.rb +1 -1
- data/lib/rails_ai_context/introspectors/migration_replay/statements.rb +83 -27
- data/lib/rails_ai_context/introspectors/migration_replay.rb +15 -14
- data/lib/rails_ai_context/introspectors/model_introspector.rb +231 -23
- data/lib/rails_ai_context/introspectors/n_plus_one_scan.rb +620 -0
- data/lib/rails_ai_context/introspectors/performance_introspector.rb +11 -174
- data/lib/rails_ai_context/introspectors/route_introspector.rb +10 -1
- data/lib/rails_ai_context/introspectors/schema_conventions.rb +23 -3
- data/lib/rails_ai_context/introspectors/schema_introspector.rb +66 -16
- data/lib/rails_ai_context/introspectors/schema_reader.rb +4 -3
- data/lib/rails_ai_context/introspectors/seeds_introspector.rb +1 -1
- data/lib/rails_ai_context/introspectors/source_scan.rb +39 -9
- data/lib/rails_ai_context/introspectors/stimulus_introspector.rb +15 -6
- data/lib/rails_ai_context/introspectors/test_introspector.rb +1 -1
- data/lib/rails_ai_context/introspectors/turbo_introspector.rb +131 -12
- data/lib/rails_ai_context/introspectors/view_introspector.rb +4 -4
- data/lib/rails_ai_context/introspectors/view_template_introspector.rb +15 -1
- data/lib/rails_ai_context/legacy_cleanup.rb +17 -4
- data/lib/rails_ai_context/live_reload.rb +20 -20
- data/lib/rails_ai_context/mcp_config_generator.rb +916 -109
- data/lib/rails_ai_context/mcp_edge.rb +182 -32
- data/lib/rails_ai_context/middleware.rb +5 -1
- data/lib/rails_ai_context/migration_status.rb +88 -25
- data/lib/rails_ai_context/output_guard.rb +48 -2
- data/lib/rails_ai_context/package_json.rb +1 -1
- data/lib/rails_ai_context/path_resolver.rb +46 -13
- data/lib/rails_ai_context/payload.rb +92 -8
- data/lib/rails_ai_context/pending_migrations.rb +5 -1
- data/lib/rails_ai_context/portable_path.rb +3 -2
- data/lib/rails_ai_context/presets.rb +15 -11
- data/lib/rails_ai_context/redaction.rb +262 -51
- data/lib/rails_ai_context/resources.rb +55 -20
- data/lib/rails_ai_context/route_coverage.rb +8 -0
- data/lib/rails_ai_context/safe_file.rb +14 -1
- data/lib/rails_ai_context/safe_path.rb +101 -4
- data/lib/rails_ai_context/schema_adapter.rb +8 -0
- data/lib/rails_ai_context/schema_version.rb +9 -0
- data/lib/rails_ai_context/serializers/claude_rules_serializer.rb +88 -33
- data/lib/rails_ai_context/serializers/compact_serializer_helper.rb +36 -34
- data/lib/rails_ai_context/serializers/context_file_serializer.rb +21 -29
- data/lib/rails_ai_context/serializers/copilot_instructions_serializer.rb +8 -6
- data/lib/rails_ai_context/serializers/copilot_serializer.rb +2 -7
- data/lib/rails_ai_context/serializers/cursor_rules_serializer.rb +19 -16
- data/lib/rails_ai_context/serializers/guide_examples.rb +142 -0
- data/lib/rails_ai_context/serializers/markdown_serializer.rb +146 -30
- data/lib/rails_ai_context/serializers/opencode_rules_serializer.rb +38 -15
- data/lib/rails_ai_context/serializers/section_facts.rb +44 -1
- data/lib/rails_ai_context/serializers/section_marker_writer.rb +94 -12
- data/lib/rails_ai_context/serializers/stack_overview_helper.rb +24 -12
- data/lib/rails_ai_context/serializers/tool_guide_helper.rb +244 -129
- data/lib/rails_ai_context/server.rb +304 -71
- data/lib/rails_ai_context/tasks/rails_ai_context.rake +447 -372
- data/lib/rails_ai_context/tools/analyze_feature.rb +72 -18
- data/lib/rails_ai_context/tools/argument_schema.rb +49 -0
- data/lib/rails_ai_context/tools/base_tool.rb +367 -46
- data/lib/rails_ai_context/tools/brakeman_guard.rb +24 -0
- data/lib/rails_ai_context/tools/dependency_graph.rb +63 -28
- data/lib/rails_ai_context/tools/diagnose.rb +218 -11
- data/lib/rails_ai_context/tools/generate_test.rb +339 -39
- data/lib/rails_ai_context/tools/get_callbacks.rb +24 -13
- data/lib/rails_ai_context/tools/get_component_catalog.rb +1 -1
- data/lib/rails_ai_context/tools/get_concern.rb +91 -27
- data/lib/rails_ai_context/tools/get_config.rb +13 -4
- data/lib/rails_ai_context/tools/get_context.rb +71 -14
- data/lib/rails_ai_context/tools/get_controllers.rb +179 -44
- data/lib/rails_ai_context/tools/get_conventions.rb +28 -6
- data/lib/rails_ai_context/tools/get_edit_context.rb +27 -14
- data/lib/rails_ai_context/tools/get_env.rb +18 -9
- data/lib/rails_ai_context/tools/get_frontend_stack.rb +3 -1
- data/lib/rails_ai_context/tools/get_gems.rb +1 -1
- data/lib/rails_ai_context/tools/get_helper_methods.rb +10 -6
- data/lib/rails_ai_context/tools/get_i18n.rb +1 -1
- data/lib/rails_ai_context/tools/get_job_pattern.rb +3 -2
- data/lib/rails_ai_context/tools/get_mailers.rb +3 -3
- data/lib/rails_ai_context/tools/get_model_details.rb +30 -10
- data/lib/rails_ai_context/tools/get_partial_interface.rb +242 -23
- data/lib/rails_ai_context/tools/get_routes.rb +29 -8
- data/lib/rails_ai_context/tools/get_schema.rb +52 -19
- data/lib/rails_ai_context/tools/get_service_pattern.rb +24 -10
- data/lib/rails_ai_context/tools/get_stimulus.rb +37 -1
- data/lib/rails_ai_context/tools/get_test_info.rb +18 -4
- data/lib/rails_ai_context/tools/get_turbo_map.rb +221 -58
- data/lib/rails_ai_context/tools/get_view.rb +14 -14
- data/lib/rails_ai_context/tools/migration_advisor.rb +355 -48
- data/lib/rails_ai_context/tools/onboard.rb +52 -8
- data/lib/rails_ai_context/tools/performance_check.rb +5 -3
- data/lib/rails_ai_context/tools/query.rb +914 -86
- data/lib/rails_ai_context/tools/read_logs.rb +69 -15
- data/lib/rails_ai_context/tools/review_changes.rb +215 -53
- data/lib/rails_ai_context/tools/runtime_info.rb +51 -1
- data/lib/rails_ai_context/tools/safe_call.rb +35 -14
- data/lib/rails_ai_context/tools/search_code.rb +243 -67
- data/lib/rails_ai_context/tools/search_docs.rb +4 -4
- data/lib/rails_ai_context/tools/security_scan.rb +143 -33
- data/lib/rails_ai_context/tools/session_context.rb +39 -5
- data/lib/rails_ai_context/tools/validate.rb +259 -40
- data/lib/rails_ai_context/tools/validate_semantics.rb +130 -56
- data/lib/rails_ai_context/version.rb +1 -1
- data/lib/rails_ai_context/vfs.rb +67 -40
- data/lib/rails_ai_context/view_file.rb +41 -3
- data/lib/rails_ai_context/watcher.rb +51 -13
- data/lib/rails_ai_context.rb +21 -0
- data/server.json +1 -1
- metadata +14 -1
|
@@ -23,21 +23,53 @@ module RailsAiContext
|
|
|
23
23
|
|
|
24
24
|
INLINE_BLOCK = "[inline_block]"
|
|
25
25
|
|
|
26
|
+
# turbo-rails' broadcast macros are callbacks too: each declares a
|
|
27
|
+
# commit callback per event, a lambda running the method named here
|
|
28
|
+
# (Turbo::Broadcastable::ClassMethods).
|
|
29
|
+
BROADCASTS = {
|
|
30
|
+
broadcasts: { "after_create_commit" => "broadcast_action_later_to", "after_update_commit" => "broadcast_replace_later",
|
|
31
|
+
"after_destroy_commit" => "broadcast_remove" },
|
|
32
|
+
broadcasts_to: { "after_create_commit" => "broadcast_action_later_to", "after_update_commit" => "broadcast_replace_later_to",
|
|
33
|
+
"after_destroy_commit" => "broadcast_remove_to" },
|
|
34
|
+
broadcasts_refreshes: { "after_create_commit" => "broadcast_refresh_later_to", "after_update_commit" => "broadcast_refresh_later",
|
|
35
|
+
"after_destroy_commit" => "broadcast_refresh" },
|
|
36
|
+
broadcasts_refreshes_to: { "after_commit" => "broadcast_refresh_later_to" }
|
|
37
|
+
}.freeze
|
|
38
|
+
|
|
26
39
|
# Macros that call args.extract_options!, so a trailing braced hash is options, not a filter.
|
|
27
40
|
EXTRACTS_OPTIONS = %i[
|
|
28
41
|
before_validation after_validation after_commit after_rollback
|
|
29
42
|
after_create_commit after_update_commit after_destroy_commit after_save_commit
|
|
30
43
|
].to_set.freeze
|
|
31
44
|
|
|
45
|
+
# `before_<event>` on a chain `define_model_callbacks` makes; outside the file that defines it
|
|
46
|
+
# (Paperclip's has_attached_file), only the booted model's chains say it is one.
|
|
47
|
+
MODEL_CALLBACK = /\A(?:before|after|around)_(\w+)\z/
|
|
48
|
+
|
|
49
|
+
# `library_chains: true` also keeps a callback on a chain the file does not define, under
|
|
50
|
+
# `library_chain`, for a caller that checks the model holds that chain.
|
|
51
|
+
def initialize(library_chains: false)
|
|
52
|
+
super()
|
|
53
|
+
@library_chains = library_chains
|
|
54
|
+
end
|
|
55
|
+
|
|
32
56
|
def on_call_node_enter(node)
|
|
33
57
|
return record_skip(node) if node.name == :skip_callback && in_scope?(node)
|
|
34
|
-
return
|
|
58
|
+
return record_broadcasts(node) if BROADCASTS.key?(node.name) && in_scope?(node)
|
|
59
|
+
return record_defined(node) if node.name == :define_model_callbacks && in_scope?(node)
|
|
60
|
+
|
|
61
|
+
standard = CALLBACK_METHODS.include?(node.name)
|
|
62
|
+
chain = MODEL_CALLBACK.match(node.name.to_s)&.[](1) unless standard
|
|
63
|
+
return unless (standard || chain) && in_scope?(node)
|
|
64
|
+
|
|
65
|
+
library_chain = chain unless defined_here.include?(node.name)
|
|
66
|
+
return if library_chain && !@library_chains
|
|
35
67
|
|
|
36
68
|
# Sources, not literals: a lambda condition reads as the line the
|
|
37
69
|
# file holds rather than as a marker.
|
|
38
70
|
hash = options_hash(node)
|
|
39
71
|
options = scope_options(receiver_name(node)).merge(extract_keyword_sources(node)).merge(hash_sources(hash))
|
|
40
|
-
emit(node, resolve_callback_types(node.name, options), callback_targets(node, hash), options)
|
|
72
|
+
emit(node, resolve_callback_types(node.name, options), callback_targets(node, hash), options, library_chain: library_chain)
|
|
41
73
|
end
|
|
42
74
|
|
|
43
75
|
ON_EVENT = "after_commit_on_"
|
|
@@ -60,6 +92,13 @@ module RailsAiContext
|
|
|
60
92
|
type.to_s.start_with?(ON_EVENT)
|
|
61
93
|
end
|
|
62
94
|
|
|
95
|
+
# What a broadcast macro's callbacks are listed as. Not a bare name:
|
|
96
|
+
# the model has no method of it, and Rails keeps each of its lambdas
|
|
97
|
+
# where it merges two callbacks of one method name.
|
|
98
|
+
def self.broadcast_target(macro)
|
|
99
|
+
"#{macro} (turbo-rails)"
|
|
100
|
+
end
|
|
101
|
+
|
|
63
102
|
# The event such a type names, `"create"`, or nil.
|
|
64
103
|
def self.event(type)
|
|
65
104
|
type.to_s.delete_prefix(ON_EVENT) if names_event?(type)
|
|
@@ -117,10 +156,20 @@ module RailsAiContext
|
|
|
117
156
|
end
|
|
118
157
|
end
|
|
119
158
|
|
|
120
|
-
def
|
|
159
|
+
def defined_here
|
|
160
|
+
@defined_here ||= Set.new
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def record_defined(node)
|
|
164
|
+
only = Array(extract_keyword_options(node)[:only]).map(&:to_s)
|
|
165
|
+
kinds = only.empty? ? KINDS : KINDS & only
|
|
166
|
+
extract_symbol_args(node).each { |event| kinds.each { |kind| defined_here << :"#{kind}_#{event}" } }
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def emit(node, callback_types, targets, options, library_chain: nil)
|
|
121
170
|
targets.each do |method_name, confidence|
|
|
122
171
|
callback_types.each do |callback_type|
|
|
123
|
-
|
|
172
|
+
record = {
|
|
124
173
|
# The declared macro, so a renderer can print what the file
|
|
125
174
|
# says instead of the resolved type.
|
|
126
175
|
name: node.name.to_s,
|
|
@@ -131,10 +180,35 @@ module RailsAiContext
|
|
|
131
180
|
location: node.location.start_line,
|
|
132
181
|
confidence: confidence
|
|
133
182
|
}
|
|
183
|
+
# A block has no method whose body could be read; its declaration is its source.
|
|
184
|
+
record.merge!(declaration(node)) if method_name == INLINE_BLOCK
|
|
185
|
+
record[:library_chain] = library_chain if library_chain
|
|
186
|
+
@results << record
|
|
134
187
|
end
|
|
135
188
|
end
|
|
136
189
|
end
|
|
137
190
|
|
|
191
|
+
def record_broadcasts(node)
|
|
192
|
+
BROADCASTS[node.name].each do |type, runs|
|
|
193
|
+
@results << {
|
|
194
|
+
name: node.name.to_s,
|
|
195
|
+
type: type,
|
|
196
|
+
method: self.class.broadcast_target(node.name),
|
|
197
|
+
# Its options are the broadcast's rendering, never a callback condition.
|
|
198
|
+
options: {},
|
|
199
|
+
runs: runs,
|
|
200
|
+
owner: @owner_stack.dup,
|
|
201
|
+
location: node.location.start_line,
|
|
202
|
+
confidence: confidence_for(node)
|
|
203
|
+
}.merge(declaration(node))
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# The call as the file writes it, at the file's indentation.
|
|
208
|
+
def declaration(node)
|
|
209
|
+
{ source: (" " * node.location.start_column) + NodeSource.text(node), end_location: node.location.end_line }
|
|
210
|
+
end
|
|
211
|
+
|
|
138
212
|
# `after_commit on: :create` is the after_commit_on_create type. One
|
|
139
213
|
# declaration for several events stays one callback, its on: kept.
|
|
140
214
|
def resolve_callback_types(name, options)
|
|
@@ -80,11 +80,20 @@ module RailsAiContext
|
|
|
80
80
|
# `ENV.fetch("PORT", defaults[:port])` has a default, but naming
|
|
81
81
|
# it `defaults[:port]` puts a Ruby expression where a reader
|
|
82
82
|
# expects something to copy into a .env file.
|
|
83
|
-
default: literal_value(args[1]),
|
|
83
|
+
default: args.size > 1 ? literal_value(args[1]) : block_value(node.block),
|
|
84
84
|
location: node.location.start_line
|
|
85
85
|
}
|
|
86
86
|
end
|
|
87
87
|
|
|
88
|
+
# `ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" }`: a block whose
|
|
89
|
+
# one statement is a literal is that default, as a second argument is.
|
|
90
|
+
def block_value(block)
|
|
91
|
+
statements = block.body if block.is_a?(Prism::BlockNode)
|
|
92
|
+
return unless statements.is_a?(Prism::StatementsNode) && statements.body.size == 1
|
|
93
|
+
|
|
94
|
+
literal_value(statements.body.first)
|
|
95
|
+
end
|
|
96
|
+
|
|
88
97
|
def literal_value(node)
|
|
89
98
|
case node
|
|
90
99
|
when Prism::StringNode then node.unescaped
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAiContext
|
|
4
|
+
module Introspectors
|
|
5
|
+
module Listeners
|
|
6
|
+
# The blocks a class body evaluates on another receiver (`translation_class.class_eval do`),
|
|
7
|
+
# each with the source of every outermost call, by the line it starts on.
|
|
8
|
+
class ForeignEvalsListener < BaseListener
|
|
9
|
+
def initialize
|
|
10
|
+
super
|
|
11
|
+
@def_depth = 0
|
|
12
|
+
@open = []
|
|
13
|
+
@conditions = []
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# An if or unless condition (`validates :x unless Rails.env.test?`) is not a declaration.
|
|
17
|
+
def on_if_node_enter(node)
|
|
18
|
+
@conditions << node.predicate.location
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def on_unless_node_enter(node)
|
|
22
|
+
@conditions << node.predicate.location
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def on_def_node_enter(_node)
|
|
26
|
+
@def_depth += 1
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def on_def_node_leave(_node)
|
|
30
|
+
@def_depth -= 1
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def on_call_node_enter(node)
|
|
34
|
+
@open.each { |_, block| record(block, node) }
|
|
35
|
+
return unless @def_depth.zero? && CallSiteExpansion.foreign_eval?(node)
|
|
36
|
+
|
|
37
|
+
body = node.block.location
|
|
38
|
+
block = { receiver: node.receiver.slice, lines: body.start_line..body.end_line, span: node.location.length, calls: {} }
|
|
39
|
+
@results << block
|
|
40
|
+
@open << [ node, block ]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def on_call_node_leave(node)
|
|
44
|
+
@open.pop if @open.last&.first.equal?(node)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
# A call inside one already recorded on its line (`-> { order(:id) }`) is part of that one.
|
|
50
|
+
def record(block, node)
|
|
51
|
+
return if @conditions.any? { |condition| node.location.start_offset.between?(condition.start_offset, condition.end_offset - 1) }
|
|
52
|
+
|
|
53
|
+
calls = (block[:calls][node.location.start_line] ||= [])
|
|
54
|
+
return if calls.any? { |call| call[:range].cover?(node.location.start_offset) }
|
|
55
|
+
|
|
56
|
+
calls << { range: node.location.start_offset...node.location.end_offset, source: CallSiteExpansion.call_source(node) }
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -30,19 +30,24 @@ module RailsAiContext
|
|
|
30
30
|
STORE_MACROS = %i[store store_accessor].to_set.freeze
|
|
31
31
|
|
|
32
32
|
BROADCAST_MACROS = %i[
|
|
33
|
-
broadcasts broadcasts_to broadcasts_refreshes_to
|
|
33
|
+
broadcasts broadcasts_to broadcasts_refreshes broadcasts_refreshes_to
|
|
34
34
|
].to_set.freeze
|
|
35
35
|
|
|
36
36
|
# Model gems' class macros, each listed as written; aasm's block is read in full.
|
|
37
|
+
# `devise` names the modules a model turns on, which no other line says.
|
|
37
38
|
GEM_MACROS = %i[
|
|
39
|
+
devise
|
|
38
40
|
has_paper_trail audited acts_as_paranoid friendly_id
|
|
39
41
|
mount_uploader mount_uploaders monetize
|
|
40
42
|
pg_search_scope multisearchable searchkick
|
|
41
43
|
acts_as_list acts_as_tenant multi_tenant acts_as_taggable acts_as_taggable_on
|
|
42
44
|
has_ancestry acts_as_nested_set has_closure_tree acts_as_tree
|
|
43
|
-
state_machine workflow
|
|
45
|
+
state_machine workflow has_attached_file
|
|
44
46
|
].to_set.freeze
|
|
45
47
|
|
|
48
|
+
# Listed by the macro and its first argument: the options are lambdas that read as noise on one line.
|
|
49
|
+
NAMED_ONLY_MACROS = %i[has_attached_file].to_set.freeze
|
|
50
|
+
|
|
46
51
|
def initialize
|
|
47
52
|
super
|
|
48
53
|
@def_depth = 0
|
|
@@ -160,8 +165,12 @@ module RailsAiContext
|
|
|
160
165
|
end
|
|
161
166
|
|
|
162
167
|
def record_gem_macro(node)
|
|
163
|
-
|
|
164
|
-
text =
|
|
168
|
+
first = node.arguments&.arguments&.first
|
|
169
|
+
text = if NAMED_ONLY_MACROS.include?(node.name) && first
|
|
170
|
+
"#{node.name} #{first.slice}"
|
|
171
|
+
else
|
|
172
|
+
one_line_source(node, upto: node.block&.location&.start_offset).gsub(/\s+/, " ").strip
|
|
173
|
+
end
|
|
165
174
|
adds = monetized_names(node) if node.name == :monetize
|
|
166
175
|
@results << { macro: :gem_macro, name: node.name, text: text, adds: adds.presence,
|
|
167
176
|
location: node.location.start_line, confidence: confidence_for(node) }.compact
|
|
@@ -74,6 +74,7 @@ module RailsAiContext
|
|
|
74
74
|
if node.name == :rename_table
|
|
75
75
|
result[:new_name] = literal_string(args[1])
|
|
76
76
|
end
|
|
77
|
+
result.merge!(default_fields(node, key: true)) if action == :create_table
|
|
77
78
|
|
|
78
79
|
@results << result
|
|
79
80
|
end
|
|
@@ -96,7 +97,7 @@ module RailsAiContext
|
|
|
96
97
|
when :rename_column
|
|
97
98
|
result[:new_name] = literal_string(args[2])
|
|
98
99
|
when :change_column
|
|
99
|
-
result
|
|
100
|
+
result.merge!(column_type: literal_string(args[2]), **default_fields(node))
|
|
100
101
|
when :change_column_null
|
|
101
102
|
# The nullability is the third positional, not a keyword.
|
|
102
103
|
result[:null] = boolean_value(args[2])
|
|
@@ -99,12 +99,13 @@ module RailsAiContext
|
|
|
99
99
|
when :remove_index then result[:columns] = literal_strings(positional.first)
|
|
100
100
|
when :rename_column then result.merge!(column: names[0], new_name: names[1])
|
|
101
101
|
when :rename_index then result.merge!(old_name: names[0], new_name: names[1])
|
|
102
|
-
when :change_column
|
|
102
|
+
when :change_column
|
|
103
|
+
result.merge!(column: names[0], column_type: names[1], **default_fields(node))
|
|
103
104
|
when :change_column_null then result.merge!(column: names[0], null: boolean_value(positional[1]))
|
|
104
105
|
when :change_column_default
|
|
105
106
|
result[:column] = names[0]
|
|
106
107
|
result[:new_default] = extract_value(positional[1]) if positional[1]
|
|
107
|
-
when :add_timestamps then result.merge!(
|
|
108
|
+
when :add_timestamps then result.merge!(default_fields(node))
|
|
108
109
|
when :add_check_constraint, :remove_check_constraint then result[:expression] = names[0]
|
|
109
110
|
end
|
|
110
111
|
result
|
|
@@ -24,8 +24,11 @@ module RailsAiContext
|
|
|
24
24
|
# `direct` and `resolve` define URL helpers and draw no route.
|
|
25
25
|
URL_HELPER_MACROS = %i[direct resolve].freeze
|
|
26
26
|
# Calls that draw routes; an unknown block that makes none of them configures a gem macro.
|
|
27
|
-
ROUTE_METHODS = (VERB_METHODS + %i[match namespace scope resources resource member collection new shallow
|
|
27
|
+
ROUTE_METHODS = (VERB_METHODS + %i[match namespace scope resources resource member collection new nested shallow
|
|
28
28
|
concern concerns with_options controller mount root draw devise_for]).freeze
|
|
29
|
+
# Blocks that draw their routes where they stand: Rails' constraints and defaults, Devise's, and route_translator's.
|
|
30
|
+
# Any other method's block may scope its routes in a way the walk cannot see.
|
|
31
|
+
IN_PLACE_BLOCKS = %i[constraints defaults devise_scope as authenticate authenticated unauthenticated localized].freeze
|
|
29
32
|
# Statement-level calls that draw nothing into the table.
|
|
30
33
|
NON_ROUTING = %i[require require_relative puts p pp print warn raise default_url_options extend include].freeze
|
|
31
34
|
|
|
@@ -38,9 +41,11 @@ module RailsAiContext
|
|
|
38
41
|
# `route_set` answers { prefix:, name_prefix: } for an app class that draws routes.
|
|
39
42
|
# `names` is the set of route names taken so far, shared by every file of one table.
|
|
40
43
|
# `multi_path`: the app's Rails draws every path of `get "/a", "/b"`; 8.1 raises instead.
|
|
41
|
-
|
|
44
|
+
# `api_only`: the app sets config.api_only, so its own table draws no new or edit route.
|
|
45
|
+
def initialize(scope: [], route_set: nil, names: Set.new, multi_path: false, api_only: false)
|
|
42
46
|
super()
|
|
43
47
|
@multi_path = multi_path
|
|
48
|
+
@api_only = api_only
|
|
44
49
|
@stack = scope.map { |frame| frame.merge(node: nil) }
|
|
45
50
|
@route_set = route_set
|
|
46
51
|
@concern_blocks = {}
|
|
@@ -93,6 +98,8 @@ module RailsAiContext
|
|
|
93
98
|
when :member then enter_member_collection(node, :member)
|
|
94
99
|
when :collection then enter_member_collection(node, :collection)
|
|
95
100
|
when :new then enter_member_collection(node, :new)
|
|
101
|
+
# A resource's frame already holds its nested scope, which a direct child draws under too.
|
|
102
|
+
when :nested then push_frame(node, suppress: current_resource.nil?) if node.block
|
|
96
103
|
when :shallow then push_frame(node, shallow: true) if node.block
|
|
97
104
|
when :concern then define_concern(node)
|
|
98
105
|
when :concerns then apply_concerns(node)
|
|
@@ -107,20 +114,25 @@ module RailsAiContext
|
|
|
107
114
|
else
|
|
108
115
|
if statement && node.block
|
|
109
116
|
# An empty block (a commented-out `constraints do`) configures nothing.
|
|
110
|
-
push_frame(node, unknown_block: @routing_calls, pending: []) if node.block.body
|
|
117
|
+
push_frame(node, unknown_block: @routing_calls, pending: [], suppress: !IN_PLACE_BLOCKS.include?(node.name)) if node.block.body
|
|
111
118
|
elsif statement && !NON_ROUTING.include?(node.name)
|
|
112
119
|
call_method_or_count(node)
|
|
113
120
|
end
|
|
114
121
|
end
|
|
115
122
|
end
|
|
116
123
|
|
|
117
|
-
# An unknown block that draws no route is a gem macro (`use_doorkeeper do`), counted once
|
|
124
|
+
# An unknown block that draws no route is a gem macro (`use_doorkeeper do`), counted once,
|
|
125
|
+
# and so is one whose routes it does not draw in place.
|
|
118
126
|
def on_call_node_leave(node)
|
|
119
127
|
return unless @stack.last && @stack.last[:node].equal?(node)
|
|
120
128
|
|
|
121
129
|
frame = @stack.pop
|
|
122
130
|
return unless frame[:unknown_block]
|
|
123
|
-
|
|
131
|
+
if frame[:unknown_block] != @routing_calls
|
|
132
|
+
return emit_dynamic(node) if frame[:suppress]
|
|
133
|
+
|
|
134
|
+
return frame[:pending].each { |call| emit_dynamic(call) }
|
|
135
|
+
end
|
|
124
136
|
|
|
125
137
|
outer = open_unknown_block
|
|
126
138
|
outer ? outer[:pending] << node : emit_dynamic(node)
|
|
@@ -248,6 +260,12 @@ module RailsAiContext
|
|
|
248
260
|
@stack.reverse.find { |f| f[:route_set] }&.dig(:route_set)
|
|
249
261
|
end
|
|
250
262
|
|
|
263
|
+
# The app's config.api_only reaches its own route set only: an engine's
|
|
264
|
+
# table, or a route set class the app draws, keeps its own setting.
|
|
265
|
+
def api_only_table?
|
|
266
|
+
@api_only && @stack.none? { |f| f[:engine] || f[:route_set] }
|
|
267
|
+
end
|
|
268
|
+
|
|
251
269
|
# The controller a route without one of its own takes: the innermost
|
|
252
270
|
# `scope(controller:)` or resource, else Rails' module path alone.
|
|
253
271
|
def current_controller
|
|
@@ -610,7 +628,7 @@ module RailsAiContext
|
|
|
610
628
|
member = layout[:member_path]
|
|
611
629
|
member_name = layout[:member_name]
|
|
612
630
|
controller = resource_controller(name, opts, singular: singular)
|
|
613
|
-
actions = requested_actions(singular ? SINGULAR_ACTIONS : PLURAL_ACTIONS, opts)
|
|
631
|
+
actions = requested_actions(singular ? SINGULAR_ACTIONS : PLURAL_ACTIONS, opts, api_only: api_only_table?)
|
|
614
632
|
|
|
615
633
|
actions.each do |action|
|
|
616
634
|
case action
|
|
@@ -751,7 +769,7 @@ module RailsAiContext
|
|
|
751
769
|
# @return [Array<[String, Boolean]>] each path, and whether it names an action
|
|
752
770
|
def literal_paths(node)
|
|
753
771
|
args = (node.arguments&.arguments || []).select { |a| a.is_a?(Prism::StringNode) || a.is_a?(Prism::SymbolNode) }
|
|
754
|
-
args.
|
|
772
|
+
args.partition { |a| a.is_a?(Prism::StringNode) }.flatten.map { |a| [ a.unescaped, a.is_a?(Prism::SymbolNode) ] }
|
|
755
773
|
end
|
|
756
774
|
|
|
757
775
|
def emit_verb_path(node, verb, segment, action_given, opts, rocket_key)
|
|
@@ -949,8 +967,11 @@ module RailsAiContext
|
|
|
949
967
|
end
|
|
950
968
|
end
|
|
951
969
|
|
|
952
|
-
|
|
970
|
+
# Rails' Resource#default_actions: an api_only route set leaves out new
|
|
971
|
+
# and edit, unless `only:` asks for them by name.
|
|
972
|
+
def requested_actions(all, opts, api_only: false)
|
|
953
973
|
actions = all
|
|
974
|
+
actions -= %i[new edit] if api_only && !opts[:only]
|
|
954
975
|
actions &= Array(opts[:only]).map(&:to_sym) if opts[:only]
|
|
955
976
|
actions -= Array(opts[:except]).map(&:to_sym) if opts[:except]
|
|
956
977
|
actions
|
|
@@ -168,6 +168,7 @@ module RailsAiContext
|
|
|
168
168
|
# id: false / id: :uuid / primary_key: ... decide whether the
|
|
169
169
|
# implicit primary-key column exists and what to call it.
|
|
170
170
|
options: extract_keyword_options(node),
|
|
171
|
+
**default_fields(node, key: true),
|
|
171
172
|
location: node.location.start_line
|
|
172
173
|
}
|
|
173
174
|
end
|
|
@@ -321,8 +322,7 @@ module RailsAiContext
|
|
|
321
322
|
options: options,
|
|
322
323
|
# A proc default (`default: -> { "now()" }`) has no literal value,
|
|
323
324
|
# so keep its source for callers that report defaults verbatim.
|
|
324
|
-
|
|
325
|
-
default_proc: proc_default?(node),
|
|
325
|
+
**default_fields(node),
|
|
326
326
|
location: node.location.start_line
|
|
327
327
|
}
|
|
328
328
|
@results.last[:virtual] = true if virtual
|
|
@@ -12,27 +12,52 @@ module RailsAiContext
|
|
|
12
12
|
# in a block, not in a LambdaNode.
|
|
13
13
|
LAMBDA_METHODS = %i[lambda proc].to_set.freeze
|
|
14
14
|
|
|
15
|
+
# A block these run once per element of the list they are sent to.
|
|
16
|
+
LOOPS = %i[each each_with_index].to_set.freeze
|
|
17
|
+
|
|
18
|
+
def initialize
|
|
19
|
+
super
|
|
20
|
+
@lists = {}
|
|
21
|
+
@loops = []
|
|
22
|
+
end
|
|
23
|
+
|
|
15
24
|
def on_call_node_enter(node)
|
|
25
|
+
enter_loop(node)
|
|
16
26
|
return record_default_scope(node) if node.name == :default_scope && in_scope?(node)
|
|
17
27
|
return unless node.name == :scope && in_scope?(node)
|
|
18
28
|
|
|
19
29
|
name = extract_first_symbol(node)
|
|
20
|
-
|
|
30
|
+
names = name == "[INFERRED]" ? loop_names(node.arguments&.arguments&.first) : [ name ]
|
|
31
|
+
return unless names
|
|
21
32
|
|
|
22
33
|
callable = scope_body_node(node)
|
|
23
34
|
body = callable ? lambda_body_source(callable) : nil
|
|
24
35
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
names.each do |scope_name|
|
|
37
|
+
@results << {
|
|
38
|
+
name: scope_name.to_s,
|
|
39
|
+
body: body,
|
|
40
|
+
required_params: callable ? lambda_required_params(callable) : [],
|
|
41
|
+
location: node.location.start_line,
|
|
42
|
+
# A lambda body sliced verbatim from source IS the scope's ground
|
|
43
|
+
# truth; only scopes whose body can't be extracted (block form,
|
|
44
|
+
# metaprogrammed) are heuristic, and so is a name a loop gives.
|
|
45
|
+
confidence: body && name != "[INFERRED]" ? Confidence::VERIFIED : Confidence::INFERRED,
|
|
46
|
+
owner: @owner_stack.dup
|
|
47
|
+
}
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def on_call_node_leave(node)
|
|
52
|
+
@loops.pop if @loops.last && @loops.last[:node].equal?(node)
|
|
53
|
+
super if defined?(super)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# A list a loop below can name by its constant.
|
|
57
|
+
def on_constant_write_node_enter(node)
|
|
58
|
+
values = literal_list(node.value)
|
|
59
|
+
@lists[node.name] = values if values
|
|
60
|
+
super
|
|
36
61
|
end
|
|
37
62
|
|
|
38
63
|
# Rails calls a `def self.default_scope` the model defines, as it does the macro's lambda.
|
|
@@ -55,6 +80,57 @@ module RailsAiContext
|
|
|
55
80
|
|
|
56
81
|
private
|
|
57
82
|
|
|
83
|
+
# `KINDS.each { |kind| scope kind, -> { where(kind: kind) } }` declares a
|
|
84
|
+
# scope per element. Over a literal list - written inline, or a constant
|
|
85
|
+
# this file assigns one - the names are known before anything runs.
|
|
86
|
+
def enter_loop(node)
|
|
87
|
+
return unless LOOPS.include?(node.name) && node.arguments.nil? && node.block.is_a?(Prism::BlockNode)
|
|
88
|
+
|
|
89
|
+
param = node.block.parameters&.parameters&.requireds&.first
|
|
90
|
+
return unless param.is_a?(Prism::RequiredParameterNode)
|
|
91
|
+
|
|
92
|
+
receiver = node.receiver
|
|
93
|
+
values = receiver.is_a?(Prism::ConstantReadNode) ? @lists[receiver.name] : literal_list(receiver)
|
|
94
|
+
@loops.push({ node: node, param: param.name, values: values }) if values
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# `%w[a b]`, `%i[a b]` or `[:a, "b"]`, frozen or not; nil for anything else.
|
|
98
|
+
def literal_list(node)
|
|
99
|
+
node = node.receiver while node.is_a?(Prism::CallNode) && node.name == :freeze && node.arguments.nil? && node.receiver
|
|
100
|
+
return nil unless node.is_a?(Prism::ArrayNode)
|
|
101
|
+
|
|
102
|
+
values = node.elements.map { |element| literal_string(element) }
|
|
103
|
+
values unless values.any?(&:nil?)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# One name per element of the innermost loop the name reads, or nil when it reads none.
|
|
107
|
+
def loop_names(arg)
|
|
108
|
+
@loops.reverse_each do |loop|
|
|
109
|
+
names = loop[:values].map { |value| loop_name(arg, loop[:param], value) }
|
|
110
|
+
return names.uniq unless names.any?(&:nil?)
|
|
111
|
+
end
|
|
112
|
+
nil
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# The name an element gives: the block's variable as written, `.to_sym`'d,
|
|
116
|
+
# or interpolated into a symbol or a string.
|
|
117
|
+
def loop_name(arg, param, value)
|
|
118
|
+
case arg
|
|
119
|
+
when Prism::LocalVariableReadNode
|
|
120
|
+
value if arg.name == param
|
|
121
|
+
when Prism::CallNode
|
|
122
|
+
loop_name(arg.receiver, param, value) if %i[to_sym to_s intern].include?(arg.name) && arg.arguments.nil?
|
|
123
|
+
when Prism::InterpolatedSymbolNode, Prism::InterpolatedStringNode
|
|
124
|
+
parts = arg.parts.map do |part|
|
|
125
|
+
next part.unescaped if part.is_a?(Prism::StringNode)
|
|
126
|
+
|
|
127
|
+
statements = part.statements&.body if part.is_a?(Prism::EmbeddedStatementsNode)
|
|
128
|
+
loop_name(statements.first, param, value) if statements&.size == 1
|
|
129
|
+
end
|
|
130
|
+
parts.join unless parts.any?(&:nil?)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
58
134
|
# Named `default_scope` and marked `default`: it is no method to call,
|
|
59
135
|
# and Rails stacks every one onto each query the model runs.
|
|
60
136
|
def record_default_scope(node)
|
|
@@ -86,7 +86,7 @@ module RailsAiContext
|
|
|
86
86
|
def middleware_files(middleware_dir)
|
|
87
87
|
return [] unless Dir.exist?(middleware_dir)
|
|
88
88
|
|
|
89
|
-
|
|
89
|
+
FileWalk.glob(middleware_dir, "**/*.rb", root: root).filter_map { |path| describe_middleware(path) }
|
|
90
90
|
end
|
|
91
91
|
|
|
92
92
|
# The name comes from the declaration: a class under lib/middleware may
|
|
@@ -199,7 +199,7 @@ module RailsAiContext
|
|
|
199
199
|
# often as an initializer does.
|
|
200
200
|
def middleware_config_files
|
|
201
201
|
PathResolver.initializer_paths(root) +
|
|
202
|
-
Dir.glob(File.join(root, "config/environments/*.rb")).sort +
|
|
202
|
+
Dir.glob(File.join(root, "config/environments/*.rb")).sort.reject { |path| PathResolver.linked_out?(path, root) } +
|
|
203
203
|
[ File.join(root, "config/application.rb") ].select { |path| File.file?(path) }
|
|
204
204
|
end
|
|
205
205
|
|
|
@@ -20,6 +20,8 @@ module RailsAiContext
|
|
|
20
20
|
# list would read as "up to date".
|
|
21
21
|
pending = pending_migrations
|
|
22
22
|
info[:pending] = pending if pending
|
|
23
|
+
secondary = secondary_databases
|
|
24
|
+
info[:secondary_databases] = secondary if secondary.any?
|
|
23
25
|
info
|
|
24
26
|
end
|
|
25
27
|
|
|
@@ -72,6 +74,31 @@ module RailsAiContext
|
|
|
72
74
|
RailsAiContext.debug_fail(e, nil, label: "current_schema_version")
|
|
73
75
|
end
|
|
74
76
|
|
|
77
|
+
# The other databases `db:migrate` runs here, each from its own
|
|
78
|
+
# migrations_paths, keyed by database.yml name. Pending is what the
|
|
79
|
+
# database's dump records, as the schema section reads that database;
|
|
80
|
+
# one that shares the primary's directories adds no files of its own.
|
|
81
|
+
def secondary_databases
|
|
82
|
+
dumps = SchemaDumpPath.secondaries(root)
|
|
83
|
+
RailsAiContext::DatabaseYml.task_secondaries(root).each_with_object({}) do |(name, entry), found|
|
|
84
|
+
dirs = MigrationReplay.configured_dirs(root, entry)
|
|
85
|
+
next if dirs.nil? || dirs == migrate_dir
|
|
86
|
+
|
|
87
|
+
files = RailsAiContext::PendingMigrations.migration_files(dirs, root: root)
|
|
88
|
+
next if files.empty?
|
|
89
|
+
|
|
90
|
+
format, dump = dumps[name]
|
|
91
|
+
pending = RailsAiContext::PendingMigrations.for(migrate_dir: dirs, applied: RailsAiContext::SchemaVersion.recorded(format, dump), root: root)
|
|
92
|
+
found[name] = {
|
|
93
|
+
total: files.size,
|
|
94
|
+
migrations_paths: dirs.select { |dir| Dir.exist?(dir) }.map { |dir| dir.delete_prefix("#{root.chomp('/')}/") },
|
|
95
|
+
pending: pending
|
|
96
|
+
}.compact
|
|
97
|
+
end
|
|
98
|
+
rescue => e
|
|
99
|
+
RailsAiContext.debug_fail(e, {}, label: "secondary_databases")
|
|
100
|
+
end
|
|
101
|
+
|
|
75
102
|
def migration_stats
|
|
76
103
|
return {} if all_migrations.empty?
|
|
77
104
|
|
|
@@ -72,7 +72,7 @@ module RailsAiContext
|
|
|
72
72
|
rescues.none? { |r| r.cover?(call.location.start_line) }
|
|
73
73
|
end
|
|
74
74
|
items = own.map { |e| [ e[:location], 0, e ] } + called.map { |c| [ c.location.start_line, 1, c ] }
|
|
75
|
-
items.sort_by { |line, kind, _| [ line, kind ] }.each do |_, kind, item|
|
|
75
|
+
items.sort_by.with_index { |(line, kind, _), i| [ line, kind, i ] }.each do |_, kind, item|
|
|
76
76
|
if kind.zero?
|
|
77
77
|
expanded << item.merge(location: anchor || item[:location], order: (order += 1))
|
|
78
78
|
else
|